
本文共 5015 字,大约阅读时间需要 16 分钟。
redis 实现 分布式锁
上节 我们讲了 线程锁 在单体项目中的作用,和 放在 分布式 项目里产生的问题,那接下来我们就来解决 分布式 架构上怎么 保证 数据的一直性
使用 redisTemplate 实现
// 设置锁setIfAbsent("lock", "1213")---> SETNX lock "1213"// 释放锁redisTemplate.delete("lock");
@GetMapping("/cut") public Object kc() { redisTemplate.setKeySerializer(new StringRedisSerializer()); /** * 设置锁,如果 lock 不存在的话,设置 lock=1213 并返回 true * 如果存在的话:就不操作 直接返回 false */ Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1213"); if(!lock){ // 锁存在 return "服务繁忙,请稍后再试"; } int num = Integer.parseInt(redisTemplate.opsForValue().get("num").toString()); if (num > 0) { int lastNum = num - 1; redisTemplate.opsForValue().set("num", lastNum + ""); System.out.println("扣减库存成功,剩余库存为:" + lastNum); } else { System.out.println("扣减库存失败,库存不足"); } // 释放锁 redisTemplate.delete("lock"); return "ok"; }
简述一下逻辑:
第一个请求 进来,设置一把锁 进行锁住,然后 往下执行减库存的操作,此时 第二个线程进来获取锁,但是第一请求没有释放锁,所以第二个请求获取锁就会失败 得到返回值为 false,进入if 方法体 直接返回了。其他线程也是如此,直到 第一个线程释放锁后 其他线程才有获取锁的机会,每次只有一个线程能够成功获取锁,其他线程获取不到直接返回。
大家 觉得 这把锁怎么样,是不是解决问题了呢?还有什么问题吗?有没有那个小可爱想到了呢?
肯定有小伙伴想到了。
情况1:一个请求进来,获取锁,执行扣减库存的操作,在它 释放锁之前 服务突然抛异常了呢?
- 那就 使用 try{}finally{}嘛,这下不怕抛异常 无法释放锁了吧
try{ int num = Integer.parseInt(redisTemplate.opsForValue().get("num").toString()); if (num > 0) { int lastNum = num - 1; redisTemplate.opsForValue().set("num", lastNum + ""); System.out.println("扣减库存成功,剩余库存为:" + lastNum); } else { System.out.println("扣减库存失败,库存不足"); } }finally { // 释放锁 redisTemplate.delete("lock"); }
情况2:要是不抛异常,在释放锁之前服务重启了呢?那就来不及执行finally了吧,这下 锁也没有释放吧。
那该怎么解决呢?
有小伙伴 就会想:给锁加一个 定时器嘛,它要挂就挂,到了时间 锁自动释放,其他人又可以获取到 锁 服务又可用了
感觉是可行哦,那我们来实际操作一下。源码如下:
OK 定时有了,要是 线程挂在里面,时间一到 锁就会自动释放
事实真的会 是这个样子的吗?
情况3:锁设置成功,接下来对 锁进行定时,此时准备定时呢,还没定时成功突然程序挂了,又会导致死锁,像前那情况一样。
那这时 我们就要保存设置锁 和 定时 两个指令的原子性,要么全部成功 要么全部失败,那该怎么实现呢?
这个问题 大牛已经都想过了,往下看
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1213",10, TimeUnit.SECONDS);
一条更比两条强。这个底层使用 Lua 实现,保证其原子性。
再看一下 我们优化后的代码
@GetMapping("/cut") public Object kc2() { redisTemplate.setKeySerializer(new StringRedisSerializer()); /** * 设置锁,如果 lock 不存在的话,设置 lock=1213 并返回 true * 如果存在的话:就不操作 直接返回 false */ String lockKey = "lock"; String redisClientId = UUID.randomUUID().toString(); Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, redisClientId,10, TimeUnit.SECONDS); if(!lock){ // 锁存在 return "服务繁忙,请稍后再试"; } try{ int num = Integer.parseInt(redisTemplate.opsForValue().get("num").toString()); if (num > 0) { int lastNum = num - 1; redisTemplate.opsForValue().set("num", lastNum + ""); System.out.println("扣减库存成功,剩余库存为:" + lastNum); } else { System.out.println("扣减库存失败,库存不足"); } }finally { // 释放锁 if (redisClientId.equals(redisTemplate.opsForValue().get(lockKey))){ redisTemplate.delete(lockKey); } } return "ok"; }
小伙伴 会说,这下没问题啦 哈哈。。。。
你确定 没问题?
那就我来给你分析分析
正常逻辑:一个请求进来 说去锁,设置有效时间为 10 秒,然后 执行下面 业务逻辑,最后释放锁,然后第二个线程进来。。。。
lock 有效时间为 10 秒,保不定 那个线程执行任务的时候 执行完要 15 秒,此时,lock 10秒就失效,那下一个线程就会进来,假如第二个线程要执行8 秒,第一个线程5秒后就执行完了 然后释放lock 锁,线程1的锁早就失效了,它释放的锁确实线程2的锁,而第二个线程还有3秒才执行完,此时线程3获取到锁,又进来了,3秒后线程2又释放线程3 的锁,这样下去线程3释放线程4.。。。 就会导致 锁永久失效。
所以 这个超时时间该设置多少呢? 就需要根据项目来考量了。
那有什么 好的解决方案吗,解决这个锁失效的问题呢?
有的,那肯定是有的,听我徐徐道来。。。
假设 设置锁有效时间为 30 秒,那当线程获取锁后,开一个子线程 做一个定时器,每隔一段时间去检查该对象的锁是否存在,存在的话 就重新给 锁续命。
那如何保证 自家的锁不会被别人释放呢?
这下那个 锁的 value 就派上用场了,给每个线程的锁配置上唯一标识(这个唯一标识就使用UUID)
每次释放锁的时候就判断是否是自己的锁,保证只释放自己的锁。
理论有了,那该怎么实现呢?
使用Redisson 实现分布式锁
1、引入 Redisson 依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.0</version></dependency>
2、配置Redisson
@Beanpublic Redisson redisson(){ // 此为单机模式 Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0).setPassword("root"); return (Redisson) Redisson.create(config);}
3、代码实现
@Autowiredprivate Redisson redisson;@GetMapping("/cut")public Object kc() { String lockKey = "lock"; RLock rLock = redisson.getLock(lockKey); try { // 加锁 rLock.lock(); int num = Integer.parseInt(redisTemplate.opsForValue().get("num").toString()); if (num > 0) { int lastNum = num - 1; redisTemplate.opsForValue().set("num", lastNum + ""); System.out.println("扣减库存成功,剩余库存为:" + lastNum); } else { System.out.println("扣减库存失败,库存不足"); } } finally { // 释放锁 rLock.unlock(); } return "ok";}
但是 主从架构 也是可能出现问题,如 redis 挂了
后期优化 可以 用 redis 集群,增加高可用。
OK redis 锁分布式就讲完了,到这里 锁就差不多了
要是还想更加完善 可以选择使用ZK来实现,但是性能是没有 redis 好。
发表评论
最新留言
关于作者
