一致性风险

分布式锁的三个主要核心要素 #

  • 安全性、互斥性。在同一时间内,不允许多个client同时获得锁。
  • 活性。无论client出现crash还是遭遇网络分区,你都需要确保任意故障场景下,都不会出现死锁,常用的解决方案是超时和自动过期机制。
  • 高可用、高性能。加锁、释放锁的过程性能开销要尽量低,同时要保证高可用,避免单点故障。

茅台超卖案例 #

Redis——由分布式锁造成的重大事故

仔细分析下来,可以发现,这个抢购接口在高并发场景下,是有严重的安全隐患的,主要集中在三个地方:

  • 没有其他系统风险容错处理

由于用户服务吃紧,网关响应延迟,但没有任何应对方式,这是超卖的导火索。

  • 看似安全的分布式锁其实一点都不安全

虽然采用了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主备异步复制,具有良好的安全性、高可用性。 然而它的实现依赖于系统时间,当发生时钟跳变的时候,也会出现安全性问题