redis 分布式锁

本次分享的话题,主要是redis分布式锁的常见实现方式和分布式的场景下如何实现一个优良的分布式锁的话题讨论,主要词汇如下:
setnx
redLock
redisson

1.为什么需要分布式锁

实际的业务场景中,有很多并发访问的问题:比如下单,修改库存等,可总结为如下的流程:

  • 客户端读数据到本地,本地修改;

  • 客户端修改完数据,会写数据;

这样的流程通常有显著的特点:"读取-修改-写回",简称为RMW操作(Read-Modify-Write)。

多个客户端对同一份数据执行RMW操作的话,需要让RMW和涉及的代码,按照原子方式执行,这种访问同一份数据的RMW操作代码,叫做临界区代码。实际场景,基本如下图:

为什么加锁.png

临界区代码必须要保证多进程串行执行,否则将产生数据不一致等影响。

在redis中,处理临界区的代码通常有三种方式

  • 多个操作合并成一条命令操作:case:INCR/DECR,set至于setnx

  • 多个操作写道Lua脚本中执行

  • 加分布式锁

前两个不是所有的场景都适合。为了保证多进程能串行执行,我们需要一个统一的外部资源来实现这种互斥的能力。

为了追求性能,我们使用redis 分布式锁,当然,还可以是MySQL,Zookeeper等。

2.Redis分布式锁实现

讨论之前,我们有必要分析和总结一下Redis分布式锁应有的设计原则:

  1. 安全属性:互斥,不管任何时候,只有一个客户端能持有同一个锁。

  2. 效率属性A:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。

  3. 效率属性B:容错,只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。

这三点,是最基本的安全和可靠性保障,除此之外,还可以考虑是否支持阻塞和非阻塞、持久性(能否自动续约活自动延期)、是否支持公平性和可重入特性。

可以以一个常见伪代码为案例:

加锁代码形式通常如下:

function writeData(filename, data) {
 var lock = redis.setNx(filename,1);
 redis.expire(filename,3000)
 if (!lock) {
 throw 'Failed to acquire lock';
 }

 try {
 var file = storage.readFile(filename);
 var updated = updateContents(file, data);
 storage.writeFile(filename, updated);
 } finally {
 redis.del(filename)
 }
}

部分代码参考:https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html</pre>

2.1 如何避免死锁

加锁代码通常如下,很容易想到加一个过期时间

SETNX  key 1    //加锁
EXPIRE key 10   //设置锁过期</pre>

时间图如下:

image-20210707200234589.png

问题:

1-进程A在T2-T3之间出现问题,没有机会释放锁,锁一直不释放

2-进程A在T3-T5之间出问题,设置有效期失败,锁一直不释放

解决方案:对于问题1-2,可以使用SET key value [EX seconds] [PX milliseconds] [NX] 保证原子性,redis单节点问题,我们稍后讨论。

修复setnx问题后,我们继续分析有另外一个进程进入的情况,考虑按时间顺序如下场景:

如果锁有效时间10s

1.进程A加锁成功,开始操作,操作时间过了锁有效期

2.进程B申请加锁,开始操作;

3.进程A释放锁(进程B的锁被释放掉了)

问题:

1-锁过期时间控制:如果一个进程执行时间过长,导致锁超期释放,别的进程可获取锁,两个进程同时拥有一把锁,操作同一份RMW 代码

2-释放别人的锁:进程A释放锁的时候,把别的进程的锁释放掉了

问题1我们先不讨论,

2.2 释放别人加的锁

上述问题,图示如下:

image-20211019212545143.png

1-进程A加锁成功

2-进程A执行时间超出锁有效期,进程B获取锁

2-进程A执行完成,释放了B进程加的锁

解决方案:

1-通过控制加锁的value值为 唯一值:SET key random PX 5000 NX,其中,random应该是唯一值

2-删除锁的时候,先获取锁的值是否等于random值,等于则释放,为了保证原子性采用lua脚本,内容如下:

//释放锁 比较random是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then    
 return redis.call("del",KEYS[1])
else    
 return 0
end

如下图:

image-20210707215344288.png

整体代码开起来如下:

