
文章目录VoucherOrderController.javaIVoucherOrderService 及其实现类ILock 及其实现类lua 脚本基于 SETNXsetIfAbsent手写分布式锁问题1- 不可重入问题2- 不可重试问题3- 超时释放问题4- Redis主从一致性问题主从异步复制锁丢失VoucherOrderController.javapackagecom.hmdp.controller;importcom.hmdp.dto.Result;importcom.hmdp.service.IVoucherOrderService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;RestControllerRequestMapping(/voucher-order)publicclassVoucherOrderController{AutowiredprivateIVoucherOrderServicevoucherOrderService;// redis分布式锁 - 版本PostMapping(seckill-redis/{id})publicResultseckillVouvherRedis(PathVariable(id)LongvoucherId){returnvoucherOrderService.getOrderBySeckillVoucherRedis(voucherId);}}IVoucherOrderService 及其实现类packagecom.hmdp.service;importcom.baomidou.mybatisplus.extension.service.IService;importcom.hmdp.dto.Result;importcom.hmdp.entity.VoucherOrder;publicinterfaceIVoucherOrderServiceextendsIServiceVoucherOrder{ResultgetOrderBySeckillVoucher(LongvoucherId);ResultgetOrderBySeckillVoucherRedis(LongvoucherId);}packagecom.hmdp.service.impl;importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;importcom.hmdp.dto.Result;importcom.hmdp.entity.SeckillVoucher;importcom.hmdp.entity.VoucherOrder;importcom.hmdp.mapper.VoucherOrderMapper;importcom.hmdp.service.ISeckillVoucherService;importcom.hmdp.service.IVoucherOrderService;importcom.hmdp.utils.RedisIdWorker;importcom.hmdp.utils.SimpleRedisLock;importcom.hmdp.utils.UserHolder;importorg.springframework.aop.framework.AopContext;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importjava.time.LocalDateTime;ServicepublicclassVoucherOrderServiceImplextendsServiceImplVoucherOrderMapper,VoucherOrderimplementsIVoucherOrderService{privatefinalRedisIdWorkerredisIdWorker;privatefinalISeckillVoucherServiceseckillVoucherService;privatefinalStringRedisTemplatestringRedisTemplate;publicVoucherOrderServiceImpl(RedisIdWorkerredisIdWorker,ISeckillVoucherServiceseckillVoucherService,StringRedisTemplatestringRedisTemplate){this.redisIdWorkerredisIdWorker;this.seckillVoucherServiceseckillVoucherService;this.stringRedisTemplatestringRedisTemplate;}// redis分布式锁 - 版本OverridepublicResultgetOrderBySeckillVoucherRedis(LongvoucherId){LonguserIdUserHolder.getUser().getId();if(userIdnull){returnResult.fail(用户未登录);}SeckillVouchervoucherseckillVoucherService.getById(voucherId);if(vouchernull){returnResult.fail(优惠券不存在);}if(voucher.getBeginTime().isAfter(LocalDateTime.now())){returnResult.fail(活动未开始);}if(voucher.getEndTime().isBefore(LocalDateTime.now())){returnResult.fail(活动已结束);}if(voucher.getStock()1){returnResult.fail(优惠券库存不足);}// 创建锁对象keylock:order:userIdSimpleRedisLocklocknewSimpleRedisLock(order:userId,stringRedisTemplate);// 获取锁锁超时1200秒booleanisLocklock.tryLock(1200);// 加锁失败 → 不允许重复下单if(!isLock){returnResult.fail(不允许重复下单);}try{// todo 启动类记得开启代理暴露EnableAspectJAutoProxy(exposeProxy true)否则AopContext.currentProxy()报错。VoucherOrderServiceImplproxy(VoucherOrderServiceImpl)AopContext.currentProxy();returnproxy.createVoucherOrder(voucherId,userId);}finally{// 无论正常/异常都释放锁lock.unlock();}}TransactionalpublicResultcreateVoucherOrder(LongvoucherId,LonguserId){// todo 一人一单IntegerorderCountlambdaQuery().eq(VoucherOrder::getUserId,userId).eq(VoucherOrder::getVoucherId,voucherId).count();if(orderCount0){returnResult.fail(一人仅限一单);}// todo 扣库存booleansuccessseckillVoucherService.lambdaUpdate().setSql(stock stock - 1).eq(SeckillVoucher::getVoucherId,voucherId).gt(SeckillVoucher::getStock,0).update();if(!success){returnResult.fail(券的库存不足);}// todo 创建订单VoucherOrdervoucherOrdernewVoucherOrder();voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);longorderIdredisIdWorker.nextId(order);voucherOrder.setId(orderId);save(voucherOrder);returnResult.ok(orderId);}}ILock 及其实现类packagecom.hmdp.utils;publicinterfaceILock{/** * todo 尝试获取锁 * param timeoutSec 锁持有的超时时间过期后自动释放 * return true获取锁成功; false获取锁失败 */booleantryLock(longtimeoutSec);/** * todo 释放锁 */voidunlock();}packagecom.hmdp.utils;importcn.hutool.core.lang.UUID;importorg.springframework.core.io.ClassPathResource;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importjava.util.Collections;importjava.util.concurrent.TimeUnit;publicclassSimpleRedisLockimplementsILock{privatestaticfinalDefaultRedisScriptLongUNLOCK_SCRIPT;static{UNLOCK_SCRIPTnewDefaultRedisScript();// todo 读取resources目录下的 unlock.lua文件UNLOCK_SCRIPT.setLocation(newClassPathResource(unlock.lua));UNLOCK_SCRIPT.setResultType(Long.class);}// 锁名字业务标识 例如order最终redis key lock:orderprivateStringname;privateStringRedisTemplatestringRedisTemplate;publicSimpleRedisLock(Stringname,StringRedisTemplatestringRedisTemplate){this.namename;this.stringRedisTemplatestringRedisTemplate;}// todo 被static final修饰的变量类加载时执行一次赋值之后值永久不变。// 锁的key前缀privatestaticfinalStringKEY_PREFIXlock:;// toString(true), 参数true去掉UUID中间的横线(38400000‑8cf0‑11bd‑b12e‑0800200c9a66)privatestaticfinalStringID_PREFIXUUID.randomUUID().toString(true)-;OverridepublicbooleantryLock(longtimeoutSec){// todo 获取线程的全局唯一标识uuid-线程longIdStringthreadIdID_PREFIXThread.currentThread().getId();// 获取锁// todo setIfAbsent 底层是SET key value NX EX 10, 不存在则设置过期时间为10秒【NX EX 是一条原子命令】BooleansuccessstringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIXname,threadId,timeoutSec,TimeUnit.SECONDS);returnBoolean.TRUE.equals(success);/*todo setIfAbsent 返回类型是包装类Boolean不是基础类型boolean。 Boolean有三种可能值 1. Boolean.TRUE → truekey不存在设置成功获取锁成功 2. Boolean.FALSE → falsekey已存在抢锁失败 3. null → Redis 通信异常、网络故障返回 null 如果直接写 return success; 等价于拆箱return success.booleanValue() 当 success null 的时候直接抛出 NullPointerException 空指针异常。*/}// todo 当前 unlock() 是get if delete非原子会有bugpublicvoidunlockCopy(){/* 问题此方法没有 Lua 脚本原子性漏洞 当前 unlock() 是get if delete非原子。 极端场景 同一个用户并发多次请求的时业务执行时间 超过 锁超时时间 锁 被 Redis 过期删掉当前线程会删掉其他线程的锁。 解决方法 unlock 改成 Lua 脚本lua 脚本只能解决【误删别人锁】 lua只是保证释放锁的原子性 key 是 lock:order:userId 同一个用户、同一把Redis key先后出现两个不同线程持有这把锁老线程把新线程的锁删掉。 */// 获取线程的全局唯一标识StringthreadIdID_PREFIXThread.currentThread().getId();// 查询 redisStringlockValuestringRedisTemplate.opsForValue().get(KEY_PREFIXname);if(threadId.equals(lockValue)){// 释放锁stringRedisTemplate.delete(KEY_PREFIXname);}}// 利用 lua脚本实现原子命令Overridepublicvoidunlock(){stringRedisTemplate.execute(// 参数1Lua脚本对象UNLOCK_SCRIPT,// 参数2ListString keys 脚本里的 KEYS[]Collections.singletonList(KEY_PREFIXname),// singletonList()JDK工具方法生成一个只能装 1 个元素的List集合只读集合// 参数3可变参数 Object... args 脚本里的 ARGV[], 多个参数直接在后面用逗号分割拼上就行ID_PREFIXThread.currentThread().getId());}}singletonList()JDK工具方法生成一个只能装 1 个元素的List集合只读集合lua 脚本resources / unlock.lualua 只是保证释放锁的原子性--释放分布式锁脚本--KEYS[1]锁的key--ARGV[1]当前线程的唯一标识UUID-线程idifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0end基于 SETNXsetIfAbsent手写分布式锁底层命令SET lock:xxx uuid‑threadId NX EX timeout存在一下几个问题问题1- 不可重入现象同一个线程再次去获取同一把锁直接获取锁失败。场景// 线程A 拿到锁 lock:order:100lock.tryLock(30);// 业务里面又调用另一个方法再次尝试获取同一把锁lock.tryLock(30);// 返回false拿不到锁业务直接失败SETNX 只判断 key 存不存在不判断是谁的线程。只要key已经存在不管是不是当前线程NX直接返回失败。手写 SimpleRedisLock 没有记录锁的持有计数不支持重入。Redisson内部用hash结构key:{threadId:count}获取锁计数1释放计数‑1实现可重入。问题2- 不可重试tryLock(timeoutSec)当前你的代码逻辑抢锁一次失败直接false返回不会阻塞、不会重试抢锁。booleanisLocklock.tryLock(1200);if(!isLock){returnResult.fail(不允许重复下单);}当前实现一次SET NX不成功直接返回。没有循环、没有自旋重试。这是业务层面设计秒杀场景用户抢不到直接返回不需要自旋等待。如果要实现可重试需要while循环 sleep自旋抢锁但高并发下大量自旋会压垮Redis。Redisson的tryLock(waitTime, leaseTime, unit)waitTime就是允许最大自旋重试等待时间。注意不是SETNX命令做不到重试是你写的 SimpleRedisLock 没有做自旋重试逻辑。问题3- 超时释放给锁设置过期时间防止宕机死锁。但是业务执行时间 锁超时时间时序线程A获取锁EX设置10秒过期业务复杂执行需要15秒10秒到Redis自动把锁key删掉线程B拿到同一把锁线程A业务执行完毕执行unlocklua 脚本判断 value 匹配此时A的value已经不存在删锁无动作后果两个线程同时执行业务并发安全失效出现超卖 / 一人多单。手写SimpleRedis锁没有看门狗续期机制。锁时间写死过期直接释放。RedissonleaseTime -1 开启看门狗默认30s每10s检查线程是否还持有锁如果业务没执行完就刷新锁过期时间解决超时释放。简易解决方案人为把超时时间设置很大但治标不治本宕机会长时间死锁。问题4- Redis主从一致性问题主从异步复制锁丢失Redis主从架构主节点写锁异步同步给从节点。时序客户端向 master 执行 SET NX拿到锁master还没来得及把锁key同步给slave从节点master宕机slave升级成为新的master新master没有锁key其他客户端又可以再次获取锁。结果锁失效多个客户端同时拿到锁。普通主从、哨兵模式解决不了。方案1Redisson红锁RedLock需要多个独立master节点半数以上节点获取锁成功才算拿到锁资源消耗大生产很少使用。方案2Redis7.0 引入wait命令写锁之后强制等待至少一个从节点同步完成再返回客户端。问题SETNX手写SimpleRedisLockRedisson可重入❌不支持✅Hash记录计数支持重入可重试自旋等待❌只尝试一次✅waitTime参数支持自旋重试锁超时释放❌业务超过过期时间锁丢失✅看门狗自动续期leaseTime-1主从切换锁丢失❌异步复制会丢锁❌普通哨兵依旧会丢红锁可以缓解