利用Redis实现分布式锁

为什么需要分布式锁?

在传统单体应用单机部署的情况下,可以使用Java并发相关的锁,如ReentrantLcok或synchronized进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统,渐渐的被部署在多机器多JVM上同时提供服务,这使得原单机部署情况下的并发控制锁策略失效了,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

分布式锁的实现条件

1、互斥性,和单体应用一样,要保证任意时刻,只能有一个客户端持有锁

2、可靠性,要保证系统的稳定性,不能产生死锁

3、一致性,要保证锁只能由加锁人解锁,不能产生A的加锁被B用户解锁的情况

4.在此我向大家推荐一个架构学习交流圈:609164807 帮助突破瓶颈 提升思维能力

Redis分布式锁的实现

Redis实现分布式锁不同的人可能有不同的实现逻辑,但是核心就是下面三个方法。

SETNX

SETNX key val

当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

Expire

expire key timeout

为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

Delete

delete key

删除key

获取锁

首先讲一个目前网上应用最多的一种实现

实现思路:

1.获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁以免产生死锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

2.获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

3.释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

public String getRedisLock(Jedis jedis, String lockKey, Long acquireTimeout, Long timeOut) {

        try {

            // 定义 redis 对应key 的value值(uuid) 作用 释放锁 随机生成value,根据项目情况修改

            String identifierValue = UUID.randomUUID().toString();

            // 定义在获取锁之后的超时时间

            int expireLock = (int) (timeOut / 1000);// 以秒为单位

            // 定义在获取锁之前的超时时间

            //使用循环机制 如果没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁

            // 使用循环方式重试的获取锁

            Long endTime = System.currentTimeMillis() + acquireTimeout;

            while (System.currentTimeMillis() < endTime) {

                // 获取锁

                // 使用setnx命令插入对应的redislockKey ,如果返回为1 成功获取锁

                if (jedis.setnx(lockKey, identifierValue) == 1) {

                    // 设置对应key的有效期

                    jedis.expire(lockKey, expireLock);

                    return identifierValue;

                }

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

        return null;

    }

这种实现方法也是目前应用最多的实现,我一直以为这确实是正确的。然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么还是会发生死锁的情况。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

当然这种情况Jedis的设计者也显然想到了,新版的Jedis可以同时set多个参数,具体实现如下:

实现思路:

基本上和原来的逻辑类似,只是将setnx和expire的操作合并为一步,改为使用新的set多参的方法。

set(final String key, final String value, final String nxxx, final String expx,final long time)

key和value自然不用多说。nxxx参数只可以传String 类型的NX(仅在不存在的情况下设置)和XX(和普通的set操作一样会做更新操作)两种。

expx是指到期时间单位,可传参数为EX (秒)和 PX (毫秒),time就是具体的过期时间了,单位为前面expx所指定的。

然后我们对上面的代码进行改造如下:

/**

    * @param acquireTimeout

    *            在获取锁之前的超时时间

    * @param timeOut

    *            在获取锁之后的超时时间

    */

    public String getRedisLock(Jedis jedis, String lockKey, Long acquireTimeout, Long timeOut) {

        try {

            // 定义 redis 对应key 的value值(uuid) 作用 释放锁 随机生成value,根据项目情况修改

            String identifierValue = UUID.randomUUID().toString();

            // 定义在获取锁之前的超时时间

            //使用循环机制 如果没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁

            // 使用循环方式重试的获取锁

            Long endTime = System.currentTimeMillis() + acquireTimeout;

            while (System.currentTimeMillis() < endTime) {

                // 获取锁

                // set使用NX参数的方式就等同于 setnx()方法,成功返回OK.PX以毫秒为单位

                if ("OK".equals(jedis.set(lockKey, lockKey, "NX", "PX", timeOut))) {

                    return identifierValue;

                }

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

        return null;

    }

 好了,获取锁的操作基本上就上面这些,有同学可能要问,为什么不直接返回一个Boolean型的true或false呢?

正如我前面所说的,要保证解锁的一致性,所以就需要通过value值来保证解锁人就是加锁人,而不能直接返回true或false了。

下面在说下解锁的过程。

释放锁

还是先举一个错误的例子:

实现思路:

释放锁的时候,通过传入key和加锁时返回的value值,判断传入的value是否和key从redis中取出的相等。相等则证明解锁人就是加锁人,执行delete释放锁的操作。

// 释放redis锁

    public void unRedisLock(Jedis jedis, String lockKey, String identifierValue) {

        try {

            // 如果该锁的id 等于identifierValue 是同一把锁情况才可以删除

            if (jedis.get(lockKey).equals(identifierValue)) {

                jedis.del(lockKey);

            }

        } catch (Exception e){

            e.printStackTrace();

        }

    }

看着好像没啥问题哈。然而仔细想想又总感觉哪里不对。

如果在执行jedis.del(lockKey)操作之前,刚好锁的过期时间到了,而这个时候又有别的客户端取到了锁,我们在此时执行删除操作,不是又不符合一致性的要求了吗。

然后我们修改为下述方案:

修改后的代码为:

public void unRedisLock(Jedis jedis, String lockKey, String identifierValue) {

        try {

            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

            Long result = (Long) jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(identifierValue));

            //0释放锁失败。1释放成功

            if (1 == result) {

                //如果你想返回删除成功还是失败,可以在这里返回

                System.out.println(result+"释放锁成功");

            }

            if (0 == result){

                System.out.println(result+"释放锁失败");

            }

        } catch (Exception e){

            e.printStackTrace();

        }

    }

实现思路:

我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为identifierValue。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与identifierValue相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。

那么为什么执行eval()方法可以确保原子性?源于Redis的特性,因为Redis是单线程,在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。


    在此我向大家推荐一个架构学习交流圈:609164807 帮助突破瓶颈 提升思维能力

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

推荐阅读更多精彩内容