Redis分布式锁的实现

分布式锁

之前看程序员小灰的公众号,通过漫画的形式讲解了分布式锁的内容。

后来想到公司的项目里,也利用到了分布式锁,但是分布式锁的具体代码实现和在项目中的应用并不是自己写的,具体情况还不是太懂。寻思,决定利用这个热度来看看公司的分布式锁实现,向大牛们学习学习。

说到锁,在生活中锁通常是用来锁门的,锁车的。在java中,锁是用来同步的,锁是用来保证数据的一致性的。两者虽不同,但最终都是以安全为结果来考虑的。

分布式锁和我们java基础中学习到的synchronized略有不同,在synchronized中我们的锁是个对象,如果一个线程拿到了该锁,别的线程就只能等待了。

分布式中的锁,通常不会是一个对象,而是一个唯一的数据,可以是商户的订单号,商户的编码,或者是一条数据的唯一主键等等。

synchronized主要对于单个应用中,多线程的同步;

而分布式锁对应的是多个应用,每个应用中都可能会处理相同的数据,所以需要对对个应用的数据进行同步,保证数据的一致性;

抛开具体代码实现,我认为两种锁的最大不同。

分布式锁的具体实现

1.redis

向redis中添加一个key,添加的操作是原子性操作,key不存在才能添加成功;

2.zookeeper

具体实现,还没了解过,等有了解在做详细解答;

下面我们就来看看redis怎么实现分布式锁;

redis实现分布式锁

说的简单些,redis来实现分布式锁的原理就是将程序中一个唯一的key写入redis中,当有其他分布式应用要访问时候此key时,就去redis中读取,读取到了则说明此数据正在被处理,读取不到则说明可以进行处理;

但是,想将分布式锁处理的妥当,还真不是一件轻松地事情,继续往后看。

在redis实现的分布式锁中,我们需要强调以下几点,只有保证了以下几点,才可说是确保了锁的实现:

互斥,在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;

不能发生死锁,一台服务器挂了,程序没有执行完,但是redis中的锁却永久存在了,那么已加锁未执行完的数据,就永远得不到处理了,直到人工发现,或者监控发现;

高可用性,可以保证程序的正常加锁,正常解锁;

加锁解锁必须由同一台服务器进行,不能出现你加的锁,别人给你解锁了。

再开始具体代码之前,我们需要来创建测试环境;首先是,在你的电脑中安装redis,具体安装如下:

下载,解压,编译:

$ wget http://download.redis.io/releases/redis-4.0.10.tar.gz
$ tar xzf redis-4.0.10.tar.gz
$ cd redis-4.0.10
$ make

编译成功后,进入到redis-4.0.10目录中,再进入到src目录下:

执行./redis-server命令,redis程序启动;

新启动一个新的窗口,进入到redis-4.0.10/src目录下执行,./redis-cli命令进入redis客户端;

创建项目工程,添加java端redis依赖:

最新的2.9.0版本:

 <dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>2.9.0</version>
</dependency>

基本的环境创建完了,接下来我们就来编写具体的代码。

前面说了,redis分布式锁实现就是像redis服务器中插入一个可做唯一条件的的key。那么,我们来看看redis中Java的api;

我们使用的是redis在Java中的jedis框架。

在Jedis老版本中,多数实现使用的是setnx()方法和expire()方法实现,而在jedis最新版本中使用的是set()方法;

例如:

public boolean setLock(Jedis jedis,String key,String val,int expireTime){
    if (jedis.setnx(key, val) == 1) {
        jedis.expire(key, expireTime / 1000);
        return true;
    }
    return false;
}

上面的例子中,你觉得会发生什么样的问题?如果当程序执行完成jedis.setnx后,key和val被设置到了redis中,此时应用程序异常,过期时间还未设置,那么此key将永久保留在redis中,不会被删除。之所以这么实现,是因为当时jedis框架并不支持多参数的setnx()方法,setnx指令本身不支持传入超时时间。

public boolean setLock(Jedis jedis,String key,String val,int expireTime){
    String response = jedis.set(key, val, "NX", "PX",
            expireTime);
    return "OK".equals(response);
}

此方法为现在通用的实现;当key不存在时,向redis中插入数据,设置过期时间,这就相当于上锁了;这里面需要注意的一点就是,val的值。我们将唯一的值当做key,那么val怎么搞?

在分布式系统环境下,val的值可以设置成该机器的唯一标识,例如时间+请求号。为什么这么说,当一个服务器向redis加锁时候,我们需要确定这个key是来自于哪台服务器,在解锁时需要校验是不是解锁的请求来自于同一个服务器;

set(final String key, final String value, final String nxxx,final String expx, final int time) 方法:

(1)key,我们使用key来当锁,key是唯一的。

(2)value,我们传的是“时间+请求号”,通过给value赋值我们在解锁的时候就会传递同样的数据进行解锁。不至于出现不同的服务器对key进行解锁。为什么说,不允许出现不同的服务器对一个key进行解锁?我们后面讲解。

(3)nxxx,NX意思为SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

(4)expx,PX意思是给这个key加一个过期设置,具体时间由第五个参数决定。

(5)time,代表key的过期时间,单位毫秒。

说完了上锁,接下来说说解锁:

解锁,就是将key删除,你可能会觉得调用jedis删除方法就行了呗,事实并不是如此;

public void deleteLock(Jedis jedis, String key){
    jedis.del(key);
}

我们前面说了,在分布式环境中,哪台服务器加的锁,在解锁时候,还让那台服务器来解锁。不能出现A服务器加锁,而B服务器解锁的情况;而上面的代码就会出现这种情况。

