ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

redisson 的使用

redisson 的使用 文章目录pomapplication.yamlRedissonConfig.javacontrollerservice接口service实现类redissonClient.getLock(锁的key)场景pomdependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.13.6/version/dependencyapplication.yamlserver:port:8081spring:application:name:hmdpdatasource:url:jdbc:mysql://localhost:3306/atguigudb?useUnicodetruecharacterEncodingutf-8useSSLfalseusername:rootpassword:rootdriver-class-name:com.mysql.jdbc.Driverredis:host:127.0.0.1port:6379password:lettuce:pool:max-active:20max-idle:10min-idle:2max-wait:1000msjackson:default-property-inclusion:non_nullmybatis-plus:configuration:log-impl:org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case:truemapper-locations:classpath*:/mapper/*.xmltype-aliases-package:com.hmdp.entitylogging:level:com.hmdp:debugredisson:address:redis://192.168.0.118:6379password:min-idle:5# 最小空闲连接max-pool:20# 最大连接数watchdog-timeout:40000RedissonConfig.javapackagecom.hmdp.config;importorg.redisson.Redisson;importorg.redisson.api.RedissonClient;importorg.redisson.config.Config;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;ConfigurationpublicclassRedissonConfig{Value(${redisson.address})privateStringaddress;Value(${redisson.password})privateStringpassword;Value(${redisson.min-idle})privateIntegerminIdle;Value(${redisson.max-pool})privateIntegermaxPool;Value(${redisson.watchdog-timeout:30000})privateLongwatchdogTimeout;BeanpublicRedissonClientredissonClient(){ConfigconfignewConfig();config.useSingleServer().setAddress(address).setPassword(password).setConnectionMinimumIdleSize(minIdle).setConnectionPoolSize(maxPool);修改看门狗默认TTL单位ms此时看门狗续期间隔: 40000 / 3 ≈ 13.33秒config.setLockWatchdogTimeout(watchdogTimeout);returnRedisson.create(config);}}controllerpackagecom.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;// redisson - 版本PostMapping(seckill-redisson/{id})publicResultseckillVoucherRedisson(PathVariable(id)longvoucherId){returnvoucherOrderService.getOrderByVoucherRedisson(voucherId);}}service接口packagecom.hmdp.service;importcom.baomidou.mybatisplus.extension.service.IService;importcom.hmdp.dto.Result;importcom.hmdp.entity.VoucherOrder;publicinterfaceIVoucherOrderServiceextendsIServiceVoucherOrder{ResultgetOrderByVoucherRedisson(LongvoucherId);}service实现类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.redisson.api.RLock;importorg.redisson.api.RedissonClient;importorg.springframework.aop.framework.AopContext;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importjava.time.LocalDateTime;importjava.util.concurrent.TimeUnit;ServicepublicclassVoucherOrderServiceImplextendsServiceImplVoucherOrderMapper,VoucherOrderimplementsIVoucherOrderService{AutowiredprivateRedissonClientredissonClient;privatefinalRedisIdWorkerredisIdWorker;privatefinalISeckillVoucherServiceseckillVoucherService;privatefinalStringRedisTemplatestringRedisTemplate;publicVoucherOrderServiceImpl(RedisIdWorkerredisIdWorker,ISeckillVoucherServiceseckillVoucherService,StringRedisTemplatestringRedisTemplate){this.redisIdWorkerredisIdWorker;this.seckillVoucherServiceseckillVoucherService;this.stringRedisTemplatestringRedisTemplate;}// redisson - 版本OverridepublicResultgetOrderByVoucherRedisson(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(优惠券库存不足);}RLocklockredissonClient.getLock(lock:order:userId);booleansuccess;try{successlock.tryLock(10,TimeUnit.SECONDS);}catch(InterruptedExceptione){// 恢复线程中断标记规范写法Thread.currentThread().interrupt();returnResult.fail(获取锁被中断);}if(!success){returnResult.fail(请勿重复下单稍后重试);}try{VoucherOrderServiceImplproxy(VoucherOrderServiceImpl)AopContext.currentProxy();returnproxy.createVoucherOrder(voucherId,userId);}finally{【只有当前线程持有锁才释放】if(lock.isHeldByCurrentThread()){发送 lua 脚本删除 Redis 里这个lock:order:userId的 key锁释放。lock.unlock();}}}TransactionalpublicResultcreateVoucherOrder(LongvoucherId,LonguserId){// 一人一单IntegerorderCountlambdaQuery().eq(VoucherOrder::getUserId,userId).eq(VoucherOrder::getVoucherId,voucherId).count();if(orderCount0){returnResult.fail(一人仅限一单);}// 扣库存booleansuccessseckillVoucherService.lambdaUpdate().setSql(stock stock - 1).eq(SeckillVoucher::getVoucherId,voucherId).gt(SeckillVoucher::getStock,0).update();if(!success){returnResult.fail(券的库存不足);}// 创建订单VoucherOrdervoucherOrdernewVoucherOrder();voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);longorderIdredisIdWorker.nextId(order);voucherOrder.setId(orderId);save(voucherOrder);returnResult.ok(orderId);}}redissonClient.getLock(锁的key)场景用户快速双击提交订单两个请求几乎同时到达后端请求 A先到执行tryLock抢到lock:order:1001开始执行创建订单逻辑请求 B几乎同时进来尝试抢lock:order:1001锁已经被 A 持有B抢锁失败 →isLockfalse→ 返回不允许重复下单redissonClient.getLock(锁的key)此命令只是在 Java内存 创建 RLock对象【此时Redis里面啥都没有没有创建锁】锁的keylock:order:{userId}按用户加锁实现一人一单不同用户互不阻塞锁粒度很细。Redisson的tryLock(waitTime, leaseTime, unit)三个参数版本waitTime: 最多等待多久去抢锁抢不到直接返回 falseleaseTime抢到锁之后锁在 Redis 里多久自动过期unit时间单位执行tryLock()才会向 Redis 发送 lua 脚本尝试创建锁、抢占锁tryLock(waitTime, unit)等价于tryLock(waitTime, -1, unit)参数一waitTime10去 Redis 尝试拿锁如果锁现在被别人占着当前线程不会立刻返回false而是循环重试抢锁最多重试 10秒10秒之内抢到锁 → 返回true10秒之后还是拿不到锁 → 返回false参数二leaseTime -1【底层自动把 leaseTime 置为 -1开启看门狗自动续期】抢到锁之后开启看门狗自动续期锁默认 30s (30s是源码默认配置支持自定义配置) 过期 每 10s (30 / 3 1030是源码的默认配置30秒除以3是源码写死的不可更改) 续一次时间线举例子0s获取锁成功Redis 锁 TTL30s剩余30s10s看门狗任务执行锁还持有 →expire key 30TTL 重置为 30s剩余30s20s看门狗任务又到点执行锁还持有 →expire key 30TTL 重置为 30s剩余30s30s看门狗任务执行锁还持有 →expire key 30TTL 重置为 30s剩余30s40s看门狗任务执行锁还持有 →expire key 30TTL 重置为 30s剩余30s…… 循环往复固定每 10s 调度一次不管锁 TTL 还剩多少只要还持有锁就重置为 30s。业务代码跑多久锁就持有多久只要程序不宕机当业务线程结束看门狗检测到不再持有锁停止续期Redis 锁等 30s 自动过期方法签名抛出InterruptedException必须捕获受检异常。tryLock()执行流程判断 Redis 有没有lock:order:xxx这个 key没有 → 创建这个 key获取锁成功记录当前线程标识设置 leaseTime-1 开启看门狗已经存在别人持有锁→ 循环等待最多 10 秒 (waitTime)等不到返回 falsebugwaitTime 建议设置为 0只尝试抢锁 1 次抢不到直接返回 false不等待、不重试0ms请求 AtryLock 抢到锁进入下单业务。请求 BtryLock 尝试抢锁锁被 A 占用一次抢锁失败直接返回 success falseB 直接进入if(!success)返回提示请勿重复下单稍后重试isHeldByCurrentThread()底层会向 Redis 发送命令判断锁 key 里面存储的线程标识 UUID:threadId是否等于当前线程标识。返回 true 锁还在并且是我这个线程占有的。返回 false锁不存在 / 锁被别的线程占有。注意它要访问 Redis有网络 IO不是本地内存标记。isHeldByCurrentThread()是 Redisson 提供的 API底层发送HEXISTS命令查询 Redis 的 hash 结构校验锁的持有者标识存在网络 IO不是本地内存判断。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进