1. 死锁问题
- 场景:
当用 redis 做分布式锁时,当 A 用户竞争锁成功,A 用户所在的主机挂了,这时候还没有来得及释放锁,那么其他用户去用 setnx 指令去竞争锁时发现 redis 有这个锁的 key,所以就导致其他用户永远都竞争不成功。
解决方案: 当用户加锁成功时给锁设置一个跟业务执行时间匹配的锁失效时间, 这样就算锁没有释放过一段时间锁也会实现,其他用户就可以竞争到锁了(这里在设置失效时间的操作,要和 setnx 指令一起执行,不能分成2步执行,否则在setnx之后,还没来得及设置失效时间,主机挂了,还是会发生死锁)。
2. 锁续命问题
- 场景:
当 A 用户获取锁成功,但是 A 执行业务操作的时间比较长,就可能会导致业务执行时间大于锁失效时间,那么 A 还在执行业务,锁失效了,就会导致其他用户也可以成功竞争到锁,这样就没办法很好的锁住业务,就会导致脏数据的出现。
解决办法: 当 A 竞争到锁时,就需要开启一个 timer 对锁续命,续命就是重新设置锁的失效时间,由定时器定时的去做这件事情。业务操作完成,unlock,timer.cancel()。
3. 主从锁失效的问题
- 场景:
当 redis 是一个主从架构时,当 A 用户从 master 节点获取到了锁,这时候出现这么一个情况,这个锁没有及时同步到 slave 节点时,master 节点挂了,由 redis 哨兵做了主从切换,以前的 slave 成了主,而这时 slave 是没有锁的这个 key 的,那么就会导致其他用户也可以成功竞争到锁进行业务操作,从而导致脏数据。
解决方案: 这时候可以从奇数台 redis 节点去获取锁,如果超过半数的节点能成功获取到锁,并且获取锁的耗时要小于设置的锁失效时间时就认为获取锁成功了,反之就失败。
这里注意两个点:
1、获取锁的时间,获取时间必须要小于锁失效时间
2、必须超过半数节点获取锁成功
如果这两点没有达到则会执行 unlock 操作释放各节点的锁。
- 这种方案因为需要和多个redis进行通信,执行效率会很低。
上面提到的3个问题都可以使用Redis官方推荐的Java版的Redis客户端Redisson解决,它提供的功能非常多,也非常强大
https://github.com/redisson/redisson
- Reentrant Lock解决死锁和锁续命问题
- RedLock解决主从锁失效的问题
4. lua 脚本做扣减库存
- 场景
在高并发情况下,对库存数据做扣减,存在线程安全问题
解决方案: 扣减库存必须满足原子操作,而 redis 的 lua 是严格的原子操作,所以不会有线 程安全问题。
5. 使用 redis 分布式锁做扣减库存
- 场景
同样是在高并发情况下,对库存数据做扣减
解决方案: 分布式锁理论上也可以完成原子操作
1、先竞争锁 lock.lock(2, TimeUnit.SECONDS);
2、查询库存 Integer stock = (Integer) redisTemplate.opsForValue().get(killGoodCount);
3、扣减库存 redisTemplate.opsForValue().increment(killGoodCount, -1)
这里跟 redis 有三次通讯,而使用 lua 脚本只需要跟 redis 进行一次 通讯,理论上 lua 的效率要高于 redis 分布式锁
6. lua 和分布式锁的优化
- Lua 和分布式锁都是严格意义上的原子操作,也就是同一时间只允许一个用户操作成功,这里肯定会有一个限制吞吐量的问题
Lua 脚本的优化:
Lua 脚本的执行在同一台 redis 主机是 one by one 执行的,那么我们可以把库 存分散到多台 redis 实例中,然后我们程序随机的选择一台 redis 节点去执行扣 减库存的 lua 脚本,处理业务的通道变多了,吞吐量就提高了。
分布式锁的优化:
优化思路跟 lua 差不多,就是加大口子,以前我们在一个 redis 节点中是去竞争一把锁,现在是有多把锁多个库存,一个锁就对应着一个库存,是一一对应关系, 程序随机的去竞争锁,如果竞争成功去扣减这把锁对应的库存就可以了,如果库存不足则又继续随机的从剩下的锁中去竞争。这样多把锁理论上是同时运行多个用户竞争多把锁成功的,比之前的一把锁吞吐量明显要高。这个就是分段锁的思路。
分段锁要注意的点:
分段锁需要物理主机有足够的性能,因为分段后同一时间跟 redis 的通讯就加多了,直接就会导致耗尽 redis 连接池的连接对象,导致大量请求在连接池的等待队列中,从而导致请求失败,redis 连接超时。
7. 并行转串行思路
- 并行转串行就是一种加快单次请求速度的处理方式,当用户请求过来时,不立刻处理这个请求的业务,而是把这次请求用本地队列缓存起来,然后用线程异步去一个个处理本地队列里的请求,这样就对本来高并发的并行请求进行了削峰处理。这样理论上单次请求速度肯定是要加快的,以前是去处理业务,现在只是把请求塞到本地队列,这样占用的 tomcat 连接池中的连接对象的时间就短了,这些连接又可以去处理其他用户的请求,所以提高单次请求速度就肯定会提高整个系统的吞吐量。
8. redis 和 zk 分布式锁比较
- redis分布式锁
速度要快,没有办法保证一致性 - Zk分布式锁
效率要低于redis分布式锁的,CAP 保证 CP,获取锁更安全