Redis分布式锁一网打尽

实现源码:

https://github.com/huangzhenshi/DistributeLearning/tree/master/DistributeLearning-master

参考

飞神RedLock
https://www.jianshu.com/p/7e47a4503b87

官网文档RedLock
https://redis.io/topics/distlock

概述

本文Redis的乐观锁实现、悲观锁实现、Redlock的原理、Redisson框架的实现。

乐观锁:基于redis的watch机制,先watch指标,再自增或者自减指标,再执行事务,最后尝试提交,提交如果失败,需要回滚事务。

但是秒杀场景下面的库存扣减和乐观锁的实现有点类似,但是并不是严格意义上的乐观锁实现,因为秒杀要解决的是防止超卖,库存的扣减和订单的消费是异步完成的,一般库存扣减成功则默认事件完成,不会去写失败回滚的逻辑,所以秒杀下的库存扣减并不是严格意义上的乐观锁实现。

悲观锁特点:并发度差、使用不好会不安全

  • 悲观锁是一个标准的锁的方式,有抢锁,抢锁成功后执行校验和业务操作,最后再释放锁的过程
  • 悲观锁抢锁成功时,必须要有timeout释放锁的动作,否则当前未释放锁线程出现异常,整个业务停摆,全部阻塞
  • 悲观锁抢锁和设置超时时间最佳是一起执行的,而不是先抢锁,再设置expire,因为如果程序在expire异常时,整个服务阻塞不可用
  • 悲观锁过程:抢锁成功占用锁,doubleCheck库存,扣减库存,修改抢锁标志位,释放锁,抢锁成功不一定抢单成功,整个过程加了锁,业务无法并发执行。

核心代码

乐观锁:如果修改成功,result不为空,则抢锁成功,快速感知结果

    transaction.set(key, String.valueOf(prdNum - 1));
    List<Object> result = transaction.exec();
    if (result == null || result.isEmpty()) {
            System.out.println("悲剧了,顾客:" + clientName + "没有抢到商品");// 可能是watch-key被外部修改,或者是数据操作被驳回
    } else {
            jedis.sadd(clientList, clientName);// 抢到商品记录一下
            System.out.println("好高兴,顾客:" + clientName + "抢到商品");
            break;
    }

悲观锁:while重试setnx抢锁,特点是一个key只有1个坑位, jedis.del(lockKey);释放锁

jedis.set(String key, String value, String nxxx, String expx, int time)

这个set()方法一共有五个形参:

  1. 第一个为key,我们使用key来当锁,因为key是唯一的
  2. 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成
  3. 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作
  4. 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定
  5. 第五个为time,与第四个参数相呼应,代表key的过期时间

乐观锁实现原理:

  • 自循环读取库存,库存为正数,就尝试扣减,扣减失败则下一个循环
