Redis典型应用 - 分布式锁,引入过期时间,Lua脚本原子解锁操作,看门狗机制实现过期时间动态续约,Redlock算法

Redis典型应用 - 分布式锁,引入过期时间,Lua脚本原子解锁操作,看门狗机制实现过期时间动态续约,Redlock算法
目录什么是分布式锁多节点对共享资源的访问分布式锁的基础实现实现步骤引入过期时间引入校验机制引入 Lua对 Lua 的介绍引入看门狗 (watch dog)两个时间概念具体例子注意点(对锁的续约时间理解)引入 Redlock 算法解决方案什么是分布式锁?多节点对共享资源的访问在分布式系统中,也会涉及到多个节点对共享资源进行访问的情况,这个时候我们需要借助锁来实现对共享资源的互斥访问,避免出现类似前面的“线程安全问题”而 Java 的synchronized或者 C的std::mutex,这样的锁只能在同一个进程中生效,也就是可以实现在一个进程中,多个线程对共享资源的互斥访问,但在分布式系统的多主机,多服务场景下就不能发挥出作用了此时就需要使用分布式锁了分布式锁实际上是利用一个公共服务器来记录锁的状态可以使用Redis,也可以是 MySQL 或者 ZooKeeper,也可以是我们自己写的服务分布式锁的基础实现简单实现: 通过一对键值对来标记锁的状态加锁设置一对特殊的键值对解锁删除这对键值对举一个具体的分布式系统买票场景,在 12306 上,每一个车次的车票都是有限的,当我们进行买票的时候,会执行以下逻辑判断余票数量是否大于 0大于 0 ,则余票数量 - 1 来更新余票数量实现步骤在每一个买票服务器执行具体的买票逻辑之前先要访问 Redis 往 Redis 中设置一对特殊的键值对key 是车次 001value 是任意值如 1如果上面设置键值对成功我们就视为加锁成功继续去执行后面具体的买票逻辑执行完之后删除这个已经设置好的键值对视为解锁成功。当买票服务器 1 设置好键值对加锁成功之后买票服务器 2 也想买票但发现 key车次 001 已经存在加锁失败则买票服务器 2 需要等到买票服务器 1 解锁之后才能成功加上锁在上面两个步骤中Redis 提供的 setnx 命令刚好适合我们的场景需要当 key 不存在时才设置key 存在时不设置引入过期时间在上面的场景中当一个买票服务器获取到锁之后如果此时服务器意外宕机了则会导致解锁操作不能被执行其他服务器则始终无法获取到锁不能进行买票为了应对这个问题我们可以在一开始加锁设置键值对的时候同时指定过期时间使用如下命令set key value[expirationEXseconds|PXmilliseconds][NX|XX]在这里面中我们需要注意设置键值对和指定过期时间必须使用一条命令Redis 的单条命令执行具有原子性当在执行 incr 命令时其他客户端也刚好在执行 incr 操作此时的执行顺序是串行化的Redis 的多条命令执行不具有原子性在 Redis 事务中当一条命令执行成功另一条命令执行失败之后此时并不会回滚。set key value expire key seconds所以当使用上面两条命令设置锁成功指定锁过期时间失败之后此时还是导致锁不能被释放其他服务器始终无法获取到锁引入校验机制在上面场景中买票服务器 1 加上的锁可能会被其他买票服务器释放因此我们可以引入一个校验 ID例如买票服务器编号是 001将一开始加锁中的 value 设置成 001代表是 001 号服务器加上的锁在后面释放锁的时候先通过 value 值来判断是不是当前服务器加上的锁如果是则可以释放锁如果不是则不能释放锁可以用一段伪代码来描述上面的逻辑:1Stringkey[要加锁的资源 id];2StringserverId[服务器的编号];34// 加锁设置过期时间为 10s5redis.set(key,serverId,NX,EX,10s);67// 执行各种业务逻辑比如修改数据库数据。8doSomething();910// 解锁删除 key。但是删除前要检验下 serverId 是否匹配。11if(redis.get(key)serverId){12redis.del(key);13}从这段伪代码中可以看出,get 和 del 这两个操作不是原子的引入 Lua:为了使锁解锁操作是原子的,我们使用 Redis 的 Lua 脚本功能对 Lua 的介绍: Lua 也是一个编程语言。读作 “撸啊”。是葡萄牙语中的 “月亮” 的意思。(出自于 Lua 官方文档 https://www.lua.org/about.html)Lua 的语法类似于 JS是一个动态弱类型的语言。Lua 的解释器一般使用 C 语言实现。Lua 语法简单精炼执行速度快解释器也比较轻量(Lua 解释器的可执行程序体积只有 200KB 左右)。因此 Lua 经常作为其他程序内部嵌入的脚本语言。Redis 本身就支持 Lua 作为内嵌脚本。使用 Lua 脚本完成上述解锁功能:1ifredis.call(get,KEYS[1])ARGV[1]then2returnredis.call(del,KEYS[1])3else4return05end;上述代码可以编写成一个.lua后缀的文件由redis-cli或者redis-plus-plus或者jedis等客户端加载并发送给 Redis 服务器由 Redis 服务器来执行这段逻辑。一个 lua 脚本会被 Redis 服务器以原子方式来执行。引入看门狗 (watch dog):两个时间概念:lockWatchdogTimeout:锁每次被续约之后的有效期(也就是还有多长时间才到期 )续约触发间隔:看门狗多长时间进行一次检查在上面买票场景中,有可能还存在一个问题:当这个服务器已经加上锁之后,可能还没有执行完具体的业务逻辑之后,锁便过期了,也就是我们要动态控制锁的过期时间.不能让锁的过期时间太长,不然当一个服务器发生宕机之后,不能完成解锁,其他服务器也就不能获取到锁不能让锁的过期时间太短,不然当这个服务器还没有执行完业务逻辑之后,锁就过期了,这把锁便失效了,其他买票服务器也可以获取到这把锁,不能实现资源的互斥访问,也就会导致超卖问题(一张票卖给两个人)所谓 watch dog,本质上是加锁的服务器(业务服务器)上的一个单独线程,负责对锁过期时间完成续约操作具体例子:初始情况下设置锁过期时间为 10s**(****lockWatchdogTimeout)**.同时设定看门狗线程每隔 3s 检测一次那么当时间间隔达到 3s 的时候,看门狗就会判定当前任务是否完成.如果当前任务已经完成,则会使用 Lua 脚本完成原子解锁如果当前任务还没有完成,看门狗线程则会对锁过期时间进行续约操作,重新设置这个锁的过期时间为10s注意点:(对锁的续约时间理解)当进行续约操作时,此时锁的有效时间不是累计增加,而是重新设置为一开始指定的有效期如一开始指定有效期为 10s,看门狗每隔 3s 进行一次检查锁设置成功,有效时间为 10s第一次续约,锁的有效时间为 10s第二次续约,锁的有效时间为 10s第三次续约,锁的有效时间为 10s引入 Redlock 算法:实践中的 Redis 一般是以集群的方式部署的(至少是主从的形式,而不是单机),有可能会出现以下极端情况:服务器1 向 master 节点进行加锁操作. 这个写入 key 的过程刚刚完成,master 挂了; slave 节点升级成了新的 master 节点. 但是由于刚才写入的这个 key尚未来得及同步给 slave 呢, 此时就相当于 服务器1 的加锁操作形同虚设了,服务器2 仍然可以进行加锁(即给新的 master 写入 key. 因为新的 master 不包含刚才的 key).解决方案:我们引入一组 Redis 节点. 其中每一组 Redis 节点都包含一个主节点和若干从节点. 并且组和组之间存储的数据都是一致的, 相互之间是 “备份” 关系(而并非是数据集合的一部分, 这点有别于 Redis cluster).加锁的时候, 按照一定的顺序, 写多个 master 节点. 在写锁的时候需要设定操作的 “超时时间”. 比如 50ms. 即如果 setnx 操作超过了 50ms 还没有成功, 就视为加锁失败。如果给某个节点加锁失败, 就立即再尝试下一个节点.当加锁成功的节点数超过总节点数的一半, 才视为加锁成功.这样的话, 即使有某些节点挂了, 也不影响锁的正确性.同理, 释放锁的时候, 也需要把所有节点都进行解锁操作. (即使是之前超时的节点, 也要尝试解锁, 尽量保证逻辑严密).简而言之, Redlock 算法的核心就是,加锁操作不能只写给一个 Redis 节点, 而要写个多个!! 分布式系统中任何一个节点都是不可靠的. 最终的加锁成功结论是“少数服从多数的”.由于一个分布式系统不至于大部分节点都同时出现故障, 因此这样的可靠性要比单个节点来说靠谱不少.