function writeData(filename, data) {
 var lock = redis.set(filename,1,px,5000,NX);
 if (!lock) {
 throw 'Failed to acquire lock';
 }

 try {
 var file = storage.readFile(filename);
 var updated = updateContents(file, data);
 storage.writeFile(filename, updated);
 } finally {
 redis.eval("if redis.call("get",KEYS[1]) == ARGV[1] then
 return redis.call("del",KEYS[1])
 else
 return 0
 end")
 }
}

这段代码看起来,解决了我们想到的很多问题,事实上,很多项目中依然采用着,除了上述我们提及过:锁过期时间与线程执行时间不好确定之外,我们继续分析,还有什么问题:

1-锁过期时间

2-redis不能是主从部署方式

3-更宽泛的说来,不支持很多锁的功能:比如,是否公平,是否可重入

其实redisson已经帮我们提供了更加健壮简洁的锁实现

3.Redisson

Redisson 是架设在 Redis 基础上的一个 Java驻内存数据网格框架, 充分利用 Redis 键值数据库提供的一系列优势, 基于 Java 使用工具包中常用接口, 为使用者提供了 一系列具有分布式特性的常用工具类。其中就提供了一种RedLock的加锁算法和实现,讨论之前,我们可以先分析单机版的Redisson如何实现一个分布式锁。

RedLock官方介绍:Distributed locks with Redis – Redis

由于 Redisson自身太过于复杂, 设计的 API 调用大多用 Netty 相关, 所以本文只对 如何加锁、如何实现重入锁,释放锁进行讨论

1-加锁流程

redisson加锁流程图.png

2-解锁流程

redisson解锁流程图.png

3.1 Redlock

3.1.1 Redlock 算法介绍

部署多台 Redis, 各实例之间相互独立, 不存在主从复制或者其他集群协调机制

使用方式大体如下:

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();

原理:

加入设置节点数N=5,所以我们需要在不同的计算机或虚拟机上运行5个Redis主站,以确保它们会以一种基本独立的方式失败。

为了获得锁,客户端执行以下操作。

  • 获取当前的时间,以毫秒为单位。

  • 依次在所有N个实例中获取锁,在所有实例中使用相同的键名和随机值。在步骤2中,当在每个实例中设置锁时,客户端使用一个与总的锁自动释放时间相比很小的超时来获取它。例如,如果自动释放时间是10秒,超时可以在~ 5-50毫秒范围内。这可以防止客户端在试图与Redis节点对话时长时间受阻:如果一个实例不可用,我们应该尽快尝试与下一个实例对话。

  • 客户端通过从当前时间减去步骤1中获得的时间戳,计算出获得锁所需的时间。如果并且只有当客户端能够在大多数实例(至少3个)中获取锁,并且获取锁的总时间小于锁的有效期,锁才被认为是被获取。

  • 如果锁被获取,其有效性时间被认为是初始有效性时间减去经过的时间,如步骤3中计算的那样。

  • 如果客户端由于某种原因未能获得锁(要么它无法锁定N/2+1个实例,要么有效性时间为负数),它将尝试解锁所有的实例(甚至是它认为无法锁定的实例)。

3.1.2 Redlock 算法是否安全

分布式系统研究员Martin Kleppmann曾对 RedLock算法深入分析并强烈反对在生产中使用,其主要原因就是redlock的实现依赖了服务器的本地时钟

如下例子,还是5个节点,Redlock失效:

  1. 客户端1获得了A、B、C节点上的锁,由于网络问题,无法到达D和E。

  2. 节点C上的时钟向前跳动,导致锁过期。

  3. 客户端2获得了节点C、D、E的锁,由于网络问题,A和B不能被联系到。

  4. 客户端1和2现在都认为他们持有锁。

也或者,在第二步骤,节点c如果出现宕机,恢复后没有之前的数据,客户端2也可能获取到锁。

再看如下例子:

  1. 客户端1请求锁定节点A、B、C、D、E。

  2. 当对客户端1的响应在路途中时,客户端1进入停止世界的GC。

  3. 所有Redis节点的锁都过期了。

  4. 客户端2获得了节点A、B、C、D、E的锁。

  5. 客户端1完成了GC,并收到了来自Redis节点的响应,表明它成功获得了锁(当进程暂停时,它们被保存在客户端1的内核网络缓冲区)。

  6. 客户端1和2现在都认为他们持有该锁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容