8、Redis分布式锁

想要实现分布式锁,Redis必须要有互斥能力,比如setnx命令,即如果key不存在,才会设置它的值。

客户端1:
在这里插入图片描述

客户端2:
在这里插入图片描述

此时,加锁成功的客户端就可以去操作共享资源。操作完成后,还要及时释放锁,给后来者让出操作共享资源的机会,这里我们可以使用del命令删除这个key即可。

问题:当客户端1命到锁后,如果程序处理业务逻辑异常,没有及时释放锁或是进程挂了,没机会释放锁,那么就会造成死锁,客户端1一直占用这个锁,其它客户端就永远拿不到锁了。

如何避免上述的死锁呢?

可以给这个key设置一个过期时间,假设操作共享资源的时间不会超过10s,那么在加锁时给这个key设置10s过期即可:这样无论客户端是否异常,这个锁10s后都会被自动释放。

setnx lock 1  //加锁
expire lock 10  //10s后自动过期

这样处理的问题又来了。。。

加锁和设置过期是2条命令,如果只执行了一条呢?不能保证原子性

那么我们就用下面这一条命令来执行吧,以保证执行的原子性

set lock 1 ex 10 nx

这样虽然解决了死锁,但是。。。。你以为就这样了吗?不。。。问题来了:

想想客户端1加锁成功,开始操作共享资源,但是由于种种原因10s后还没有处理完,锁就被自动释放了,然后客户端2来加锁成功,开始操作共享资源,这时客户端1操作共享资源完成,释放锁(释放的是客户端2的锁)

有什么好的解决方案吗?

锁过期:我们可以延长过期时间,比如把10s改成15s,

锁被别人释放:我们可以在客户端在加锁时,设置一个只有自己知道的唯一标识进去

set lock $uuid ex 20 nx

在释放锁的时候,先判断这把锁是否还归自己持有,伪代码:

if redis.get("lock") == $uuid: 
redis.del("lock")

这里释放锁使用的get、del两条命令,那么新的问题又来了,在这里我们又会遇到之前说的原子性问题

解决方案:可以写成lua脚本,让Redis执行。

因为 Redis 处理每一个请求是「单线程」执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了。

安全释放锁的lua脚本:

if redis.call("GET",KEYS[1]) == ARGV[1] 
then 
	return redis.call("DEL",KEYS[1]) 
else 
	return 0 
end

好了,现在我们小结一下优化后的基于Redis实现分布式锁的流程:

加锁:

SET lock_key $unique_id EX $expire_time NX

操作共享资源

释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁

现在要考虑的是锁过期时间怎样评估,这个不好评估要怎么办???

方案来了:加锁时,先设置一个过期时间,然后我们开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。

嗯嗯,Redis都已经帮我们封装好了,这个守护线程一般我们把它叫做看门狗线程

Github上可以学习如何使用:https://github.com/redisson/redisson/

好了,我们再来看看前面遇到的几个主要问题的解决方案:

1、死锁:设置过期时间

2、过期时间评估不好,锁提前过期:守护线程,自动续期

3、锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放

这还只是在单机的情况下,那么我们一般使用Redis都会采用主从集群+哨兵模式部署,在主从发生切换时,这个分布式锁还会安全吗?

试想:客户端1在主库上执行set命令加锁成功,此时主库异常宕机,set命令还未同步到从库上(主从复制是异步的)从库被哨兵提升为新主库,这个锁在新的主库上,丢失了!

那么又怎样解决这个问题呢?

Redis的作者提出的一种解决方案:红锁(Redlock)

来看看Redis作者是怎样用红锁来解决主从切换后,锁失效问题的:

不再需要部署从库和哨兵实例,只部署主库,3、5、7。。。官方推荐至少5个实例,而且都是主库,它们之间没有任何关系,都是孤立的实例。

整体流程分为5步:

  1. 客户端先获取当前时间戳 T1
  2. 客户端依次向这 5 个 Redis 实例发起加锁请求,且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
  3. 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳 T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
  4. 加锁成功,去操作共享资源
  5. 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)

Redlock还会遇到三座大山:NPC问题

  • N:Network Delay,网络延迟
  • P:Process Pause,进程暂停(GC)
  • C:Clock Drift,时钟漂移

总体来说:Redlock还是不太建议用的,对于效率来说,Redlock比较重,没必要同时部署那么多台实例,对于正确性来说,Redlock是不够安全的,时钟假设不合理,该算法对系统时钟做出了危险的假设(假设多个节点机器时钟都是一致的),如果不满足这些假设,锁就会失效。 无法保证正确性。

基于Zookeeper的锁安全:

  1. 客户端1和2都尝试创建临时节点 如/lock
  2. 假设客户端1先到达,则加锁成功,客户端2加锁失败
  3. 客户端1操作共享资源
  4. 客户端1删除 /lock 节点,释放锁

