《Redis 深度历险》读书笔记-分布式锁

背景:分布式锁本质上就是在redis里面占一个坑,当别的进程进来的时候,发现坑里有人了,就放弃或者等待。


 分布式锁

分布式锁实现一

redis核心指令:setnx(set if not exists) 更多说明

操作代码:

> setnx lock:codehole true
OK
> expire lock:codehole 5
... do something critical ...
> del lock:codehole
(integer) 1

存在问题:

 setnx 和 expire 执行之间服务出现问题,会导致expire不被执行,造成死锁。本质上是因为这俩个指令执行不是原子性。

图例说明

解决方案:作者在后续的版本中对setnx指令添加 expire 指令参数。

> set lock:codehole true ex 5 nx
OK
... do something critical ...
> del lock:codehole

SET 指令解析 SET

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。

实现三大要点:

  • set命令要用set key value px milliseconds nx;

  • value要具有唯一性;

  • 释放锁时要验证value值,不能误解锁;

引入问题

             超时问题:服务的过长影响其他系统处理能力或者过短造成线程并发

一种方式是:添加value随机数,谁加锁,谁来解。存在问题是判断value值和del不是原子的,还会失败,所以要使用lua脚本解决方案。

            主从问题:redis主从复制机制导致数据丢失,会出现如下场景:A 线程获得了锁,但锁数据还未同步到 slave 上,master 挂了,slave 顶成主,线程 B 尝试加锁,仍然能够成功,造成 A、B 两个线程并发访问同一个资源。


分布式锁实现二

结合导读链接效果更佳

Redlock 源码分析

前置说明:引入redisson

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.3.2</version>
</dependency>

 

扩展阅读

如何预防死锁

  1. Client 定时向 Server 发送心跳包,Server 收到心跳包之后,维护 Server 端 Session 并立即回复,Client 收到心跳包响应后,维护 Client 端 Session。心跳包同时承担了延长 Session 租约的功能。
  2. 当锁持有方发生故障时,Server 会在 Session 租约到期后,自动删除该 Client 持有的锁,以避免锁长时间无法释放而导致死锁。Client 会在 Session 租约到期后,进行回调,可选择性的决策是否要结束对当前持有资源的访问。
  3. 对于未设置过期的锁,也就意味着无法通过租约自动释放故障 Client 持有的锁。因此额外提供了一种协商机制,在加锁的时候传递一些 condition 到服务端,用于约定 Client 端期望 Server 端对异常情况的处理,包括什么情况下能够释放锁。譬如可以通过这种机制实现 Server 端在未收到十个心跳请求后自动释放锁,Client 端在未收到五个心跳响应后主动结束对共享资源的访问。
  4. 尽最大程度保证锁被加锁进程主动释放。

                   a)进程正常关闭时调用钩子来尝试释放锁。

                   b)未释放的锁信息写文件,进程重启后读取锁信息,并尝试释放锁。

如何确保锁的安全性


1. 尽量不打破谁加锁谁解锁的约束,尽最大程度保证锁被加锁进程主动释放。

        a)进程正常关闭时调用钩子来尝试释放锁。

        b)未释放的锁信息写文件,进程重启后读取锁信息,并尝试释放锁。

2. 依靠自动续约来维持锁的持有状态,在正常情况下,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。一定程度上防止如下情况发生。

        a)线程 A 获取锁,进行资源访问。

        b)锁已经过期,但 A 线程未执行完成。

        c)线程 B 获得了锁,导致同时有两个线程在访问共享资源。

3. 提供一种安全检测机制,用于对安全性要求极高的业务场景。

        a)对于同一把锁,每一次获取锁操作,都会得到一个全局增长的版本号。

        b)对外暴露检测 API checkVersion(lock_name,version),用于检测持锁进程的锁是不是已经被其他进程抢占(锁已经有了更新的版本号)。

        c)加锁成功的客户端与后端资源服务器通信的时候可带上版本号,后端资源服务器处理请求前,调用 checkVersion 去检查锁是否依然有效。有效则认为此客户端依旧是锁的持有者,可以为其提供服务。

        d)该机制能在一定程度上解决持锁 A 线程发生故障,Server 主动释放锁,线程 B 获取锁成功,A 恢复了认为自己仍旧持有锁而发起修改资源的请求,会因为锁的版本号已经过期而失败,从而保障了锁的安全性。

猜你喜欢

转载自blog.csdn.net/onnwll/article/details/84872677