分布式锁的三个主要核心要素 #
- 安全性、互斥性。在同一时间内,不允许多个client同时获得锁。
- 活性。无论client出现crash还是遭遇网络分区,你都需要确保任意故障场景下,都不会出现死锁,常用的解决方案是超时和自动过期机制。
- 高可用、高性能。加锁、释放锁的过程性能开销要尽量低,同时要保证高可用,避免单点故障。
茅台超卖案例 #
仔细分析下来,可以发现,这个抢购接口在高并发场景下,是有严重的安全隐患的,主要集中在三个地方:
- 没有其他系统风险容错处理
由于用户服务吃紧,网关响应延迟,但没有任何应对方式,这是超卖的导火索。
- 看似安全的分布式锁其实一点都不安全
虽然采用了set key value [EX seconds] [PX milliseconds] [NX|XX]的方式,但是如果线程A执行的时间较长没有来得及释放,锁就过期了,此时线程B是可以获取到锁的。当线程A执行完成之后,释放锁,实际上就把线程B的锁释放掉了。这个时候,线程C又是可以获取到锁的,而此时如果线程B执行完释放锁实际上就是释放的线程C设置的锁。这是超卖的直接原因。
- 非原子性的库存校验
非原子性的库存校验导致在并发场景下,库存校验的结果不准确。这是超卖的根本原因。
通过以上分析,问题的根本原因在于库存校验严重依赖了分布式锁。
因为在分布式锁正常set、del的情况下,库存校验是没有问题的。
但是,当分布式锁不安全可靠的时候,库存校验就没有用了。
其他风险 #
-
单Redis Master节点存在单点故障
-
一主多备Redis实例又因为Redis主备异步复制,当Master节点发生crash时,可能会导致同时多个client持有分布式锁,违反了锁的安全性问题
一般使用 setnx 方法,通过 Redis 实现锁和超时时间来控制锁的失效时间。但是在极端的情况下,当 Reids 主节点挂掉,但锁还没有同步到从节点时,根据哨兵机制,从就变成了主,继续提供服务。 这时,另外的线程可以再来请求锁,此时就会出现两个线程拿到了锁的情况。
setnx和expire命令分开写,没有原子性
- lua脚本
SET key value NX EX seconds
忘记设置过期时间
- 存在app崩溃,导致锁永远无法释放
RedLock分布式锁 #
它基于多个独立的Redis Master节点工作,只要一半以上节点存活就能正常工作,同时不依赖Redis主备异步复制,具有良好的安全性、高可用性。 然而它的实现依赖于系统时间,当发生时钟跳变的时候,也会出现安全性问题