Redis的分布式锁

引入:在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况.此时就需要通过锁来做互斥 控制,避免出现类似于"线程安全"的问题. ⽽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成功的节点个数超过总数的一般,就是视为加锁成功,同理解锁的时候,也就会把上述节点都设置一遍解锁

相关推荐
Elastic 中国社区官方博客3 小时前
在 Elasticsearch 中使用 Mistral Chat completions 进行上下文工程
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
编程爱好者熊浪5 小时前
两次连接池泄露的BUG
java·数据库
cr7xin6 小时前
缓存三大问题及解决方案
redis·后端·缓存
TDengine (老段)6 小时前
TDengine 字符串函数 CHAR 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
qq7422349847 小时前
Python操作数据库之pyodbc
开发语言·数据库·python
姚远Oracle ACE7 小时前
Oracle 如何计算 AWR 报告中的 Sessions 数量
数据库·oracle
Dxy12393102167 小时前
MySQL的SUBSTRING函数详解与应用
数据库·mysql
码力引擎8 小时前
【零基础学MySQL】第十二章:DCL详解
数据库·mysql·1024程序员节
杨云龙UP8 小时前
【MySQL迁移】MySQL数据库迁移实战(利用mysqldump从Windows 5.7迁至Linux 8.0)
linux·运维·数据库·mysql·mssql
l1t8 小时前
利用DeepSeek辅助修改luadbi-duckdb读取DuckDB decimal数据类型
c语言·数据库·单元测试·lua·duckdb