Redis分布式锁事故复盘:揭秘超卖背后的技术陷阱

创始人
2024-12-14 23:12:01
0 次浏览
0 评论

一次由Redis分布式锁造成的重大事故,避免以后踩坑!

使用基于Redis的分布式锁如今已经不是什么新鲜事了。
本文主要是根据我们实际项目中重分布锁导致的事故的分析和解决。
我们项目中的紧急订单是使用分布式锁解决的。
经营者曾举办飞天茅台抢购活动,库存100瓶,但已售空!要知道,这个地球上就缺少飞天茅台!!!这次事故定性为P0级重大事故……我们可以放心。
整个项目组的业绩都被扣了~~事故发生后,CTO点名我,让我带头处理之前购买活动的接口,但是这次为什么超卖了?原因是:之前的抢购产品并不紧缺,而这次的抢购对象却是飞天茅台。
通过数据分析,所有数据初步增长。
活动现场的热烈程度可想而知。
废话不多说,直接上核心代码,机密部分已经用伪代码处理了。


上面的代码通过10秒的分布式锁过期时间来保证业务逻辑有足够的执行时间。
try-finally语句块用于保证锁的及时释放。
库存也在业务代码中进行验证。
看起来很安全~别担心,继续分析。


飞天茅台的抢购活动吸引了大量新用户下载注册我们的APP。
其中,有不少羊毛地段采用专业方式注册新用户收羊毛、刷单。
当然,我们的用户系统提前做好了防范,配备了阿里云人机验证、三因素认证和自研风控系统,拦截了大量非法用户。
忍不住竖起大拇指~但也正因为如此,用户服务始终处于较高的运行负载下。
抢购活动一开始,大量的用户验证请求就冲击了用户服务。
这导致用户服务网关的响应延迟很短。
部分请求响应时间超过10s,但由于HTTP请求超时,我们将其设置为30s,导致用户验证时接口阻塞。
10s后,分配的Lock已经过期,如果这时候有新的请求进来,就可以获取到锁,也就是说锁被覆盖了。
这些阻塞的接口执行完后,会再次执行释放锁的逻辑,将锁释放给其他线程,导致新的请求竞争锁。
这确实是一个极其糟糕的循环。
此时我们只能依靠库存验证,但是库存验证不是非原子的,它使用getandcompare方法。
高当代场景下,存在严重的安全风险,主要集中在三个地方:通过上述分析,问题的主要原因是库存验证严重依赖分布式锁。
因为分布式锁正常设置和删除的时候,库存确认是没有问题的。
然而,当分布式锁不安全可靠时,库存验证就没用了。
了解了原因之后,我们就可以对症下药了。
实现相对安全的分布式锁相对安全的定义:set和share一一映射,其他完成的锁不会被删除。
从实际角度来看,即使set和part能够一一映射,也无法保证业务的绝对安全。
因为锁的过期时间总是有限的,除非不设置过期时间或者过期时间设置得很长,这个还会引起其他问题。
因此它是没有意义的。
要实现相对安全的分布式锁,必须信任密钥的值。
当你释放锁的时候,通过value的值来保证不被删除。
我们基于LUA脚本实现原子getandcompare,如下:我们使用LUA脚本实现安全解锁。
实现安全库存验证如果我们对并发有更深入的了解,我们会发现像getandcompare/readandsave这样的操作都是非原子的。
如果我们想要实现原子性,我们也可以使用LUA脚本来实现。
但在我们的例子中,由于抢购活动期间每个订单只能下一瓶,所以可以不基于LUA脚本来实现,而是基于redis本身的原子性来实现。
原因是:发现代码中没有Stock确认完全是多余的。
经过上述增强代码的分析,我们决定创建一个新的DistributedLocker类专门用于处理分布式锁。
仔细思考分布式锁是否有必要或者改进之后,我们其实可以发现,利用redis本身的原子性进行库存减量,就可以保证超卖不会超卖。
是的。
但如果没有这一层锁,所有传入的请求都会经过业务逻辑,因为它们依赖于其他系统,这会导致其他系统的压力增大。
这会增加性能损失和服务不稳定,得不偿失。
基于分布式网关可以对部分流量进行一定程度的拦截。
分布式锁的选择有人建议使用RedLock来实现分布式锁。
RedLock具有更高的可靠性,但代价是牺牲了一些性能。
在这种情况下,这种可靠性的提升远不如性能的提升划算。
对于可靠性要求极高的场景,可以使用RedLock。
我们再次思考是否需要分布式锁,还是因为bug需要修复并上线,我们对其进行了优化,并在测试环境中进行了压力测试,并立即上线实施。
确实,这种优化已经被证明是成功的,性能略有提升,而且分布式锁失效时也没有出现超卖的情况。
但还有优化的空间吗?一些!由于服务是分布在集群中的,我们可以将库存均匀的分配到集群中的每台服务器上,并通过广播的方式通知集群中的每台服务器。
网关层使用基于用户ID的哈希算法来确定请求哪个服务器。
这样就可以根据申请缓冲区实现库存扣减和考核。
性能进一步提升!通过上面的改造,我们就完全不再需要依赖redis了。
无论是性能还是安全性都可以进一步提升!当然,这个方案并没有考虑到机器动态伸缩等复杂场景。
如果还需要考虑这些,最好还是考虑一下分布式锁的方案。
总结:稀缺商品的超卖绝对是一大不幸。
如果超卖数量较大,甚至会对平台造成非常严重的运营影响和社会影响。
经过这次事故,我意识到项目中的任何一行代码都不能掉以轻心,否则在某些场景下这些正常工作的代码会成为致命的杀手!对于开发商来说,在制定开发计划时必须仔细评估计划。
我们如何全面评估该计划?继续学习!

