Redis原理之分布式锁

发布于:2025-07-24 ⋅ 阅读:(20) ⋅ 点赞:(0)

上篇文章:

Redis原理之缓存https://blog.csdn.net/sniper_fandc/article/details/149141968?fromshare=blogdetail&sharetype=blogdetail&sharerId=149141968&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link​​​​​​​

目录

1 基本实现

2 引入过期时间

3 引入校验id

4 引入lua脚本

5 引入看门狗机制(watch dog)

6 引入Redlock算法


        普通的锁比如Java中的synchronized通常解决的是线程安全问题,但是到了多进程多服务器环境下,普通的锁就无法保证多个进程或节点同时访问同一个资源的问题,这就需要分布式锁。

        分布式锁从本质来讲是通过一个公共服务器来记录加锁状态。所有的进程或节点访问某个资源时先访问该服务器尝试加锁,待某个已经加锁的进程或节点访问结束后再解锁让其他进程或节点访问。

        注意:个人理解的分布式锁核心问题是进程间通信问题。分布式环境下,进程间通过网络通信,网络IO的时间是各种数据IO方式中最慢的(寄存器>缓存>内存>磁盘>网络),因此进程对一个数据进行访问就有了时延。而在这个时延期间,如果有其他进程也对该数据进行访问(失去了原子性),就可能造成数据不一致或重复读写等问题,因此需要通过锁来控制进程通信的顺序问题(互斥性)。

1 基本实现

        注意:实现分布式锁可以有很多方式,比如Redis、MySQL和ZooKeeper等等。

        这里使用Redis来实现分布式锁。核心操作是当多个客户端进程查询同一个资源时,请求负载均衡到某个服务器后首先访问Redis,第一个进程会在Redis设置关于该资源的key-value(加锁)。此时其他进程(服务器)如果也想查询该资源就会去Redis尝试加锁,如果发现key已经存在就加锁失败,可以选择阻塞或返回(具体哪种看实现场景)。待第一个进程访问资源结束后,就会把Redis上的这个key-value删除(解锁)。

        使用setnx命令即可实现这个分布式锁:key不存在则设置(加锁),否则失败(加锁失败)。使用del命令即可实现解锁。

2 引入过期时间

        当在Redis加锁的服务器如果出现意外情况,比如宕机、断电等等,锁还未释放,此时其它服务器就无法对该资源加锁,从而无法访问资源。

        解决办法:对key-value引入过期时间,即使用set ex nx命令来设置过期时间。

        注意:不能使用setnx、expire两条命令来实现上述效果,因为Redis的命令是串行化执行的,无法保证多条命令执行的原子性。

3 引入校验id

        上述方案还有问题,由于多个服务器都可以访问Redis来执行各种命令,就可能出现一个服务器设置键值对(加锁),另一个服务器删除键值对(解锁),从而导致问题(当然这个问题不是故意的)。

        解决办法:为每个服务器引入校验id,加锁时针对某个资源的key的value设置为服务器的校验id值。当解锁时,首先查询准备执行del命令的服务器的校验id是否和value值相同,如果相同则允许执行del;如果不同则不允许执行。上述过程由服务器来完成:

String key = [要加锁的资源id];

String serverId = [服务器的id];

// 加锁, 设置过期时间为 10s

redis.set(key, serverId, "NX", "EX", "10s");

// 执行各种业务逻辑, 比如修改数据库数据.

doSomeThing();

// 解锁, 删除 key. 但是删除前要检验下serverId是否匹配.

if (redis.get(key) == serverId) {

    redis.del(key);

}

4 引入lua脚本

        新的问题又出现了,执行解锁的过程中,由于get命令和del命令的执行不是原子性的,因此就会出现命令插队的情况。比如同一个服务器多个线程都需要操作同一个资源,当执行解锁时按照如下所示顺序执行:

        在线程2执行完get判断锁的校验id和服务器id一样后,线程1继续执行del已经解锁了。而此时服务器2判断该资源是无锁状态就加锁,此时线程2又执行del把刚加的锁给解除了,从而引起问题。

        解决办法:保证解锁时两个命令执行的原子性。可以用redis事务来解决,但是更好的方案是lua脚本。Lua是Redis的内嵌脚本语言,Lua脚本的执行是原子性的:

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

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

else

    return 0

end;

        KEYS和ARGV是调用脚本时输入的参数。上述代码存于.lua后缀的文件,并发送给Redis服务器来执行。

5 引入看门狗机制(watch dog)

        从引入过期时间开始就一直存留一个问题:过期时间设多长合适?过短的时间可能服务器还没有访问完资源锁就被释放了;过长的时间可能服务器早已经结束资源访问但是锁迟迟无法释放(影响系统并发量和吞吐量)。

        解决办法:动态调整过期时间,初始设置一个时间,待时间快结束时(定期)查询服务器是否访问资源结束,如果未结束则延长时间(续约,重新设置过期时间);如果已经结束则通过lua脚本解锁。这个动态调整过期时间的过程由服务器的一个线程专门负责,该线程也被成为看门狗,因此这个机制就被称为看门狗机制

        上述过程,如果服务器宕机,看门狗线程也就挂了,此时key的过期时间无人续约,也就快速解锁便于让其它服务器进程能及时访问资源。

6 引入Redlock算法

        通常Redis节点的部署不是单机形式,至少是主从复制。主从复制存在从节点和主节点短时间的数据不一致,假设向master进行加锁操作,master还未及时同步给slave就故障,此时其它服务器也就可以进行加锁操作。

        解决办法:引入Redlock算法,具体而言,加锁时不是向一个master节点写入,而是向所有的master节点都加锁。如果超过超时时间仍未加锁成功,则认为对该master加锁失败(该节点可能宕机)。当向所有master都尝试加锁后,统计加锁成功的节点数超过半数master节点数,则认为加锁成功。通过这样,即使有master节点挂了,其它master节点仍然保存着锁的信息。

        解锁时,也要向所有的master节点尝试解锁。即Redlock算法的核心思想就是冗余备份,同时结合分布式的少数服从多数的思想。


网站公告

今日签到

点亮在社区的每一天
去签到