当前位置:首页 > 问答 > 正文

秒杀怎么稳?Redis分布式锁来帮忙,性能和安全都能顶得住

秒杀怎么稳?Redis分布式锁来帮忙,性能和安全都能顶得住 来源:综合自多个技术社区博客、开发者经验分享以及Redis官方文档的应用场景描述)

秒杀这事儿,听起来就刺激,但对后台系统来说,简直就是一场高压测试,想象一下,成千上万的用户在同一秒钟点击同一个按钮,都想去抢那寥寥几件特价商品,如果系统没处理好,轻则商品超卖(库存变成负数),重则整个服务器直接被流量冲垮,页面卡死,谁都买不了。“稳”是秒杀的第一要义,那怎么才能稳呢?在很多技术方案里,Redis分布式锁扮演了一个非常关键的角色,它就像一个大活动现场唯一的总指挥,确保关键环节井然有序,既能扛住高并发,又能保证数据不出错。

要理解分布式锁为啥重要,得先看看秒杀的核心难题在哪,最核心的问题就是“超卖”,比如某款手机秒杀只剩10台了,但同时有1万个请求涌进来,系统需要准确无误地只让前10个请求成功扣减库存,后面的9990个请求都得老老实实被告知“已售罄”,如果这个扣减库存的动作控制不好,很可能出现多个请求同时读到的库存都是10,都判断为“有货”,然后都去执行“10-1=9”的操作,结果实际卖出的数量远远超过了10台,这就是典型的并发安全问题。

秒杀怎么稳?Redis分布式锁来帮忙,性能和安全都能顶得住

在单台服务器上,我们可以用编程语言自带的锁(比如Java里的synchronized关键字)来防止这种情况,把扣减库存这段关键代码“锁”起来,同一时间只允许一个线程执行,但秒杀系统为了扛住流量,肯定是好多台服务器(组成了分布式系统)一起干活,这时候,每台服务器自己的锁就管不了别的服务器上的操作了,就好比每个分公司都有自己的规章制度,但无法协调其他分公司的事务,我们需要一个全局的、所有服务器都能认可和遵守的“仲裁者”,这就是分布式锁。

Redis之所以能成为这个“仲裁者”的热门人选,主要是因为它有几个突出的优点(来源:基于Redis特性的普遍认知),第一是快,Redis的数据都在内存里操作,速度极快,这对于分秒必争的秒杀场景至关重要,加锁解锁的延迟必须足够低,第二是它提供了一些原子操作命令,原子操作的意思是,这个操作是不可分割的,要么完全成功,要么完全失败,不会执行到一半被打断,这对于实现锁的可靠性至关重要。

秒杀怎么稳?Redis分布式锁来帮忙,性能和安全都能顶得住

用Redis怎么实现一个简单的分布式锁呢?一个最基础的思路是这样的(来源:Redis官方文档早期提到的SETNX命令应用思路,虽然后续有更完善的方案,但此思路易于理解):

  1. 加锁:当某个秒杀请求到来时,服务器尝试向Redis执行一个命令,比如在一个特定的键(Key)上设置一个值,这个键可以叫lock:seckill:商品ID,设置的时候会用一个命令检查这个键是否已经存在,如果不存在(NX选项),说明锁是空闲的,当前请求成功抢到锁,可以继续执行后续的扣库存、生成订单等操作,如果键已经存在,说明锁已经被其他请求占用了,那么这个请求就只能等待或者直接返回失败。
  2. 执行业务逻辑:抢到锁的请求,获得了“独占权”,可以安全地去数据库里查询库存、判断是否充足、然后扣减库存,因为同一时间只有一个请求能持有这把锁,所以不可能出现超卖。
  3. 解锁:等扣库存、下单等所有关键操作完成后,这个请求必须释放锁,也就是删除Redis里的那个键,这样,其他正在等待的请求才有机会抢到锁,继续处理。

上面这个简单的想法在实际生产中会面临很多挑战,直接用它可能会漏洞百出,这就需要我们精心设计,让这把锁既安全又可靠(来源:技术社区中关于Redis分布式锁最佳实践的广泛讨论)。

秒杀怎么稳?Redis分布式锁来帮忙,性能和安全都能顶得住

死锁问题。 如果某个服务器抢到锁之后,还没来得及释放锁,自己突然宕机了怎么办?那这个锁就永远留在Redis里了,其他所有请求再也拿不到锁,秒杀业务就彻底卡死了,解决办法是给锁设置一个“过期时间”(TTL),即使在加锁成功的同时,给这个键设置一个比如10秒的自动过期时间,这样哪怕持有锁的服务器宕机,10秒后Redis也会自动删除这个锁,避免系统永久死锁,设置过期时间和加锁本身最好是一个原子操作,防止刚加完锁还没设过期时间服务器就挂了的情况。

误删别人锁的问题。 假设请求A抢到锁,设置的超时时间是30秒,但可能因为网络延迟或程序内部某些原因,A的业务逻辑执行了35秒(超过了30秒锁的过期时间),这时候,锁因为超时被Redis自动释放了,请求B趁虚而入,成功加锁,紧接着,A老兄终于执行完了,它还以为锁是自己的,顺手就把锁给删了——结果删的是B刚刚创建的锁!这会导致锁的安全性彻底失效,解决这个问题,需要我们在删除锁的时候,确认一下这把锁是不是还是自己当初设置的那把,可以在设置锁的值(Value)时,存入一个唯一标识(比如UUID),删除前先获取当前锁的值,判断是否与自己的UUID一致,只有一致才允许删除,这个“获取值+判断+删除”的操作也需要是原子的,通常可以用Lua脚本实现,确保执行过程不被中断。

锁过期时间不好把握。 设短了,业务没执行完锁就丢了,容易引发上面误删的问题,设长了,万一持有锁的客户端真的宕机了,系统需要等待更长时间才能恢复,一种优化的思路是使用“看门狗”机制,也就是另起一个线程,在业务逻辑执行期间,定时去检查锁是否还在持有,如果还在就适当延长锁的过期时间。

通过解决这些挑战,我们就能用Redis构建一个相对稳健的分布式锁,在秒杀场景中,这把锁通常用在最关键的“扣减库存”环节,确保库存检查和高并发下的精确扣减是原子性的,这样一来,商品就不会超卖,数据的一致性得到了保障,由于Redis本身的高性能,加锁解锁操作带来的开销很小,不会成为系统的瓶颈,能够顶住海量并发的冲击。

没有任何技术是银弹,Redis分布式锁在极端情况下(如Redis主从架构发生故障切换时可能出现的锁丢失)也可能存在风险,这就需要更复杂的算法(如RedLock)或根据业务场景进行权衡,但对于绝大多数秒杀场景来说,正确实现和使用的Redis分布式锁,无疑是平衡性能与安全性的一个强大工具,是确保秒杀活动“稳”住阵脚的关键技术支柱之一,它就像交通枢纽中的红绿灯,虽然不能消除车辆的数量(并发请求),但能确保车辆(请求)有序通过关键路口(核心资源),避免撞车(数据冲突)和瘫痪(系统崩溃)。