外卖CPS场景下Java后端接口幂等性设计的多种实现技巧
在外卖CPS(Cost 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.jdbcTemplate=jdbcTemplate;}@TransactionalpublicbooleantryAcquire(StringrequestId){StringcheckSql="SELECT 1 FROM idempotent_record WHERE request_id = ?";Integerexists=jdbcTemplate.queryForObject(checkSql,Integer.class,requestId);if(exists!=null){returnfalse;// 已处理}StringinsertSql="INSERT 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.idempotentService=idempotentService;this.orderService=orderService;}@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.redisTemplate=redisTemplate;}publicbooleanacquire(Stringkey,longexpireSeconds){Booleanresult=redisTemplate.opsForValue().setIfAbsent(key,"1",expireSeconds,TimeUnit.SECONDS);returnBoolean.TRUE.equals(result);}}配合前端预获取Token:
@PostMapping("/cps/order/token")publicStringgenerateToken(){Stringtoken="token_"+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,而是检查是否存在Booleanexists=redisTemplate.hasKey(token);if(Boolean.FALSE.equals(exists)){thrownewIllegalArgumentException("Invalid or expired token");}// 执行业务并删除token(防止重用)OrderResultresult=orderService.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.redissonClient=redissonClient;}public<T>TexecuteWithLock(StringlockKey,java.util.concurrent.Callable<T>task){RLocklock=redissonClient.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();}}}}调用示例:
StringlockKey="idempotent:order:"+req.getUserId()+":"+req.getActivityId();returndistributedIdempotentLock.executeWithLock(lockKey,()->{// 查询是否已存在订单Orderexisting=orderMapper.selectByUserAndActivity(...);if(existing!=null)returnexisting;returnorderMapper.insertNewOrder(req);});本文著作权归 俱美开放平台 ,转载请注明出处!