【干货】分布式锁之redis设计

 

设计分布式锁时,先要关注它有哪些特点?

1、排它性(线程并发时,只有一个线程获取锁成功)

2、可重入(获取锁成功的线程,可多次进行获取)

还需要关注如下

1、功能点:获取锁、释放锁。

2、过期时间(保证程序异常后,锁也会正常释放)。

3、未获取到锁的线程,他们这时候在干嘛,应该怎么办?

4、有多个操作步骤时,如何保证它的原子性?(正常开发可能会想到加锁,可我们就是在设计锁,还没有锁呢?--lua脚本?它是原子的?如何保证原子的?感兴趣的小伙伴可以搜lua脚本在redis中使用,怎么就原子了?)

5、线程识别(线程的唯一标识--这里也可以多想想,本文用到的是redis自增和ThreadLocal来保证线程唯一且可获取到它)

下面贴一下获取锁的流程图:

释放的流程图小伙伴们自行脑补,大差不差,有兴趣的同学可以自己画一下。

关键代码:

   /**
     * 获得锁 lua脚本
     * 1个key,两个参数:key、线程标识、超时时间
     * 可作为样例
     * 步骤:(key和线程标识绑定)
     * 1、先获取key的flag(flag是当前线程的标识--通过thredLocal维护的标识--每个线程有一个标识--redis自增保证标识不会重复)
     *      2、如果不是字符串类型,且不等于第一个参数。获取锁失败(f第一次进来是nil,type(f)是boolean类型,所以可以往下执行)
     * 3、给key的flag存上参数1做标识
     * 4、给key设置过期时间参数2
     * 5、获取key的次数(可重入)
     *      6、如果次数不是字符串或小于0,给次数设置为1
     *      7、否则给次数加1
     * 8、返回1获取锁成功
     */
    public static String LOCK_SCRIPT =
            "local f = redis.call('HGET',KEYS[1],'flag');" +
            "if type(f) == 'string' and f ~= ARGV[1] then return 0;end " +
            "redis.call('HSET',KEYS[1],'flag',ARGV[1]);" +
            "redis.call('EXPIRE',KEYS[1],ARGV[2]);" +
            "local c = redis.call('HGET',KEYS[1],'count');" +
                    "if type(c) ~= 'string' or tonumber(c) < 0 then redis.call('HSET',KEYS[1],'count',1);" +
                    "else redis.call('HSET',KEYS[1],'count',c+1);end " +
                    "return 1";
    /**
     * 释放锁 lua脚本
     * 两个参数:key、线程标识
     * 1、获取key锁被哪个线程占用,取它的标识flag
     * 2、如果类型不是string,或者是string但不是当前线程的标识,返回0--不用释放
     *      3、进入第3步,说明是当前线程占用的锁,取它的占用次数
     *      4、如果次数为空或者次数小于2--代表只占用了一次,直接删除key(这个key是传过来的key加了特殊后缀,当锁的key),返回1
     *      5、否则的话代表重入了多次,次数减1,返回2
     *
     *
     */
    public static String UNLOCK_SCRIPT =
            "local f = redis.call('HGET',KEYS[1],'flag');" +
            "if type(f) ~= 'string' or (type(f) == 'string' and f ~= ARGV[1]) then return 0;end " +
                    "local c = redis.call('HGET',KEYS[1],'count');" +
                    "if type(c) ~= 'string' or tonumber(c) < 2 then redis.call('DEL',KEYS[1]);return 1;" +
                    "else redis.call('HSET',KEYS[1],'count',c-1);return 2;end";

    /**
     * 获得锁
     * 1、如果锁获取成功了,还可以继续获取,可重入
     * 2、如果没成功,那么就从锁key的list队列执行blpop操作,如果list有数据(当占用锁的线程释放锁成功时,会往list中加入ok),弹出,夺取资源成功,重新去获得锁
     *
     *
     * @param jedis        redis连接
     * @param expireSecond 持有锁超时秒数
     * @param waitSecond   等待锁超时秒数
     * @param flag         线程标识
     * @return
     */
    private boolean tryLockInner(Jedis jedis, int expireSecond, int waitSecond, String flag) {
        // 尝试获得锁 如果自身持有锁则可以再次获得
        if ((Long) jedis.eval(LOCK_SCRIPT, 1, redisLockKey, flag, "" + expireSecond) > 0) {
            return true;
        }
        //阻塞等待释放锁通知
        List<String> lp = jedis.blpop(waitSecond, redisListKey);
        System.err.println("flag:"+flag+",blpop,得到了list弹出的ok,重新获取锁");
        if (lp == null || lp.size() < 1) {
            //如果超时则返回锁定失败
            return false;
        }
        return tryLockInner(jedis, expireSecond, waitSecond, flag);
    }

    /**
     * 释放锁
     * 1、可重入,需要占用的次数减为1时,对应的程序返回1则释放成功,返回2代表次数减1
     * 2、释放成功后,往Key的list队列中放入Ok,此时有多个线程开启了blpop操作,会争夺资源,谁弹出成功代表夺取资源成功,可重新去获得锁
     *
     * @param flag 线程标识
     * @return
     */
    public boolean tryUnlock(String flag) {
        Jedis jedis = jedisPool.getResource();
        try {
            //删除锁定的key
            Long l = (Long) jedis.eval(UNLOCK_SCRIPT, 1, redisLockKey, flag);
            if (l < 1) {
                return false;
            }
            // 因为是可重入锁 所以释放成功不一定会释放锁
            if (l.intValue() == 2) {
                return true;
            }
            //如果锁释放消息队列里没有值 则释放一个信号
            if (l.intValue() == 1 && jedis.llen(redisListKey).intValue() == 0) {
                //通知等待的线程可以继续获得锁
                System.err.println("flag:"+flag+",放的ok");;
                jedis.rpush(redisListKey, "ok");
            }
            return true;
        } finally {
            jedis.close();
        }
    }

 原理:使用redis的lua脚本实现 lua脚本可保证原子性,使用BLPOP实现释放锁时的通知,可防止等待线程自旋浪费cpu资源。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值