分布式锁方案—redlock算法

分布式系统的复杂之处在于在不同进程需要互斥的访问共享资源时的问题。例如,

1、分布式ID,当数据水平拆分之后,如何保证ID的唯一性,并且尽可能的短;

2、秒杀系统中的库存,数据结构为商品ID,剩余数量,每次成交会减掉响应数量。如何保证不会超卖;

锁的目的是确保多个节点、进程做同样工作的时候,只有一个可以执行成功。有且只有一次。

 

实现分布式锁有很多方案,例如基于数据库实现,基于zookeeper实现,如果吞吐量还是不能满足,比较广泛的做法是用分布式缓存来实现。

一、Redis单节点方式实现

核心就是围绕SETNX(SETIF NOT EXISTS)实现,

 


key不存在时返回1,当key存在时返回0。因为我们都知道redis是单线程的,所以在redis服务侧不会有线程安全问题。当返回1的时候认为获得锁成功,可以进行相应的业务处理,处理完成后,删除key,释放锁,当返回0时表示失败;

1.超时问题

如果线程A拿到了锁,去处理业务的过程中,发生阻塞,例如数据库执行比较慢,或者service1发生故障了,这时候如果没有超时时间,系统将永久的死锁。所以setnx可以接受第三个参数,也就是超时时间。


这里面存在问题,因为你并不知道超时的情况下,业务到底有没有处理成功,还有没有在继续进行。所以,这里的锁并不是绝对的。

2.如何释放锁?

直接del可以吗?答案是否定的。

这里要注意另一个问题,因为超时时间是放在redis服务端计算的,如果service1超时了,但是他自己是不知道自己超时的,除非不断的去轮训redis确认,不断轮训也是有问题的,因为轮训是有时间差的,例如,你请求redis的时候还没超时,恰好删除的时候超时了,service2刚拿到锁,service1就误删了service2的锁,造成锁失效。

解决方案就是setnx的时候value值可以在客户端生成一个随机值,例如set lock_namerandom_value 。删除的时候根据key获取value,如果相同就删除。当然,这个地方必须是原子的,否则,判断到删除之前还是有可能发生变化。可以通过lua脚本来实现。

3.单点问题

还有另外一个问题,redis是单点的,如果redis一旦挂掉,整个全玩完。
有人说,可以用master-slave啊,但是master-slave之间是异步传输数据的,也就是不能设定为masterslave都写成功了才返回。Redis-cluster也是异步的。

 

二、Redlock实现方案



Redlockredis作者antirez大神在redis官网中给出的一种基于redis的分布式锁方案。直白点说,就是采用N(通常是5)个独立的redis节点,同时setnx,如果多数节点成功,就拿到了锁,这样就可以允许少数(2)个节点挂掉了。整个取锁、释放锁的操作和单节点类似。

是不是这样就完美了呢?当然不是。

1.    重启问题

假设一共有5个节点(A/B/C/D/E),service1成功获取了锁,注意这里,service1A/B/Csetnx成功,但是并没有在D/E上成功,如果C节点挂掉,又恰巧重启了,如果C节点并没有持久化,这时候service2也可以成功锁住C/D/E,导致锁失效。

有人说,那C持久化不就行了吗,实际上设置为同步的持久化方式对性能影响比较大。也就是常说的appendfsync always,如果是机械硬盘,吞吐量可能会从10万级降到几百。有人说用固态硬盘不就解决了吗?土豪是可以用的,吞吐量确实能到万级,但是大量的、频繁的写入容易导致写入放大。

这个问题的解决方案也非常简单。就是延迟重启(elayed restarts),说白了就是等到挂掉节点上的所有锁都过期了再重启,重启后,以前的锁都已经失效。

2.    响应失败

如果一个节点获取锁成功了,四个节点都setnx成功,一个失败了,失败的情况是:发起请求成功,在返回ack的时候失败,这时候客户端会认为是失败了,而删除锁的时候没有删除这个节点,这就会导致过期之前,这个节点获取锁一直失败,所以,正确的做法是无论setnx成功还是失败,都应该执行一次删除操作。

三、总结

看似已经是比较完美的方案了,里面实际上还有很多问题值得深入思考。另一位大神Martin Kleppmann发起了挑战,在他的blog中描述了很多漏洞。《Howto do distributed locking》,antirez给予了正面回应。限于篇幅,下回分解。

四、参考文档

https://redis.io/topics/distlock

猜你喜欢

转载自blog.csdn.net/douliw/article/details/75137928