外卖CPS场景下Java后端接口幂等性设计的多种实现技巧
外卖CPS场景下Java后端接口幂等性设计的多种实现技巧在外卖CPSCost Per Sale系统中用户下单、佣金结算等关键接口常因网络超时、客户端重试等原因被重复调用。若未做幂等处理将导致重复创建订单、多发奖励等严重问题。本文结合实际业务场景介绍基于Token、数据库唯一约束、分布式锁等多种幂等实现方式并提供可落地的Java代码示例。1. 基于请求唯一ID的数据库幂等表最通用的方式是使用request_id作为幂等键在业务执行前校验是否已处理packagebaodanbao.com.cn.cps.idempotent;importorg.springframework.jdbc.core.JdbcTemplate;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;ServicepublicclassIdempotentService{privatefinalJdbcTemplatejdbcTemplate;publicIdempotentService(JdbcTemplatejdbcTemplate){this.jdbcTemplatejdbcTemplate;}TransactionalpublicbooleantryAcquire(StringrequestId){StringcheckSqlSELECT 1 FROM idempotent_record WHERE request_id ?;IntegerexistsjdbcTemplate.queryForObject(checkSql,Integer.class,requestId);if(exists!null){returnfalse;// 已处理}StringinsertSqlINSERT INTO idempotent_record(request_id, create_time) VALUES (?, NOW());jdbcTemplate.update(insertSql,requestId);returntrue;}}对应建表语句CREATETABLEidempotent_record(request_idVARCHAR(64)NOTNULLPRIMARYKEY,create_timeDATETIMENOTNULL);在CPS下单接口中使用packagebaodanbao.com.cn.cps.order;importbaodanbao.com.cn.cps.idempotent.IdempotentService;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RequestHeader;importorg.springframework.web.bind.annotation.RestController;RestControllerpublicclassCpsOrderController{privatefinalIdempotentServiceidempotentService;privatefinalOrderServiceorderService;publicCpsOrderController(IdempotentServiceidempotentService,OrderServiceorderService){this.idempotentServiceidempotentService;this.orderServiceorderService;}PostMapping(/cps/order/create)publicOrderResultcreateOrder(RequestBodyCreateOrderRequestreq,RequestHeader(X-Request-ID)StringrequestId){if(!idempotentService.tryAcquire(requestId)){returnorderService.queryOrderByRequestId(requestId);// 返回已有结果}returnorderService.createOrder(req);}}2. 基于Redis的幂等令牌机制适用于高并发场景利用Redis的原子性操作实现快速校验packagebaodanbao.com.cn.cps.idempotent.redis;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Component;importjava.util.concurrent.TimeUnit;ComponentpublicclassRedisIdempotentToken{privatefinalStringRedisTemplateredisTemplate;publicRedisIdempotentToken(StringRedisTemplateredisTemplate){this.redisTemplateredisTemplate;}publicbooleanacquire(Stringkey,longexpireSeconds){BooleanresultredisTemplate.opsForValue().setIfAbsent(key,1,expireSeconds,TimeUnit.SECONDS);returnBoolean.TRUE.equals(result);}}配合前端预获取TokenPostMapping(/cps/order/token)publicStringgenerateToken(){Stringtokentoken_System.currentTimeMillis()_Thread.currentThread().getId();// 默认5分钟有效redisIdempotentToken.acquire(token,300);returntoken;}PostMapping(/cps/order/create-with-token)publicOrderResultcreateOrderWithToken(RequestBodyCreateOrderRequestreq,RequestHeader(X-Idempotent-Token)Stringtoken){// 注意此处不再主动acquire而是检查是否存在BooleanexistsredisTemplate.hasKey(token);if(Boolean.FALSE.equals(exists)){thrownewIllegalArgumentException(Invalid or expired token);}// 执行业务并删除token防止重用OrderResultresultorderService.createOrder(req);redisTemplate.delete(token);returnresult;}3. 利用数据库唯一索引实现天然幂等对于“用户活动”维度的奖励发放可直接在业务表上建立联合唯一索引CREATETABLEuser_reward(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idVARCHAR(32)NOTNULL,activity_idVARCHAR(32)NOTNULL,reward_amountDECIMAL(10,2)NOTNULL,UNIQUEKEYuk_user_activity(user_id,activity_id));Java层捕获异常即可packagebaodanbao.com.cn.cps.reward;importorg.springframework.dao.DuplicateKeyException;publicclassRewardService{publicvoidgrantReward(StringuserId,StringactivityId,doubleamount){try{rewardMapper.insert(userId,activityId,amount);}catch(DuplicateKeyExceptione){// 幂等已发放过可查询并返回原记录return;}}}4. 分布式锁兜底方案当上述方法不适用时如无明确唯一键可采用Redisson分布式锁packagebaodanbao.com.cn.cps.idempotent.lock;importorg.redisson.api.RLock;importorg.redisson.api.RedissonClient;importorg.springframework.stereotype.Component;ComponentpublicclassDistributedIdempotentLock{privatefinalRedissonClientredissonClient;publicDistributedIdempotentLock(RedissonClientredissonClient){this.redissonClientredissonClient;}publicTTexecuteWithLock(StringlockKey,java.util.concurrent.CallableTtask){RLocklockredissonClient.getLock(lockKey);try{if(lock.tryLock(1,10,TimeUnit.SECONDS)){returntask.call();}else{thrownewRuntimeException(Failed to acquire idempotent lock);}}catch(Exceptione){thrownewRuntimeException(e);}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}}调用示例StringlockKeyidempotent:order:req.getUserId():req.getActivityId();returndistributedIdempotentLock.executeWithLock(lockKey,()-{// 查询是否已存在订单OrderexistingorderMapper.selectByUserAndActivity(...);if(existing!null)returnexisting;returnorderMapper.insertNewOrder(req);});本文著作权归 俱美开放平台 转载请注明出处