Redis的分布式锁

发布于:2024-04-24 ⋅ 阅读:(29) ⋅ 点赞:(0)

引入:在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况.此时就需要通过锁来做互斥 控制,避免出现类似于"线程安全"的问题. ⽽java的synchronized或者C++的std::mutex,这样的锁都是只能在当前进程中⽣效,在分布式的这 种多个进程多个主机的场景下就⽆能为⼒了. 此时就需要使⽤到分布式锁.

线程安全(多个线程并发执行的时候,执行的先后顺序,是不确定的=>随机性=>需要保证在任意执行下顺序下,执行逻辑都是OK的)=>锁

在分布式系统中,是有很多进程的(每个服务器,都是独立的进程),因此,之前的锁,就难以对现在分布式系统中的多个进程产生制约...分布式系统中,多个进程之间的执行顺序也是不确定的=>随机性

于是就需要引入分布式锁来解决这个问题

例如:

在购买车票的时候:

客户端1先执行查询测票.发现剩余1张,在即将执行1->0过程之前,客户端2也会执行查询余票,发现也是剩余1张,客户端2也会执行1->0过程,就会导致超卖了,卖给两个人

买票服务器在进行买票操作的过程中,先需要加锁(往Redis上设置一个特殊的key-value完成上述买票操作,再把这个key-value删掉)

其他服务器也想买票的时候,也去尝试设置key-value,如果发现key-value已经存在,就认为"加锁失败"(是放弃/阻塞,就要看具体的实现策略了)

就可以保证第一个服务器执行"查询->更新"过程中,第二个服务器不会执行"查询",也就解决了上述"超卖"问题

所谓的分布式锁,也是一个/一组单独的服务器程序,给其他服务器提供"加锁"这样的服务器(Redis是一种典型的可以用来实现分布式锁的方案,但是不是唯一的一种  业界可能会使用MySQL/zookeeper这样的组件来实现分布式锁的效果)

刚才买票场景,使用MySQL的事务,也可以使用批量执行 查询+修改操作,但是分布式系统中,要访问的共享资源,不一定MySQL......也可能是其他的存储介质,没有事务,也可能是执行一段特定的操作,是通过统一的服务器完成执行动作

使用setnx确实可以得到"加锁"效果,针对解锁,就可以使用del命令来完成(某个服务器加锁成功了(setnx成功)执行后续逻辑过程中,程序崩溃了(没有执行到解锁))

针对这一问题:可以给set的key设置过期时间,一旦时间到,key就会自动删除掉了

set ex nx这样的命令来完成设置(比如:设置key的过期时间为1000ms,那么意味着即使出现极端情况,某个服务器挂了,没有正确的释放锁,这个锁最多保持1000ms,也会自动释放了)

注:setnx   expire这样的设置方式是不对的,务必要使用set ex  nx这样的方式来设置

因为Redis上的多个命令之间,无法保持原子性的,此时,就可能会出现,这两个命令,一个成功,一个失败的情况,相比之下,使用一条命令设置,是更加稳妥的

问题1:

所谓的加锁就是给Redis上设置一个key-value;所谓的解锁就是把Redis上这个key-value删除掉

是否会出现,服务器1执行了加锁,服务器2执行了解锁(正常来说肯定不是故意的,但是代码总会有bug,不小心执行到了解锁操作,就可鞥进一步给整个操作系统带来更加严重的问题(比如像超卖))?

方法:

为了解决上述问题,就需要引入一点校验机制

1.给服务器编号,每个服务器都有自己的身份标识

2.进行加锁的时候,设置key-value,key对应着要针对那个资源加锁(比如车次),value就可以存储刚才服务器编号,标识出当前这个锁是那个服务器加上的

后续解锁的时候就可以校验了:解锁的时候,先查询一下这个锁对应的服务器编号,然后判定一下这个编号是否就是当前执行解锁的服务器编号,如果是,才能真正执行del,如果不是,就失败(服务器这边要完成的逻辑,通过上述校验,就可以有效避免"误解锁")

问题2:

在解锁的时候,先查询判定,再进行del(此处是两次操作(不是原子的),就可能出现问题)

一个服务器内部,也可能是多线程的,此时,就可能同一个服务器,两个线程都在执行上述解锁操作

第一种情况:在线程A执行完GET之后,线程B来执行GET,线程B和线程A获取到了同一个锁的服务器编码,后面线程执行了DEL操作,就把锁给删了,线程B不知道锁删了,会二次执行删锁操作

第二种情况:在线程B执行了GET操作之后,因为线程A已经把解锁了所以服务器2可以进行加锁操作,这样,后面线程B在执行解锁操作的时候,就会把服务器2的锁给删了

这里使用事务,能解决上述问题(Redis事务虽然弱,但是能够避免插队),但是有更好的方案lua脚本(lua语言特别轻量(实现一个lua解释器,消耗的体积是非常小的),可以使用lua编写一些逻辑,把这个脚本上传到Redis服务器上,然后就可以让客户端来控制Redis执行上述脚本了,Redis执行lua脚本的过程也是原子的,相当于执行一条命令一样(实际上lua中可以写多个命令))

if redis.call('get',KEYS[1]) == ARGV[1] then

    return redis.call('del',KEYS[1])

else

    return 0

end; 

问题3:过期时间续约:

要在加锁的时候,给key设定过期时间

过期时间设置多少合适?

*如果设置的过短,就可能在业务逻辑还没执行完,就释放锁了

*如果设置的时间太长,就也会导致"锁释放不及时"的问题(就是突然系统崩溃,解不了锁了)

更好的方式是"动态续约"(往往也是需要服务器这边有一个专门的线程(watch dog),负责续约这件事情)

初始情况下,设置一个过期时间(比如设置1s)就提前在还剩300ms的时候(也不一定是300ms,数值都是灵活调整的),如果当前业务还没有执行完,就把过期时间再续上1s,等到时间又快到了,软任务还没执行完,就再续(无限续杯)

如果服务器崩溃了,自然就没人负责续约了,此时,锁就能在较短时间内自动释放

问题4:使用Redis作为分布式锁,Redis本身有没有可能挂了呢?

     进行加锁,就是把key设置到主节点上,如果主节点挂了,有哨兵自动的把从节点升级成主节点,进一步才能保证刚才锁仍然可用

注:主节点和从节点数据同步,是存在延时的,可能主节点收到了了set请求,还没来的及同步给从节点,但是,刚才加锁对应的数据,也是不存在的

此时Redis给出的一个方案就是redlock算法(冗余):

 此处加锁,就是按照一定的顺序,针对这些组Redis都进行加锁操作;如果某个节点挂了(某个节点加不上锁,没关系,可能是Redis挂了)继续给一个节点加锁即可;如果写入key成功的节点个数超过总数的一般,就是视为加锁成功,同理解锁的时候,也就会把上述节点都设置一遍解锁