分布式锁(1) redisson
从伪超杀问题的解决思路及问题演变理解Redisson加锁机制
场景
功能: 秒杀 –> 高并发
架构: nginx + multiple TomCat + Redis Database (Redis中存一简单KV, K = 商品名, V = 库存)
Note: 均为伪场景, 实际秒杀问题并非这样设计
V1
1 | public string deductStock(){ |
问题:
单机内,TomCat集群皆无锁。
线程1 得到stock = 200 时, 线程2 也可能得到stock = 200, 两个线程操作后正常数量应为198, 但是均写回了199。
V2
1 | public string deductStock(){ |
evolution1: added Synchronized keyword
问题:
单机内实现了并发安全, 但集群下无效,因为多个TomCat同一时刻下均各自出来一条线程访问redis, 此时依然有线程安全问题
V3
1 | public string deductStock(){ |
evolution2:
- 利用 redis的单线成特性, 使用setnx指令, 使各个线程串行的取set一个key, 如果set成功则代表抢锁成功, 此时没抢到锁的线程做自旋等待。
- 同时需将key设置expire时间, 其目的是防止程序跑到finally前宕机而无法释放这个key, 也即无法释放锁导致死锁。
问题:
- 线程实际运行时间超过设置的expire时间, 一个线程还未结束时就释放了锁, 此时线程2拿到锁, 线程2运行过程中线程1运行结束调用finally代码, 从而删除了这个key,也即释放了这个锁。 但问题是此时这个被删除的锁其实是线程2的。 –> 这个现象称为永久失效?
V4
1 | public string deductStock(){ |
- Evolution:
- V3的问题是一个线程删除了不属于自己的锁, 也即不属于自己的key, 所以V4将key的value设置成线程唯一的, 再删除key之前先判断这个key是不是自己的
- 问题:
- 依然会出现一个线程删除了不属于自己的key的情况。 如: key的超时时间为10s, 即10s后一个key会被自动删除。 假设在第9.99999s时, 一个线程运行了finally 中的if条件, 判断if为true, 所以开始执行delete操作。 而在执行delete时, 已经是第10.000001s。 这意味着该线程的key其实已经被自动删除了, 而此时再此执行delete, 则删除的还是另一个线程的锁,即key。
- 该问题原因:
- 所有线程都用了同一个名称的key,即锁。 –> 无法解决
- 因为,利用redis实现分布式线程安全的本质是利用了“SETNX”, 这意味着所有线程必须使用同一个名称的KEY。
- 一个线程还未正常运行结束时, key就超时被自动删除了 –> 可解决: 线程运行中为其持有的key续时间, which is Redisson did。
- 所有线程都用了同一个名称的key,即锁。 –> 无法解决
Redisson Lock
Redisson Lock即帮助自动的为一个线程持有的锁“续命”, 从而避免出现误删其他线程的key的问题。
总结 Redisson Lock工作原理本质:
- 利用SETNX的特点(实际源码中并不是这么写, 但原理是一样的), 即判断一个key是否存在, 存在即不可持有, 不存在则可更新value, 来实现给线程分发锁。
- 使每个线程持有的key中的value唯一, 来实现防止误删的问题。
- 使每个线程持有的key自动续命, 来实现防止误删的问题。
- Redisson Lock的源码中大量使用Lua脚本语言来保证操作的原子性。
Extra: 如何提高并发效率?
上述场景中, 所有操作时串行的,例如stock=1000, 那么每个线程都需排队操作者1000个库存。 效率低下。
若想提高并发效率, 可将stock分段, 如分成stock_001 = 200, stock_002 = 200 …. , 如此并发效率便提高五倍。 同时服务器压力也会增大。 即分段粒度越高, 并发效率越高。