Redis实现分布式锁

什么是分布式锁

在学习Java多线程编程的时候,锁是一个很重要也很基础的概念,锁可以看成是多线程情况下访问共享资源的一种线程同步机制。这是对于单进程应用而言的,即所有线程都在同一个JVM进程里的时候,使用Java语言提供的锁机制可以起到对共享资源进行同步的作用。如果分布式环境下多个不同线程需要对共享资源进行同步,那么用Java的锁机制就无法实现了,这个时候就必须借助分布式锁来解决分布式环境下共享资源的同步问题。分布式锁有很多种解决方案,今天我们要讲的是怎么使用缓存数据库Redis来实现分布式锁。

Redis分布式锁方案一

使用Redis实现分布式锁最简单的方案是在获取锁之前先查询一下以该锁为key对应的value存不存在,如果存在,则说明该锁被其他客户端获取了,否则的话就尝试获取锁,获取锁的方法很简单,只要以该锁为key,设置一个随机的值就行了。比如,我们有一批任务需要由多个分布式线程处理,每个任务都有一个taskId,为了保证每个任务只被执行一次,在工作线程执行任务之前,先获取该任务的锁,锁的key可以为taskId。因此,获取锁的过程可以用如下伪代码实现:

a.jpeg

上述就是最简单的获取锁的方案了,但是大家可以想想这个方案有什么问题呢?有没有什么潜在的坑?在分析这种方案的优缺点之前,先说一下获取锁后我们一般是怎么使用锁,并且又是如何释放锁的,以Java语言为例,我们一般获取锁后会将释放锁的代码放在finally块中,这样做的好处是即使在使用锁的过程中出现异常,也能顺利将锁释放掉。用伪代码描述如下:

b.jpeg

其中,getLock方法的伪代码上文已经给出,releaseLock方法是释放锁的方法,在该方案中,只是简单地删除掉key,就不给出伪代码了。

上述使用锁的代码咋一看是没有什么问题的,学过Java的人都知道,在try...finally...代码块中,即使try代码块中抛出异常,最终也会执行finally代码块,然而这样就能保证锁一定会被释放吗?考虑这样一种情况:代码执行到doSomething()方法的时候,服务器宕机了,这个时候finally代码块就没法被执行了,因此在这种情况下,该锁不会被正常释放,在上述案例中,可能会导致任务漏算。因此,这种方案的第一个问题是会出现锁无法正常释放的风险,解决这个问题的方法也很简单,Redis设置key的时候可以指定一个过期时间,只要获取锁的时候设置一个合理的过期时间,那么即使服务器宕机了,也能保证锁被正确释放。

该方案的另外一个问题是,获取到的锁不一定是排他锁,也就是说同一把锁同一时间可能被不同客户端获取到。仔细分析一下getLock方法,该方法并不是原子性的,当一个客户端检查到某个锁不存在,并在执行setKey方法之前,别的客户端可能也会检查到该锁不存在,并也会执行setKey方法,这样一来,同一把锁就有可能被不同的客户端获取到了。

既然这种方案有以上缺点,那么该如何改进呢?且听我慢慢道来。

Redis分布式锁方案二

上一小节的方案有2个缺点,一个是获取的锁可能无法释放,另一个是同一把锁在同一时间可能被不同线程获取到。通过查看Redis文档,可以找到Redis提供了一个只有在某个key不存在的情况下才会设置key的值的原子命令,该命令也能设置key值过期时间,因此使用该命令,不存在上述方案出现的问题,该命令为:

SET my_key my_value NX PX milliseconds

其中,NX表示只有当键key不存在的时候才会设置key的值,PX表示设置键key的过期时间,单位是毫秒。

如此一来,获取锁的过程可以用如下伪代码描述:

c.jpeg

其中,setKeyOnlyIfNotExists方法表示的是原子命令SET my_key my_value NX PX milliseconds。

如此一来,获取锁的代码应该就没什么问题了,但是这种方案还是会有其他问题。大家再仔细研究下释放锁的代码。因为现在我们设置key的时候也设置了过期时间,所以原来的释放锁的代码现在看来就有问题了。考虑这样一种情况:客户端A获取锁的时候设置了key的过期时间为2秒,然后客户端A在获取到锁之后,业务逻辑方法doSomething执行了3秒(大于2秒),当执行完业务逻辑方法的时候,客户端A获取的锁已经被Redis过期机制自动释放了,因此客户端A在获取锁经过2秒之后,该锁可能已经被其他客户端获取到了。当客户端A执行完doSomething方法之后接下来就是执行releaseLock方法释放锁了,由于前面说了,该锁可能已经被其他客户端获取到了,因此这个时候释放锁就有可能释放的是其他客户端获取到的锁。

Redis分布式锁方案三

既然方案二可能会出现释放了别的客户端申请的锁的问题,那么该如何进行改进呢?有一个很简单的方法是,我们设置key的时候,将value设置为一个随机值r,当释放锁,也就是删除key的时候,不是直接删除,而是先判断该key对应的value是否等于先前设置的随机值,只有当两者相等的时候才删除该key,由于每个客户端产生的随机值是不一样的,这样一来就不会误释放别的客户端申请的锁了。新的释放锁的方案用伪代码描述如下:

d.jpeg

其中,getKey方法就是Redis的查询key值的方法,deleteKey就是Redis的删除key值的方法,在此不给出伪代码了。

那么这种方案就没有问题了吗?很遗憾地说,这种方案也是有问题的。原因在于上述释放锁的操作不是原子性的,不是原子性操作意味着当一个客户端执行完getKey方法并在执行deleteKey方法之前,也就是在这2个方法执行之间,其他客户端是可以执行其他命令的。考虑这样一种情况,在客户端A执行完getKey方法,并且该key对应的值也等于先前的随机值的时候,接下来客户端A将会执行deleteKey方法。假设由于网络或其他原因,客户端A执行getKey方法之后过了1秒钟才执行deleteKey方法,那么在这1秒钟里,该key有可能也会因为过期而被Redis清除了,这样一来另一个客户端,姑且称之为客户端B,就有可能在这期间获取到锁,然后接下来客户端A就执行到deleteKey方法了,如此一来就又出现误释放别的客户端申请的锁的问题了。

Redis分布式锁方案四

既然方案三的问题是因为释放锁的方法不是原子操作导致的,那么我们只要保证释放锁的代码是原子性的就能解决该问题了。很遗憾的是,查阅Redis开发文档,并没有发现相关的原子操作。不过幸运的是,在Redis中执行原子操作不止有通过官方提供的命令的方式,还有另外一种方式,就是Lua脚本。因此,方案三中的释放锁的代码可以用以下Lua脚本来实现:

e.jpeg

其中ARGV[1]表示设置key时指定的随机值。

由于Lua脚本的原子性,在Redis执行该脚本的过程中,其他客户端的命令都需要等待该Lua脚本执行完才能执行,所以不会出现方案三所说的问题。至此,使用Redis实现分布式锁的方案就相对完善了。

总结

上述分布式锁的实现方案中,都是针对单节点Redis而言的,然而在生产环境中,我们使用的通常是Redis集群,并且每个主节点还会有从节点。由于Redis的主从复制是异步的,因此上述方案在Redis集群的环境下也是有问题的。关于在Redis集群中如何优雅地实现分布式锁,后续再写文章详述。

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