Redis分布式锁

前言

           锁独占一个资源,只能有一个在对这个资源的操作,为了避免多个端对同一资源操作导致的与预期不一样的问题,最近写了下redis锁,也看了比较多的博客,写的例子也不断变化,最终确定Redlock锁,这是Redis的作者antirez设计的锁,我以为是完全安全的,但看了诸多文章与讨论,得出结论是Redlock锁是相对安全的,而安全系数够用。

加锁

正确的代码

/**
     *  枷锁
     * @param lockName 锁key
     * @param randomVal uuid 确定解锁和枷锁是一个锁
     * @param lockSeconds 锁的超时时间
     * @return
     */
    public boolean lock(String lockName,String randomVal,int lockSeconds){
        Jedis jedis = null;
        //加锁
        try {
            jedis = pool.getResource();
            boolean locked = "OK".equals(jedis.set(lockName, randomVal, "NX", "EX", lockSeconds));
            return locked;
        } catch (JedisException e) {
            throw e;
        } finally {
            jedis.close();
        }
    }

命令介绍:SET key value [NX|XX] [EX|PX] seconds

        key  平常用rediskey,这里是锁定资源key,或者说锁的名字

        value  这里是一个唯一id,比如uuid,为了保证枷锁和解锁都是一个端,后面会说

        NX – 只有键key不存在的时候才会设置key的值

        XX – 只有键key存在的时候才会设置key的值

        EX seconds – 设置键key的过期时间,单位时秒

        PX milliseconds – 设置键key的过期时间,单位时毫秒

原子性操作

        这个枷锁的操作是原子性的,那如果不是原子性的会有什么问题呢,请看错误例子

错误例子1

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }

}

错误例子2

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {

    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);

    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }

    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
        
    // 其他情况,一律返回加锁失败
    return false;

}

错误:1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。

 

解锁

正确的代码

/**
     * 解锁
     * @param lockName    锁key
     * @param randomVal  uuid 确定解锁和枷锁是一个锁
     * @return
     */
    public boolean release(String lockName, String randomVal) {
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            String program = "if redis.call(\"get\",KEYS[1] == ARGV[1] then)"
                    + "return resis.call(\"del\",KEYS[1])"
                    + "else return 0"
                    + "end";
            Object rlt = jedis.eval(program, 1, lockName, randomVal);
            //解锁:判断当前的value是否还是原值,如果是原值则删除之
            return Long.valueOf(1) == rlt;
        } finally {
            jedis.close();
        }

    }

 Redis+Lua实现:

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

参数:

       program   lua语句

       keycount  这里填1

       lockName   唯一的key 

       randomVal   唯一id只能释放自己加的锁uuid

如果获取key的值和我们穿进去的uuid是一样,则删除,如果不一样返回0代表解锁失败,这样能确保枷锁和解锁的是同一个客户端,举个例子看图       

所以为了避免redis超时导致用户1释放用户2的锁,需要在加锁和释放的时候加一个唯一标识,确定用户1只能释放用户1的锁

 原子性操作

           当然释放锁使用 Redis+Lua 也是原子性操作的,释放的错误例子如下

错误例子1

/最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

错误例子2

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {      
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }
}

使用

这里简单封装写个使用的例子代码如下

/**
     * 分布式锁,警告:如果执行时间过长,大于超时时间后,会存在并行执行的情况
     *
     * @param redisKey    要lock的key
     * @param lockSeconds 超时时间
     * @param func        回调函数
     * @param t           回调参数
     * @param <T>         回调参数类型
     * @return 是否拿到锁并执行了回调函数
     */
    public <T> boolean distributedLock(String redisKey, int lockSeconds, Consumer<T> func, T t) {
        if (lockSeconds == 0)
            throw new RuntimeException("分布式锁过期时间必须大于0");
        if (StringUtils.isEmpty(redisKey))
            throw new RuntimeException("分布式锁必须设置key");

        String uuid = UUID.randomUUID().toString();
        //获取锁
        if (lock(redisKey,uuid,lockSeconds) == false) {
            return false;
        }
        //加锁成功继续执行回调函数
        try {
            func.accept(t);
        } catch (Exception ex) {
            System.out.println("RedisCacheFacade->Lock,执行方法体报错:" + ex);
        } finally {
            //解锁
            release(redisKey, uuid);
        }
        return true;
    }

后记

           其实安全都是相对的,虽然用户1释放不了用户2的锁,但是因为redis设置超时的关系,导致用户1和用户2同时执行被锁住的资源,也会导致一些问题,比如用户1查出来资源   然后锁的时间到了,用户2查出来资源,用户1更新资源,用户2更新资源就会导致用户1更新的资源失效,这里解决这个问题可以用数据库的乐观锁来解决,也就是为数据加版本,这样用户2执行保存的时候就会发现与自己查出来的版本不符,就可以事物回滚了。

神仙打架有兴趣一定要看看

https://www.jianshu.com/p/dd66bdd18a56?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

文章内容参考

https://www.cnblogs.com/linjiqin/p/8003838.html

https://www.jianshu.com/p/bc8dd3c0db22

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值