public void run() {
        while (true) {
            System.out.println("顾客:" + clientName + "开始抢商品");
            jedis = RedisUtil.getInstance().getJedis();
            try {
                jedis.watch(key);
                int prdNum = Integer.parseInt(jedis.get(key));// 当前商品个数
                if (prdNum > 0) {
                    Transaction transaction = jedis.multi();
                    transaction.set(key, String.valueOf(prdNum - 1));
                    List<Object> result = transaction.exec();
                    if (result == null || result.isEmpty()) {
                        System.out.println("悲剧了,顾客:" + clientName + "没有抢到商品");// 可能是watch-key被外部修改,或者是数据操作被驳回
                    } else {
                        jedis.sadd(clientList, clientName);// 抢到商品记录一下
                        System.out.println("好高兴,顾客:" + clientName + "抢到商品");
                        System.out.println("account is: "+ prdNum);
                        break;
                    }
                } else {
                    System.out.println("悲剧了,库存为0,顾客:" + clientName + "没有抢到商品");
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                jedis.unwatch();
                RedisUtil.returnResource(jedis);
            }

        }
    }

悲观锁的实现

通过redis的set (key,value, px ,milliseconds, nx)方法,多个线程对同一个key操作,一次只有一个线程会成功,来实现锁操作,解锁通过jedis.del(lockKey)。

  • 类似于Zookeeper的实现原理,需要一个辅助类RedisBasedDistributedLock来实现 tryLock和lock、unlock等操作
  • 但是这里的逻辑是非公平锁的形式去抢锁,每个用户循环操作,先读取存量,如果大于0就有3s的时间去抢锁,抢锁失败然后进入下一个循环,这样设计防止库存为0了,其它未抢到锁的线程还不知道。
  • 抢锁成功的话,再校验一次存货,仍然大于0,就消费一个库存,添加消费者,最后释放锁,结束循环
  • 引入了锁超时机制,即使当前线程出现异常,未成功释放锁,也不会让当前业务永远停摆
  • 抢锁的过程也为在超时时限内循环抢锁
        //setnx尝试加锁
        if (jedis.setnx(lockKey, stringOfLockExpireTime) == 1) { // 获取到锁
            System.out.println("test5");
            //成功获取到锁, 设置相关标识
            locked = true;
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        //用完之后del 释放锁
        private void doUnlock() {
            jedis.del(lockKey);
        }

RedLock

https://www.jianshu.com/p/7e47a4503b87
https://redis.io/topics/distlock

  1. Redlock不是针对秒杀场景而研发的,redlock实现严格的分布式悲观锁。

因为在秒杀业务场景下,即使ClientA和ClientB同时拥有了锁,即使两个人都做扣减,大部分的情况下也不会出岔子,只有在库存只有1的时候,ClientA 和ClientB同时Double Check库存为1,同时进行扣减,最终会有一个Client会让库存为 -1的。其它业务场景下,比如库存充足时,即使有多个节点同时获取到锁,也不会超卖。实际上为了预防这种极端情况,可以添加第三次check即可,没必要在秒杀时引入Redlock。
而且秒杀的业务场景下,乐观锁的性能和安全性都是更好的。不太确认,乐观锁在集群环境下,会不会因为重新选举导致扣减的丢失。需要研究Redis的一致性算法。

  1. Redlock的代价是引入了新的jar包,额外引入至少2个MasterNode(2N+1)

  2. Redlock解决的问题:Redis集群环境下,主从数据的同步是异步的,Master宕机时如果锁信息未同步的话,slave成为new master时,锁会丢失(概率有点低)

  3. Redlock只是提高了分布式锁系统的容错性,但是如果过半节点都宕机的话,整个功能仍然不可用。

RedLock的三大特性:安全性、不会死锁(程序异常、集群网络分区)、具备一定的容错性

  1. Safety property: Mutual exclusion. At any given moment, only one client can hold a lock.
  2. Liveness property A: Deadlock free. Eventually it is always possible to acquire a lock, even if the client that locked a resource crashes or gets partitioned.
  3. Liveness property B: Fault tolerance. As long as the majority of Redis nodes are up, clients are able to acquire and release locks

极端情况,会导致ClientA 和ClientB同时拥有锁

  1. Client A acquires the lock in the master.
  2. The master crashes before the write to the key is transmitted to the slave.
  3. The slave gets promoted to master.
  4. Client B acquires the lock to the same resource A already holds a lock for. SAFETY VIOLATION!

Redlock算法:(3个Master节点情况,只有在规定时间里,超过2个节点加锁成功,才算成功争抢到锁)
引入的参数变量:锁过期时间、响应超时时间

  1. 先获取currentTime
  2. 挨个尝试获取锁(悲观锁),如果在响应时间内抢锁失败,则认定该节点抢锁失败,最终如果过半节点加锁成功,就是加锁成功
  3. 如果抢锁失败,则会在成功获得锁的Master上释放锁

该算法提升了系统的容错性,如果3个Master的环境,挂了一个Master,其它两个Master正常,那么Client也无法重复获取锁。
而且真正工作的Master可以是一个集群。例如:集群A是工作集群(Node1(masterA) Node2 Node3), Master B , MasterC,这种模式下,既可以保证工作集群的高可用,也可以保证不会出现极端情况下的双锁的问题。

Redisson

具体的使用细节,查看飞神的博客
https://www.jianshu.com/p/f302aa345ca8

直接封装了使用,有3种方法,都是悲观锁
第一种阻塞式等待获取锁,无限期循环,不建议使用
第二种带最长等待时间的获取锁,获取失败会抛出异常
第三种带最长等待时间的尝试获取锁,获取成功返回true,获取失败返回false

disLock.lock();
disLock.lock(1,TimeUnit.MILLISECONDS);
isLock = disLock.tryLock(0, 5000, TimeUnit.MILLISECONDS);

public RLock getLock(String lockId) {
   RLock rlock = null;
   try{
      rlock = redissonClient.getLock(lockId);
      rlock.lock(5000, TimeUnit.MILLISECONDS);
      System.out.println("获取锁成功");
   }catch (Exception e) {
      System.out.println("获取锁失败!");
      unLock(rlock);
   }

Redis官方封装了一套基于悲观锁策略的锁框架。
非常灵活:分别支持单Master、哨兵模式、Cluster模式

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

推荐阅读更多精彩内容