Redis中的分布式锁Redisson:
-
应用场景:
涉及到资源争抢的高并发场景,比如: 秒杀下单、抢票等等
-
问题:
如果在单机场景下,大家同时点击(比如三个人同时点击下单)库存(原始库存100)可能依旧是99而不是97出现超卖的问题。 [并发量越高,就越可能出现超卖的情况]
-
解决方法一:
- 单机环境部署项目的话可以使用synchronized(this) 就可以解决,但是现在的公司大多数至少是进行分布式的服务器进行部署(服务器至少>2台)所以如果使用上面的单机锁就会出现问题
-
持续优化v1.0:
-
这时可以考虑使用以下方式来构建一把分布式锁从而解决高并发带来的一系列问题:
- MySQL
- Zookeeper
- Redis(最常用)
-
如何使用redis实现分布式锁呢?
-
SETNX 命令(就可以轻松的构建出一把分布式锁): setnx key value (将key的值设置为value,会判断当前key是否存在,如果存在就不会进行操作,相反才进行操作) 所以最基本最入门的分布式锁就是在方法的前面设置一把锁,然后再方法执行完成了之后再将锁进行释放(任何请求只要到了分布式锁这里,redis里面都必须按照顺序进行排队,因为redis是单线程的,所以只有一个执行完成之后才会执行下一个,这样就实现了分布式锁的简易版)
String lockKey = "lockKey"; // 创建一个redis的操作对象,并进行锁的设置 Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "yyds"); // 只有加锁加成功了之后才继续执行后面的命令,否则就return if(!result) { return "error_code"; } ... // 释放锁 stringRedisTemplate.delete(lockKey);
-
-
总结:
如果方法中间的代码(加锁之后的代码)如果出现了错误,抛出异常之后就会导致最后的释放锁永远执行不成功,后面进来的请求就会加锁失败,不能执行方法体,就会出现死锁的问题
-
-
持续优化v2.0:
-
解决上面的异常问题 使用try-finally将代码块进行包裹,将容易可能出现异常的代码部分放在try里面,然后finally里面放入释放锁的代码
// 释放锁 try { Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "yyds"); // 只有加锁加成功了之后才继续执行后面的命令,否则就return if(!result) { return "error_code"; } ... } finally { // 释放锁 stringRedisTemplate.delete(lockKey); }
-
总结:
try能够处理异常,但是如果是服务器宕机了应该怎么解决可能造成的死锁问题? 比如这台服务器宕机了,但是另外的两台服务器上面还是存在的锁,所以就会造成死锁的问题
-
-
持续优化v3.0:
-
解决服务器宕机问题: 解决方法就是给锁添加一个过期时间: 即使是某台服务器宕机了,但是你给锁添加了过期时间的,所以它会自动的进行清除锁
// 释放锁 try { // 给锁添加过期时间 进行自动的清除锁 设置锁和添加过期时间具有原子性(要么都成功,要么都回滚失败) Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "yyds", 10, TimeUnit.SECONDS); // 只有加锁加成功了之后才继续执行后面的命令,否则就return if(!result) { return "error_code"; } ... } finally { // 释放锁 stringRedisTemplate.delete(lockKey); }
-
总结:
- 上面的分布式锁只适合于并发量比较低的场景下,如果是大厂里面的高并发场景就不合适
- 如果是特别的高并发场景的话可能出现的问题就是我自己加的锁,可能会被别人给我删掉
-
-
持续优化v4.0:
-
解决上面别人可能删掉我手里的锁的问题: 核心就是在删除之前判断是否是和该线程id相等
// 解决办法就是在加锁的时候使用UUID进行加锁 String clientID = UUID.randomUUID.toString(); // 释放锁 try { // 给锁添加过期时间 进行自动的清除锁 设置锁和添加过期时间具有原子性(要么都成功,要么都回滚失败) Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientID, 10, TimeUnit.SECONDS); // 只有加锁加成功了之后才继续执行后面的命令,否则就return if(!result) { return "error_code"; } ... } finally { // 释放锁 进行判断是否和当前的id相等 if(clientID.equals(stringRedisTemplate.opsForValue).get(lockKey)) { stringRedisTemplate.delete(lockKey); } }
-
总结:
- 上面的代码相对来说比较符合高并发的场景了,但是依旧存在一个问题,就是线程执行的时间问题,这里设置的过期时间是10秒,试想如果线程执行的时间超过了10秒怎么办?可能依旧会出现死锁问题
- 如果自己去实现这个方案的话就会比较的麻烦,自己可能会写出许多的bug,可以直接使用开源框架即可实现锁的续期 Redisson
-
-
持续优化v5.0:
-
解决上面的锁提前过期的问题:使用Redisson, 首先需要获取到一个Redisson的锁的对象,然后再到代码里面加锁,最后就释放锁即可
// 01. 获取到一个锁的对象 String lockKey = "yyds"; Rlock redissonLock = redisson.getLock(lockKey); // 02. 加锁 使用redisson的方式进行加锁 try { redisson.lock(); ... } finally { // 释放锁 进行判断是否和当前的id相等 redissonLock.unlock(); }
-
总结:
-
到这儿就可以就此打住了,该方法基本能满足项目的高并发要求,Redis的并发单机最高可达到10万级别,至少也是有几万。
-
当然后面还可能会出现什么主从架构锁失效的问题等,这些问题可以通过人工写入脚本进行认为的修复即可,不算太大的问题而且出现的概率也比较的小。
-
大部分场景已经够用,实际上就是将高并发的并行执行的请求,到redis这里来进行排队阻塞,将其按照一定的顺序排列之后变换成了串行执行。
-
-
-
Redisson的底层原理简析:
-
redisson的底层其实就是使用的lua脚本语言:首先判断lockkey是否存在, 不存在就调用hset命令设置一把锁,并设置过期时间(30s默认) 通过hash的数据类型实现, 我们自己使用的string字符串类型实现 lua脚本语言的执行具有原子性,要么都执行,要么都不执行,而我们自己执行的string命令就是不一定能保证其原子性
-
过期时间30s实际上就是一个"看门狗"的机制,它是一个定时执行的任务,会判断当前线程是否持有这一把hash锁,如果有的话就续期 同样也是使用的lua脚本
-