当A服务器将一个key设置超时时间为5秒钟,获取到锁执行业务逻辑,但是呢,5秒钟没有执行完,此时key由于到了过期时间而被删除了。正好B服务器进行了获取锁操作,发现key没有上锁,进而加锁开始执行业务逻辑。过了1秒后,A服务器执行完毕,执行释放所操作,del(key),将B服务器上的锁给删除了。A、B服务器对同一个可以执行了一样的操作;

实现如下:

public void deleteLock(Jedis jedis, String key, String value){
    if (value.equals(jedis.get(key))) {
        jedis.del(lockKey);
    }
}

上面实现,你觉得有问题吗?会不会出现A服务器加锁,而B服务器解锁的情况。

答案:是。

由于判断和del()操作不是原子性的,那么就会存在判断后,让其他服务器删除的情况;

例如:A服务器加锁,执行业务逻辑,很快执行完毕,进行解锁操作,解锁判断,OK,准备进行del()操作,此时CPU切换到执行别的操作了,或者JVM虚拟机进行垃圾回收操作。这时候,key到了过期时间,B服务器执行获取到锁,执行业务逻辑,还没执行完成,A服务器复活,执行del()操作,删除key;此时,A服务器上的锁,超时而被删除,B服务器加锁,A服务器将其删除;

终极大招,lua脚本实现:

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

jedis.eval(luaScript,Arrays.asList(key),Arrays.asList(value));

总体代码结构为:

 public void redisLock(Jedis jedis,String key,String val,int expireTime){
    String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    try{
        String response = jedis.set(key, val, "NX", "PX", expireTime);
        if(!"OK".equals(response)){
            return;
        }
        //.....执行业务逻辑
    }catch (Exception e){

    }finally {
        jedis.eval(luaScript,Arrays.asList(key),Arrays.asList(val));
    }
}

通过lua脚本,解决了 解铃还须系铃人 的问题,但是并没有解决由于A服务器执行时间过长,导致锁失效,从而使得B服务器获取到了锁,对同一个key执行了相同的逻辑。

笔者想到了两种方式。首先,需要确认的是,以上的情况发生概率很低,如果你的系统并发量不大,业务逻辑不复杂的话,基本上很难遇到这个误删除的问题,或者A、B服务器都对同一个key执行业务逻辑的问题。

第一个解决办法,我给他起名叫“懒政”,意思是重复执行就重复执行吧,不影响数据的一致性就行。但是,此解决办法有个前提条件,不影响数据的最终一致性。比如说,在加锁的业务逻辑中有一个远程调用的接口,此接口不是幂等性的,你调用几次,此接口就接受几次你的数据,那么这种情况下就不能使用该方法。还有就是说,在业务逻辑中有一个update数据库更新操作,sql为 update set amount = amount - 10 where id = 1 and amount >=0,很常见的金额更新操作,但是如果对id为1的数据连续执行2次,那金额就不对了,这个是个大问题,这种情况也不行。

第二个解决办法是,守护线程,当A服务器设置的锁要超时的时候,守护线程再对该锁进行续命,加血,延长存活时间。

守护线程例子:

public class ThreadTest implements Runnable{
    @Override
    public void run() {
        for (;;){
            System.out.println("111111111");
        }
    }
    public static void main(String[] agrs){
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        //thread.setDaemon(true);
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

当设置setDaemon为true时,当main方法执行结束后,新启动的线程也随之结束,这就是守护线程,守护这main方法主线程,随着main结束而结束;

具体实现如下:

public void redisLockAndDaemonThread(final Jedis jedis, String key, String val, int expireTime){
    String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    try{
        String response = jedis.set(key, val, "NX", "PX", expireTime);
        if(!"OK".equals(response)){
            return;
        }
        //开启守护线程:
        final int tmpExpireTime = expireTime;
        final String tmpKey = key;
        Thread thread = new Thread(new Runnable(){
            @Override
            public void run() {
                for(;;){
                    jedis.expire(tmpKey,tmpExpireTime);
                    try {
                        Thread.sleep(tmpExpireTime);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        //.....执行业务逻辑
    }catch (Exception e){

    }finally {
        jedis.eval(luaScript,Arrays.asList(key),Arrays.asList(val));
    }
}

与之前的一样,首先我们来对key进行加锁,设置超时间。获取到锁后,开启守护线程,再对key进行超时间设置,增加其寿命,接下来进行睡眠,如果在睡眠后还能继续执行,则说明此业务逻辑执行还未结束,再次对key进行寿命延长。

如果业务线程结束,那么守护线程也随之结束。

至此,如上便是redis实现分布式锁的逻辑。

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

推荐阅读更多精彩内容

  • 引题 比如在同一个节点上,两个线程并发的操作A的账户,都是取钱,如果不加锁,A的账户可能会出现负数,正确的方式是对...
    阿康8182阅读 4,734评论 0 75
  • 以下翻译自redis官网. 基于redis的分布式锁 当多个进程需要以互斥的方式来操作共享数据的时候,分布式锁在这...
    有惑阅读 822评论 0 2
  • Java 并发包里面以及你给提供了很多方便的工具来帮助我们充分发挥单个节点上面的多线程并发处理能力,但是随着并发量...
    ggr阅读 704评论 1 1
  • 目前实现分布式锁的方式主要有数据库、Redis和Zookeeper三种,本文主要阐述利用Redis的相关命令来实现...
    Aldeo阅读 1,985评论 0 6
  • 除夕夜的22:18,抢支付宝红包的朋友们应该都不陌生,话说,大家抢了多少呢? 我是1.68,个人觉得运气还不错。 ...
    苏诉阅读 262评论 5 3