它是采用了「临时节点」,保证客户端 1 拿到锁后,只要连接不断,就可以一直持有锁。

如果客户端 1 异常崩溃了,那么这个临时节点会自动删除,保证了锁一定会被释放。

客户端 1创建临时节点后,Zookeeper 是如何保证让这个客户端一直持有锁呢?

客户端 1 此时会与 Zookeeper 服务器维护一个 Session,这个 Session 会依赖客户端「定时心跳」来维持连接。 如果 Zookeeper 长时间收不到客户端的心跳,就认为这个 Session 过期了,也会把这个临时节点删除。

总结

Redlock 只有建立在「时钟正确」的前提下,才能正常工作,如果你可以保证这个前提,那么可以拿来使用。

但是时钟偏移在现实中是存在的:

第一,从硬件角度来说,时钟发生偏移是时有发生,无法避免。

第二,人为错误也是很难完全避免的。

所以,Redlock 尽量不用它,而且它的性能不如单机版 Redis,部署成本也高,优先考虑使用主从+ 哨兵的模式 实现分布式锁。

优化方案:

1、使用分布式锁,在上层完成「互斥」目的,虽然极端情况下锁会失效,但它可以最大程度把并发请求阻挡在最上层,减轻操作资源层的压力。

2、但对于要求数据绝对正确的业务,在资源层一定要做好「兜底」,设计思路可以借鉴 fencing token 的方案来做。

键的迁移

move

move key db

move 命令用于在 Redis 内部进行数据迁移,Redis 内部可以有多个数据库,这里只需要知道 Redis 内部可以有多个数据库,彼此在数据上是相互隔离的,move key db 就是把指定的键从源数据库移动到目标数据库中,但多数据库功能不建议在生产环境使用。

dump + restore

dump key
restore key ttl value

dump + restore 可以实现在不同的 Redis 实例之间进行数据迁移的功能,整个迁移的过程分为两步:

  1. 在源 Redis 上,dump 命令会将键值序列化,格式采用的是 RDB 格式。
  2. 在目标 Redis 上,restore 命令将上面序列化的值进行复原,其中 ttl 参数代表过期时间,如果 ttl=0 代表没有过期时间。

需要注意的二点:

第一,整个迁移过程并非原子性的,而是通过客户端分步完成的。

第二,迁移过程是开启了两个客户端连接,所以 dump的结果不是在源 Redis 和目标 Redis 之间进行传输。

migrate

migrate host port key |"" destination-db timeout [copy] [replace] [keys key [key ...]]

migrate 命令也是用于在 Redis 实例间进行数据迁移的,实际上 migrate 命令就是将 dump,restore,del 三个命令进行组合,从而简化了操作流程。migrate 命令具有原子性,而且从 Redis 3.0.6 版本以后已经支持迁移多个键的功能,有效地提高了迁移效率,migrate 在水平扩容中起到重要作用。

整个过程和 dump + restore 基本类似,但是有 3 点不太相同:

第一,整个过程是原子执行的,不需要在多个 Redis 实例上开启客户端的,只需要在源 Redis 上执行 migrate 命令即可。

第二,migrate 命令的数据传输直接在源 Redis 和目标 Redis 上完成的。

第三,目标 Redis 完成 restore 后会发送 OK 给源 Redis,源 Redis 接收后会根据 migrate 对应的选项来决定是否在源 Redis 上删除对应的键。

migrate的参数:

host:目标 Redis 的 IP 地址。

port:目标 Redis 的端口。

keyl “”:在 Redis 3.0.6 版本之前,migrate 只支持迁移一个键,所以此处是要迁移的键,但 Redis 3.0.6 版本之后支持迁移多个键,如果当前需要迁移多个键,此处为 空字符串""。

destination-db :目标 Redis 的数据库索引,例如要迁移到 0 号数据库,这里就写 0。

timeout:迁移的超时时间(单位为毫秒)。

[copy] :如果添加此选项,迁移后并不删除源键。

[replace] :如果添加此选项,migrate 不管目标 Redis 是否存在该键都会正常迁移进行数据覆盖。

[ keys key [ key …]] :迁移多个键,例如要迁移 key1、key2、key3,此处填写“keys key1 key2 key3”。

假设有两个Redis,分别使用源6379端口、目标6380端口,现在将Redis源Redis的键hello迁移到目标Redis中,分为以下几种情况:

情况1:源Redis有键hello,目标Redis没有

migrate 127.0.0.1 6380 hello 0 1000OK

情况2:源Redis和目标Redis都有键hello

如果 migrate 命令没有加 replace 选项会收到错误提示,如果加了 replace 会返回 OK 表明迁移成功。

情况3:源Redis没有键hello

此种情况会收到nokey的提示

情况4:源 Redis执行如下命令完成多个键的迁移

migrate 127.0.0.1 6380 "" 0 5000 keys key1 key2 key3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值