Java后端如何通过分布式锁避免美团外卖API并发写操作冲突在对接美团外卖API如霸王餐核销、库存同步的高并发场景下多个服务实例可能同时尝试修改同一份资源例如同一个优惠券的核销状态、同一商品的库存数量这极易导致数据不一致或超卖问题。为了解决跨JVM的并发安全问题我们需要引入分布式锁机制。本文将结合baodanbao.com.cn包结构详细解析如何利用Redis实现分布式锁来保护核心写操作。1. 并发冲突场景分析假设系统接收到美团的“核销通知”或定时任务批量拉取核销状态。当两个线程同时处理同一张券tokenA时可能会发生以下情况线程1查询数据库发现券状态为“未核销”。线程2查询数据库发现券状态也为“未核销”因为线程1还没来得及更新。线程1执行核销逻辑更新状态为“已核销”。线程2执行核销逻辑也更新状态为“已核销”。虽然最终状态是对的但如果核销逻辑包含“发送短信”或“积分发放”就会导致用户收到两条短信。因此我们需要确保针对同一个token的操作是串行的。2. 基于Redis的分布式锁实现我们将使用Redis的SET key value NX PX命令来实现锁。其中NX表示只有键不存在时才设置避免死锁PX设置毫秒级过期时间防止节点宕机导致锁无法释放。分布式锁工具类packagecom.baodanbao.cn.util;importredis.clients.jedis.Jedis;importredis.clients.jedis.params.SetParams;importjava.util.Collections;publicclassDistributedLock{privatestaticfinalStringLOCK_SUCCESSOK;privatestaticfinalLongRELEASE_SUCCESS1L;privatestaticfinalStringSET_IF_NOT_EXISTNX;privatestaticfinalStringSET_WITH_EXPIRE_TIMEPX;privateJedisjedis;privateStringlockKey;privateStringrequestId;privateintexpireTime;publicDistributedLock(Jedisjedis,StringlockKey,StringrequestId,intexpireTime){this.jedisjedis;this.lockKeylockKey;this.requestIdrequestId;this.expireTimeexpireTime;}/** * 尝试获取锁 */publicbooleantryLock(){Stringresultjedis.set(lockKey,requestId,SetParams.setParams().nx().px(expireTime));returnLOCK_SUCCESS.equals(result);}/** * 释放锁Lua脚本保证原子性 */publicbooleanunlock(){Stringscriptif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;Objectresultjedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));returnRELEASE_SUCCESS.equals(result);}}3. 在美团API写操作中应用锁假设我们正在处理美团推送的“霸王餐核销回调”。我们需要根据券码token加锁确保同一时间只有一个线程能处理该券。服务实现类packagecom.baodanbao.cn.service.impl;importcom.baodanbao.cn.model.ApiResponse;importcom.baodanbao.cn.model.BaWangCanRecord;importcom.baodanbao.cn.service.MeituanCallbackService;importcom.baodanbao.cn.util.DistributedLock;importcom.baodanbao.cn.util.JedisPoolUtil;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importjava.util.UUID;publicclassMeituanCallbackServiceImplimplementsMeituanCallbackService{privatestaticfinalLoggerloggerLoggerFactory.getLogger(MeituanCallbackServiceImpl.class);privatestaticfinalintLOCK_EXPIRE_TIME5000;// 锁过期时间5秒OverridepublicApiResponseStringhandleWriteOffCallback(Stringtoken,StringorderId){Jedisjedisnull;DistributedLocklocknull;try{jedisJedisPoolUtil.getResource();// 1. 生成唯一请求ID防止锁被其他线程误删StringrequestIdUUID.randomUUID().toString();// 2. 锁的Key为业务唯一标识StringlockKeymt:writeoff:lock:token;locknewDistributedLock(jedis,lockKey,requestId,LOCK_EXPIRE_TIME);// 3. 尝试获取锁if(lock.tryLock()){logger.info(成功获取锁开始处理核销: token{},token);// --- 此处为临界区核心写操作 ---// 1. 查询数据库确认状态BaWangCanRecordrecordqueryFromDB(token);if(recordnull){returnApiResponse.fail(订单不存在);}if(USED.equals(record.getStatus())){returnApiResponse.success(已核销);// 幂等性处理}// 2. 执行核销业务逻辑更新DB、发消息等booleansuccessupdateDBAndSendMessage(record);if(success){returnApiResponse.success(核销成功);}else{returnApiResponse.fail(核销失败);}// --- 临界区结束 ---}else{// 获取锁失败可能是重复请求或系统繁忙logger.warn(获取锁失败可能有其他线程正在处理: token{},token);returnApiResponse.fail(系统繁忙请稍后重试);}}catch(Exceptione){logger.error(处理美团核销回调异常,e);returnApiResponse.fail(系统异常);}finally{// 4. 释放锁if(lock!null){lock.unlock();}if(jedis!null){jedis.close();}}}// 模拟数据库查询privateBaWangCanRecordqueryFromDB(Stringtoken){// 实际查询逻辑returnnewBaWangCanRecord();}// 模拟更新数据库及发送消息privatebooleanupdateDBAndSendMessage(BaWangCanRecordrecord){// 1. 更新数据库状态// 2. 发送MQ消息通知其他系统// 3. 记录日志returntrue;}}4. 优化使用AOP与自定义注解为了减少模板代码我们可以使用Spring AOP结合自定义注解将加锁逻辑从业务代码中剥离。自定义锁注解packagecom.baodanbao.cn.annotation;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceDistributedLockAnnotation{// 锁的前缀Stringprefix()defaultmt:lock;// 锁的过期时间毫秒intexpire()default5000;// 键的分隔符Stringdelimiter()default:;}切面逻辑简化版packagecom.baodanbao.cn.aop;importcom.baodanbao.cn.annotation.DistributedLockAnnotation;importcom.baodanbao.cn.util.DistributedLock;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.springframework.stereotype.Component;AspectComponentpublicclassLockAspect{Around(annotation(distributedLock))publicObjectaround(ProceedingJoinPointpjp,DistributedLockAnnotationdistributedLock)throwsThrowable{// 这里需要从方法参数中解析出业务Key例如token// 为了简化假设第一个参数是tokenObject[]argspjp.getArgs();Stringtokenargs[0].toString();StringlockKeydistributedLock.prefix()distributedLock.delimiter()token;// 实际应用中需要注入Jedis及requestId生成逻辑// DistributedLock lock new DistributedLock(...);// if(lock.tryLock()){ try{ return pjp.proceed();} finally{lock.unlock();}}// 伪代码执行业务方法returnpjp.proceed();}}使用注解的业务方法DistributedLockAnnotation(prefixmt:writeoff,expire5000)publicApiResponseStringhandleWriteOffCallbackAnnotated(Stringtoken,StringorderId){// 业务逻辑无需关心加锁细节// ...}本文著作权归 俱美开放平台 转载请注明出处
Java后端如何通过分布式锁避免美团外卖API并发写操作冲突
Java后端如何通过分布式锁避免美团外卖API并发写操作冲突在对接美团外卖API如霸王餐核销、库存同步的高并发场景下多个服务实例可能同时尝试修改同一份资源例如同一个优惠券的核销状态、同一商品的库存数量这极易导致数据不一致或超卖问题。为了解决跨JVM的并发安全问题我们需要引入分布式锁机制。本文将结合baodanbao.com.cn包结构详细解析如何利用Redis实现分布式锁来保护核心写操作。1. 并发冲突场景分析假设系统接收到美团的“核销通知”或定时任务批量拉取核销状态。当两个线程同时处理同一张券tokenA时可能会发生以下情况线程1查询数据库发现券状态为“未核销”。线程2查询数据库发现券状态也为“未核销”因为线程1还没来得及更新。线程1执行核销逻辑更新状态为“已核销”。线程2执行核销逻辑也更新状态为“已核销”。虽然最终状态是对的但如果核销逻辑包含“发送短信”或“积分发放”就会导致用户收到两条短信。因此我们需要确保针对同一个token的操作是串行的。2. 基于Redis的分布式锁实现我们将使用Redis的SET key value NX PX命令来实现锁。其中NX表示只有键不存在时才设置避免死锁PX设置毫秒级过期时间防止节点宕机导致锁无法释放。分布式锁工具类packagecom.baodanbao.cn.util;importredis.clients.jedis.Jedis;importredis.clients.jedis.params.SetParams;importjava.util.Collections;publicclassDistributedLock{privatestaticfinalStringLOCK_SUCCESSOK;privatestaticfinalLongRELEASE_SUCCESS1L;privatestaticfinalStringSET_IF_NOT_EXISTNX;privatestaticfinalStringSET_WITH_EXPIRE_TIMEPX;privateJedisjedis;privateStringlockKey;privateStringrequestId;privateintexpireTime;publicDistributedLock(Jedisjedis,StringlockKey,StringrequestId,intexpireTime){this.jedisjedis;this.lockKeylockKey;this.requestIdrequestId;this.expireTimeexpireTime;}/** * 尝试获取锁 */publicbooleantryLock(){Stringresultjedis.set(lockKey,requestId,SetParams.setParams().nx().px(expireTime));returnLOCK_SUCCESS.equals(result);}/** * 释放锁Lua脚本保证原子性 */publicbooleanunlock(){Stringscriptif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;Objectresultjedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));returnRELEASE_SUCCESS.equals(result);}}3. 在美团API写操作中应用锁假设我们正在处理美团推送的“霸王餐核销回调”。我们需要根据券码token加锁确保同一时间只有一个线程能处理该券。服务实现类packagecom.baodanbao.cn.service.impl;importcom.baodanbao.cn.model.ApiResponse;importcom.baodanbao.cn.model.BaWangCanRecord;importcom.baodanbao.cn.service.MeituanCallbackService;importcom.baodanbao.cn.util.DistributedLock;importcom.baodanbao.cn.util.JedisPoolUtil;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importjava.util.UUID;publicclassMeituanCallbackServiceImplimplementsMeituanCallbackService{privatestaticfinalLoggerloggerLoggerFactory.getLogger(MeituanCallbackServiceImpl.class);privatestaticfinalintLOCK_EXPIRE_TIME5000;// 锁过期时间5秒OverridepublicApiResponseStringhandleWriteOffCallback(Stringtoken,StringorderId){Jedisjedisnull;DistributedLocklocknull;try{jedisJedisPoolUtil.getResource();// 1. 生成唯一请求ID防止锁被其他线程误删StringrequestIdUUID.randomUUID().toString();// 2. 锁的Key为业务唯一标识StringlockKeymt:writeoff:lock:token;locknewDistributedLock(jedis,lockKey,requestId,LOCK_EXPIRE_TIME);// 3. 尝试获取锁if(lock.tryLock()){logger.info(成功获取锁开始处理核销: token{},token);// --- 此处为临界区核心写操作 ---// 1. 查询数据库确认状态BaWangCanRecordrecordqueryFromDB(token);if(recordnull){returnApiResponse.fail(订单不存在);}if(USED.equals(record.getStatus())){returnApiResponse.success(已核销);// 幂等性处理}// 2. 执行核销业务逻辑更新DB、发消息等booleansuccessupdateDBAndSendMessage(record);if(success){returnApiResponse.success(核销成功);}else{returnApiResponse.fail(核销失败);}// --- 临界区结束 ---}else{// 获取锁失败可能是重复请求或系统繁忙logger.warn(获取锁失败可能有其他线程正在处理: token{},token);returnApiResponse.fail(系统繁忙请稍后重试);}}catch(Exceptione){logger.error(处理美团核销回调异常,e);returnApiResponse.fail(系统异常);}finally{// 4. 释放锁if(lock!null){lock.unlock();}if(jedis!null){jedis.close();}}}// 模拟数据库查询privateBaWangCanRecordqueryFromDB(Stringtoken){// 实际查询逻辑returnnewBaWangCanRecord();}// 模拟更新数据库及发送消息privatebooleanupdateDBAndSendMessage(BaWangCanRecordrecord){// 1. 更新数据库状态// 2. 发送MQ消息通知其他系统// 3. 记录日志returntrue;}}4. 优化使用AOP与自定义注解为了减少模板代码我们可以使用Spring AOP结合自定义注解将加锁逻辑从业务代码中剥离。自定义锁注解packagecom.baodanbao.cn.annotation;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceDistributedLockAnnotation{// 锁的前缀Stringprefix()defaultmt:lock;// 锁的过期时间毫秒intexpire()default5000;// 键的分隔符Stringdelimiter()default:;}切面逻辑简化版packagecom.baodanbao.cn.aop;importcom.baodanbao.cn.annotation.DistributedLockAnnotation;importcom.baodanbao.cn.util.DistributedLock;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.springframework.stereotype.Component;AspectComponentpublicclassLockAspect{Around(annotation(distributedLock))publicObjectaround(ProceedingJoinPointpjp,DistributedLockAnnotationdistributedLock)throwsThrowable{// 这里需要从方法参数中解析出业务Key例如token// 为了简化假设第一个参数是tokenObject[]argspjp.getArgs();Stringtokenargs[0].toString();StringlockKeydistributedLock.prefix()distributedLock.delimiter()token;// 实际应用中需要注入Jedis及requestId生成逻辑// DistributedLock lock new DistributedLock(...);// if(lock.tryLock()){ try{ return pjp.proceed();} finally{lock.unlock();}}// 伪代码执行业务方法returnpjp.proceed();}}使用注解的业务方法DistributedLockAnnotation(prefixmt:writeoff,expire5000)publicApiResponseStringhandleWriteOffCallbackAnnotated(Stringtoken,StringorderId){// 业务逻辑无需关心加锁细节// ...}本文著作权归 俱美开放平台 转载请注明出处