高并发必备,使用Redis分布式锁必须注意的10个细节

在分布式系统中,实现分布式锁是一个常见的需求。
为了进一步提升性能,采用了Redis作为分布式锁工具,但高性能且数据安全的分布式锁并不容易实现。
下面,我们将讨论分布式锁应该具备的特性以及使用Redis实现分布式锁的常用方法和注意事项。

分布式锁需要满足以下特性:

互斥性:保证同一时刻只有一个线程可以获取锁。
原子性:加锁和解锁过程应该被视为不可分割的原子操作。
耐用性:即使在异常情况下,锁也必须保持有效。
撤销:即使业务代码执行失败,也允许释放锁。
重入:允许同一个线程多次获取同一个锁。

下面是使用Redis实现分布式锁的10种方式,每种方式都有自己的优缺点。
下面将重点分析几种常见的方法:

使用setnx命令

使用Redis的setnx命令可以实现分布式锁。
该命令的作用是设置键值对。
实现示例如下:

伪代码实现如下:

但是这种方法有缺点:如果执行业务代码时出现异常,锁可能永远不会被释放。
要解决此问题,请确保try/eventual中不包含业务代码在块中运行,保证无论出现异常都能释放锁。

防止锁释放失败并添加过期时间

仅仅设置过期时间并不能防止服务器宕机时锁释放失败。
为了解决这个问题,必须在原子操作中添加锁定和过期操作。
从Redis版本2.6.12开始,使用setnx命令同时设置过期时间是一个原子操作。
示例命令为:“SETkeyvalue[EX秒][PX毫秒][NX|XX]”。

原子锁和锁释放原子操作

锁释放过程必须避免出现问题,例如可能导致其他线程释放锁的非原子操作。
正确的做法是在释放锁时判断lock_value是否属于当前线程,并使用Lua脚本实现原子操作。

锁自动续订

为了防止其他线程因为锁过期而被抢占,必须实现锁自动续订功能。
通过周期检测和自动延长锁有效期,确保锁的耐用性。

提交事务之前释放锁

在事务中加锁和释放锁时,必须确保在提交事务之前释放锁,以避免互斥锁失败。
正确的做法是将加锁和释放锁操作从事务逻辑中分离出来,分别在事务开始和结束时执行。

可重入锁

要实现可重入锁,需要计数器必须递增以记录锁的数量。
每关闭一次锁,计数器就加一;当锁打开时,计数器减一。
当计数器达到零时,释放锁。

集群锁定

使用Redis集群锁定时,互斥锁可能会因故障转移而失效。
RedisLock(Redis分布式锁)通过计算多个Redis实例的锁状态来保证锁的可靠性,提供了集群锁问题的解决方案。

热门文章
1
Redisson分布式锁深度解析:Red... Redis实现分布式锁+Redisson源码解析在某些场景下,多个进程需要以互斥...

2
深度解析Docker:容器技术提升应用部... docker是什么Docker是一种强大的开源容器技术,它将应用程序及其所有依赖...

3
Docker dockercp命令:容器... Dockercp命令详解:在Docker容器和主机之间复制文件/...

4
Redis KEY模糊查询优化策略及SC... RedisKEY*模糊查询导致交互速度慢、阻塞其他Redis操作在Redis中使...

5
Redisson深度解析:分布式锁实战与... Redis:redis分布式锁实战之redisson在分布式环境中;个体锁不能再...

6
Python float()函数:Web... Pythonfloat(input())的用法,web中的应用float(inp...

7
Java单例模式深入解析及实例代码分享 单例模式单例模式实例在Java中,单例模式确保类只存在一个实例。该模式的主要作用...

8
Docker核心原理解析:深入理解Nam... DOCKER总结Docker是一个开源应用程序容器引擎,允许开发人员将其应用程序...

9
C语言字符串输出技巧:指针与数组首地址的... C语言字符串输出Chara[]="aaaaa";printf...

10
200本Java开发精选书籍免费分享!附... Java开发书籍推荐(200多本)我整理了一份Java开发的邮件资源,一共大概2...