Redis分布式锁学习记录
一、为什么要用分布式锁
首先我们要明确一个定义,分布式锁是为了保证统一时间只有一个客户端能对共享资源进行操作(update、delete、insert)。
在涉及到一些订单、库存、金额时,为了避免出现超卖、重复扣款等情况发生,我们必须要保证从下单到扣款一系列的操作都只能成功执行一次,而在一般的正式生产环境中为了保证服务的高可用性我们会采用服务集群的模式,这也就直接宣布@Synchronized、Lock这些方法没办法有效地保证集群服务之间的数据一致性,分布式事务锁也就由此诞生。
二、分布式锁的实现
分布式锁可以有多种实现方式,比如通过Mysql数据库、Redies等就能有多种实现方式。
1.通过唯一索引或主键索引,达到互斥锁的效果
把一把锁的id作为主键插入到数据库,如果锁已存在,其他人就会插入失败,也就抢不到锁,在业务逻辑完成或者超时后再把这条数据删掉。
2.通过Mysql的乐观锁,实现数据一致性的效果
当完成一个update或者delete命令时,再重新查一遍数据看看是否与预期数据一样,如果和预期数据一样这轮操作就算成功完成,如果不一样就要进行事务回滚,但是这种方法显然难以避免在高并发情况下产生脏读的情况。
3.通过Redis实现
用Mysql固然可以实现分布式锁,但是在高并发的场景下效率显然是比较容易出现性能瓶颈的,因此我们最好的选择还是通过Redis来实现。
1)抢锁
用Redis的Setnx命令去抢锁,当一个请求成功抢到锁后,其他请求就只能进入等待或者直接返回失败
2)死锁
当一个请求抢到锁后还会存在一个问题也就是如果业务逻辑由于数据库、第三方接口等种种原因导致无限超时,那这把锁就会变成死锁,因此我们要用expire命令给这把锁加一个过期时间,过期时间比必须大于业务正常执行所需要的时间,保证即使锁拥有者不主动释放锁也不会产生死锁的结果。
3)看门狗
当一个请求抢到锁后由于种种原因导致业务执行时间超出正常预期(只超出几秒或者其他可控范围内)时,如果锁由于自动过期时间直接被释放,这时第二个请求抢到这把锁开始执行业务,执行业务期间上一个得到锁的线程执行释放锁的动作,把第二个请求得到的锁释放了,这就产生锁错乱的结果。这时候我们要引入一个看门狗的概念,其作用就是对请求一这样小超时但是不至于到死锁的级别时进行一个锁续时的操作,看门狗默认有效时间是30s,也就是说30s就不会再进行续时操作了,同时在finally代码块中写上释放锁的动作,进一步保证锁的安全和有效性。
4)分布式Redis节点
经过以上几个步骤的逐步完善,在大部分情况下我们已经能在分布式锁的作用下保证数据的安全性和一致性,但是在极端情况下譬如Redis节点崩了,那就直接造成缓存穿透、缓存血崩等等情况,分布式锁自然也就失效了。这时候我们就可以考虑用多台(单数)Redis单节点来保证redis的高可用,譬如用3台单节点Redis服务,每个请求要在这3个节点中抢到至少2把锁才算是抢锁成功,这样即使暂时一台redis节点宕机了也不会直接造成数据错乱等后果。
5)延迟启动Redis节点
假设现在有3台Redis单机节点,当其中一台Redis节点宕机后,已经抢到锁的请求在业务执行完毕后会释放掉已经抢到的2个锁,其中一个锁的Redis节点已宕机,也就是释放另一个锁,此时第二个请求只抢到一把锁,在这之后拿到第二把锁,抢锁成功,业务继续执行。如果在第一个请求释放锁之前直接把宕机的Redis节点重启,那第二个请求就会直接拿到重启的Redis节点的锁,开始执行业务,就会出现两个请求同时执行业务的情况,也就是锁失效了。因此我们要至少确保一轮业务跑完,释放掉锁之后再重启宕机的Redis节点。
三、通过阿里开源组件Seata实现强一致性的分布式锁
阿里开源组件Seata在进行配置后可以用一个@GlobaTranscational注解保证这个业务类下所有方法都能具有数据强一致性,也就是所有步骤都结束后和预期结果对比没有报错,再统一对数据库进行操作。相比于前面用Redis就算弱一致性,报错后回滚是分开回滚的。由此不难判断Seata虽然能保证数据的强一致性,但是效率也是比较低的,在对于金融操作如转账等我们可以用Seata来处理,对于一些库存的加减我们可以用效率更高的Redis分布式锁来处理。