SpringBoot 接口幂等性的5种实现方案
1. 幂等性的概念与重要性1.1 什么是幂等性幂等性Idempotence是指无论对同一操作执行多少次结果都是相同的。在API设计中幂等性确保了重复请求不会导致意外的副作用。1.2 为什么需要幂等性网络波动网络不稳定可能导致请求重复发送客户端重试客户端在超时后可能会重试请求用户误操作用户可能重复点击提交按钮分布式系统分布式环境下消息可能重复投递1.3 幂等性的应用场景支付接口订单创建接口数据更新接口资源创建接口2. 实现方案方案一基于数据库唯一索引实现原理通过在数据库表中添加唯一索引确保重复的请求不会导致数据重复插入。核心源码数据库表设计CREATETABLEorder(idbigint(20)NOTNULLAUTO_INCREMENT,order_novarchar(32)NOTNULLCOMMENT订单号,user_idbigint(20)NOTNULLCOMMENT用户ID,amountdecimal(10,2)NOTNULLCOMMENT金额,statusint(11)NOTNULLCOMMENT状态,create_timedatetimeNOTNULLCOMMENT创建时间,PRIMARYKEY(id),UNIQUEKEYuk_order_no(order_no)COMMENT订单号唯一索引)ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT订单表;Service层实现ServicepublicclassOrderService{AutowiredprivateOrderMapperorderMapper;TransactionalpublicOrdercreateOrder(OrderCreateRequestrequest){// 生成唯一订单号StringorderNogenerateOrderNo();OrderordernewOrder();order.setOrderNo(orderNo);order.setUserId(request.getUserId());order.setAmount(request.getAmount());order.setStatus(0);order.setCreateTime(newDate());try{orderMapper.insert(order);returnorder;}catch(DuplicateKeyExceptione){// 唯一索引冲突说明订单已存在returnorderMapper.selectByOrderNo(orderNo);}}privateStringgenerateOrderNo(){// 生成唯一订单号的逻辑returnORDSystem.currentTimeMillis()RandomUtil.randomString(6);}}优劣势分析优点实现简单依赖数据库自身的约束可靠性高数据库保证唯一性缺点仅适用于插入操作并发性能受数据库限制错误处理需要捕获异常适用场景订单创建用户注册资源分配方案二基于Token令牌实现原理客户端请求获取令牌服务端生成唯一令牌并存储客户端携带令牌发起请求服务端验证令牌并执行操作操作完成后删除令牌核心源码Token生成与验证ComponentpublicclassIdempotentTokenService{AutowiredprivateRedisTemplateString,ObjectredisTemplate;/** * 生成幂等性令牌 */publicStringgenerateToken(){StringtokenUUID.randomUUID().toString();// 存储令牌设置过期时间为5分钟redisTemplate.opsForValue().set(idempotent_token:token,token,5,TimeUnit.MINUTES);returntoken;}/** * 验证令牌 */publicbooleanvalidateToken(Stringtoken){if(StringUtils.isEmpty(token)){returnfalse;}Stringkeyidempotent_token:token;// 原子操作获取并删除令牌returnredisTemplate.delete(key);}}Controller层实现RestControllerRequestMapping(/api/order)publicclassOrderController{AutowiredprivateIdempotentTokenServiceidempotentTokenService;AutowiredprivateOrderServiceorderService;/** * 获取幂等性令牌 */GetMapping(/token)publicResponseEntityStringgetToken(){StringtokenidempotentTokenService.generateToken();returnResponseEntity.ok(token);}/** * 创建订单 */PostMappingpublicResponseEntityOrdercreateOrder(RequestHeader(X-Idempotent-Token)Stringtoken,RequestBodyOrderCreateRequestrequest){// 验证令牌if(!idempotentTokenService.validateToken(token)){returnResponseEntity.badRequest().build();}OrderorderorderService.createOrder(request);returnResponseEntity.ok(order);}}优劣势分析优点适用范围广可用于各种操作实现灵活可自定义过期时间性能较好基于Redis操作缺点需要额外的Redis存储增加了请求次数获取令牌客户端需要额外处理令牌适用场景支付接口表单提交数据更新方案三基于分布式锁实现原理通过分布式锁确保同一时间只有一个请求执行操作避免并发问题。核心源码分布式锁实现ComponentpublicclassDistributedLockService{AutowiredprivateRedisTemplateString,ObjectredisTemplate;/** * 获取分布式锁 */publicbooleantryLock(Stringkey,longexpireTime){BooleanresultredisTemplate.opsForValue().setIfAbsent(key,locked,expireTime,TimeUnit.MILLISECONDS);returnBoolean.TRUE.equals(result);}/** * 释放分布式锁 */publicvoidreleaseLock(Stringkey){redisTemplate.delete(key);}}Service层实现ServicepublicclassPaymentService{AutowiredprivateDistributedLockServicedistributedLockService;AutowiredprivatePaymentMapperpaymentMapper;publicPaymentprocessPayment(PaymentRequestrequest){// 生成锁键StringlockKeypayment_lock:request.getOrderNo();try{// 尝试获取锁设置过期时间为30秒if(!distributedLockService.tryLock(lockKey,30000)){thrownewBusinessException(支付处理中请稍后再试);}// 检查是否已支付PaymentexistingPaymentpaymentMapper.selectByOrderNo(request.getOrderNo());if(existingPayment!nullexistingPayment.getStatus()1){returnexistingPayment;}// 处理支付逻辑PaymentpaymentnewPayment();payment.setOrderNo(request.getOrderNo());payment.setAmount(request.getAmount());payment.setStatus(1);payment.setCreateTime(newDate());paymentMapper.insert(payment);returnpayment;}finally{// 释放锁distributedLockService.releaseLock(lockKey);}}}优劣势分析优点适用范围广可用于各种操作支持并发场景实现灵活可自定义锁的粒度缺点依赖分布式锁实现可能存在锁竞争问题实现复杂度较高适用场景支付处理库存扣减数据同步方案四基于Redis实现实现原理通过Redis的原子操作如SETNX实现幂等性控制。核心源码Redis幂等性实现ComponentpublicclassRedisIdempotentService{AutowiredprivateRedisTemplateString,ObjectredisTemplate;/** * 检查并设置幂等性键 */publicbooleancheckAndSet(Stringkey,longexpireTime){BooleanresultredisTemplate.opsForValue().setIfAbsent(key,processed,expireTime,TimeUnit.MILLISECONDS);returnBoolean.TRUE.equals(result);}/** * 生成幂等性键 */publicStringgenerateKey(Stringprefix,String...params){StringBuildersbnewStringBuilder(prefix);for(Stringparam:params){sb.append(:).append(param);}returnsb.toString();}}Service层实现ServicepublicclassOrderService{AutowiredprivateRedisIdempotentServiceredisIdempotentService;AutowiredprivateOrderMapperorderMapper;publicOrdercreateOrder(OrderCreateRequestrequest){// 生成幂等性键StringkeyredisIdempotentService.generateKey(order_create,request.getUserId(),request.getProductId());// 检查是否已处理if(!redisIdempotentService.checkAndSet(key,30000)){thrownewBusinessException(订单已处理请不要重复提交);}// 创建订单逻辑OrderordernewOrder();order.setUserId(request.getUserId());order.setProductId(request.getProductId());order.setAmount(request.getAmount());order.setStatus(0);order.setCreateTime(newDate());orderMapper.insert(order);returnorder;}}优劣势分析优点实现简单基于Redis原子操作性能优异Redis操作速度快适用范围广缺点依赖Redis服务需要合理设置过期时间键的设计需要考虑唯一性适用场景高频接口表单提交数据更新方案五基于乐观锁实现原理通过版本号或时间戳实现乐观锁确保数据更新的一致性。核心源码数据库表设计CREATETABLEproduct(idbigint(20)NOTNULLAUTO_INCREMENT,namevarchar(100)NOTNULLCOMMENT产品名称,stockint(11)NOTNULLCOMMENT库存,versionint(11)NOTNULLDEFAULT0COMMENT版本号,update_timedatetimeNOTNULLCOMMENT更新时间,PRIMARYKEY(id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT产品表;Service层实现ServicepublicclassProductService{AutowiredprivateProductMapperproductMapper;TransactionalpublicbooleandeductStock(LongproductId,intquantity){// 获取产品信息ProductproductproductMapper.selectById(productId);if(productnull){thrownewBusinessException(产品不存在);}// 检查库存if(product.getStock()quantity){thrownewBusinessException(库存不足);}// 更新库存使用乐观锁intresultproductMapper.updateStock(productId,quantity,product.getVersion());if(result0){// 更新失败说明版本号已变化thrownewBusinessException(库存更新失败请重试);}returntrue;}}Mapper层实现publicinterfaceProductMapper{Update(UPDATE product SET stock stock - #{quantity}, version version 1, update_time NOW() WHERE id #{productId} AND version #{version})intupdateStock(Param(productId)LongproductId,Param(quantity)intquantity,Param(version)intversion);}优劣势分析优点实现简单基于数据库版本号无锁设计并发性能好适用于更新操作缺点仅适用于更新操作可能需要重试机制实现复杂度较高适用场景库存扣减数据更新状态变更3. 方案比较方案实现复杂度性能可靠性适用场景依赖基于数据库唯一索引低中高插入操作数据库基于Token令牌中高高各种操作Redis基于分布式锁高中高并发操作Redis/Zookeeper基于Redis实现中高高各种操作Redis基于乐观锁中高中更新操作数据库4. 最佳实践4.1 选择合适的方案插入操作优先使用基于数据库唯一索引的方案更新操作优先使用基于乐观锁的方案高频接口优先使用基于Redis的方案并发操作优先使用基于分布式锁的方案通用场景优先使用基于Token令牌的方案4.2 性能优化合理设置过期时间根据业务场景设置适当的过期时间批量处理对于批量操作考虑批量验证幂等性异步处理对于耗时操作考虑异步处理幂等性验证缓存优化使用Redis集群提高性能和可靠性4.3 安全性考虑防止Token泄露使用HTTPS传输Token防止暴力破解限制Token验证失败次数防止重放攻击使用时间戳或随机数数据加密对敏感数据进行加密存储4.4 代码示例综合方案RestControllerRequestMapping(/api/payment)publicclassPaymentController{AutowiredprivateIdempotentTokenServiceidempotentTokenService;AutowiredprivatePaymentServicepaymentService;GetMapping(/token)publicResponseEntityStringgetToken(){StringtokenidempotentTokenService.generateToken();returnResponseEntity.ok(token);}PostMappingpublicResponseEntityPaymentprocessPayment(RequestHeader(X-Idempotent-Token)Stringtoken,RequestBodyPaymentRequestrequest){// 1. 验证Tokenif(!idempotentTokenService.validateToken(token)){returnResponseEntity.badRequest().body(null);}// 2. 处理支付PaymentpaymentpaymentService.processPayment(request);returnResponseEntity.ok(payment);}}ServicepublicclassPaymentService{AutowiredprivateDistributedLockServicedistributedLockService;AutowiredprivatePaymentMapperpaymentMapper;publicPaymentprocessPayment(PaymentRequestrequest){StringlockKeypayment_lock:request.getOrderNo();try{// 3. 获取分布式锁if(!distributedLockService.tryLock(lockKey,30000)){thrownewBusinessException(支付处理中请稍后再试);}// 4. 检查是否已支付PaymentexistingPaymentpaymentMapper.selectByOrderNo(request.getOrderNo());if(existingPayment!nullexistingPayment.getStatus()1){returnexistingPayment;}// 5. 处理支付逻辑PaymentpaymentnewPayment();payment.setOrderNo(request.getOrderNo());payment.setAmount(request.getAmount());payment.setStatus(1);payment.setCreateTime(newDate());paymentMapper.insert(payment);returnpayment;}finally{// 6. 释放锁distributedLockService.releaseLock(lockKey);}}}5. 总结幂等性是API设计中非常重要的特性它确保了系统的可靠性和一致性。在SpringBoot应用中我们可以通过多种方式实现接口幂等性基于数据库唯一索引简单可靠适用于插入操作基于Token令牌灵活通用适用于各种操作基于分布式锁支持并发适用于复杂场景基于Redis实现性能优异适用于高频接口基于乐观锁无锁设计适用于更新操作在实际应用中我们应该根据具体的业务场景选择合适的实现方案也可以结合多种方案来提高系统的可靠性和性能。通过合理的幂等性设计可以有效防止重复请求导致的问题提高系统的稳定性和用户体验。 福利时间如果你正在备战面试或者想要学习其他知识给大家推荐一个宝藏知识库作者整理了一些列 Java 程序员需要掌握的核心知识有需要的自取不谢。知识库地址https://farerboy.com/