一对一源码,基于Redis实现分布式锁的方式

一对一源码,基于Redis实现分布式锁的方式

方案1:setnx 方案(不建议使用)

redis 提供 setnx 命令,是「SET if Not eXists」的缩写,只有不存在时才会设置返回1,否则返回0,如下:

127.0.0.1:6379> setnx javabk.cn 1
(integer) 1
127.0.0.1:6379> setnx javabk.cn 1
(integer) 0

 

实际设置成功时,表示获取到锁,一般会马上通过 expire 设置过期时间,避免处理一对一源码业务时没有及时删除导致后面的请求都获取不到锁,具体例子如下:

public class DistributedLockDemoTest {

    Jedis jedis = new Jedis("127.0.0.1",6379);

    @Test
    public void setnxAndExpire() {
        String myId = UUID.randomUUID().toString();
        String key = "javabk.cn";
        //1. 通过 setnx 抢锁
        long result = jedis.setnx(key, myId);
        //2. 结果判断
        if (result == 1) {//成功获取锁
            try {
                //3. 设置过期,避免死锁
                jedis.expire(key, 30);//30 seconds expired
                //4. 业务处理....
            } finally {
                //5. 释放锁(这里可以优化成:将判断+删除写成lua脚本进行删除)
                if (myId.equals(jedis.get(key))) {//判断value是自身设置才删除
                    jedis.del(key);
                }
            }
        } else {  //获取锁失败
            // ....
        }
    }
}

 

该方案存在的问题

1、存在死锁的可能
2、锁在持有期间过期

方案2:set扩展命令

针对方案1存在的问题,在redis版本 >=2.8 ,针对set 命令进行扩展来解决这个setnx + expire 的原子性问题。命令如下:

SET key value [EX seconds] [PX milliseconds] [NX|XX]

 

其中 EX 表示秒,PX 表示毫秒,NX 表示不存在才设置,XX 表示存在才设置。将命令其实就是将 setnx + expire 2个命令合并成1个,保证了原子性。命令例子如下:

127.0.0.1:6379> set javabk.cn 1 ex 30 nx //不存在时设置成功返回OK
OK
127.0.0.1:6379> ttl javabk.cn
(integer) 24
127.0.0.1:6379> set javabk.cn 1 ex 30 nx //存在时,设置不成功,返回空
(nil)

 

代码例子:

public void setExtendCommand() {
        String myId = UUID.randomUUID().toString();
        String key = "javabk.cn";
        //1. 通过 setnx 抢锁
        String result = jedis.set(key, myId, SetParams.setParams().ex(30).nx());
        System.out.println("result is:" + result);
        //2. 结果判断
        if ("OK".equals(result)) {//成功获取锁
            try {
                //3. 业务处理....
            } finally {
                //4.释放锁
                unLockAfterCompareWithLua(key, myId);
            }
        } else {  //获取锁失败
            // ....
        }
}

public boolean unLockAfterCompareWithLua(String key, String value) {
    String luaSrcipt = "if redis.call('get',KEYS[1]) == ARGV[1] then\n" +
        "redis.call('del',KEYS[1])\n" +
        "return 1 \n" +
        "else\n" +
        "return 0\n" +
        "end";

    List<String> placeHolderKeys = Lists.newArrayList(key);
    List<String> placeHolderValues = Lists.newArrayList(value);
    Object result = jedis.eval(luaSrcipt, placeHolderKeys, placeHolderValues);
    if (result != null  && "1".equals(result.toString())) {
        return true;
    }
    return false;
}

 

方案3:通过lua脚本打包 setnx + expire 命令

redis可以通过lua脚本打包多个命令进行执行,保证其执行原子性,可以解决 setnx + expire 原子性执行问题。其实多个命令执行的原子性问题都可以通过将其打包成lua脚本来保证原子性执行。

代码例子:

public void setNxAndExpireWithLua() {
        String myId = UUID.randomUUID().toString();
        String key = "javabk.cn";
        //放到服务端执行的lua脚本
        String luaSrcipt = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then\n" +
                                "redis.call('expire',KEYS[1],ARGV[2])\n" +
                                "return 1 \n" +
                            "else\n" +
                                "return 0\n" +
                            "end";

        //1. 通过 setnx 抢锁
        List<String> placeHolderKeys = Lists.newArrayList(key);
        List<String> placeHolderValues = Lists.newArrayList(myId, "30");
        Object result = jedis.eval(luaSrcipt, placeHolderKeys, placeHolderValues);//核心改动
        System.out.println("result is:" + result);
        //2. 结果判断
        if ("1".equals(result)) {//成功获取锁(跟lua脚本的返回1对应)
            try {
                //3. 业务处理....
            } finally {
                //4. 释放锁
                unLockAfterCompareWithLua(key, myId);
            }
        } else {  //获取锁失败
            // ....
        }
}

public boolean unLockAfterCompareWithLua(String key, String value) {
    String luaSrcipt = "if redis.call('get',KEYS[1]) == ARGV[1] then\n" +
        "redis.call('del',KEYS[1])\n" +
        "return 1 \n" +
        "else\n" +
        "return 0\n" +
        "end";

    List<String> placeHolderKeys = Lists.newArrayList(key);
    List<String> placeHolderValues = Lists.newArrayList(value);
    Object result = jedis.eval(luaSrcipt, placeHolderKeys, placeHolderValues);
    if (result != null  && "1".equals(result.toString())) {
        return true;
    }
    return false;
}

 

其实通过方案2和方案3,可以解决大部分业务场景,如果有些业务场景需要锁的可重入性,那么可以参考[可重入性]

方案4 基于redisson(推荐使用)

redisson 是基于一个 redis java client,底层实现做了很多封装,比如分布式锁、读写锁等等,具体请看 官网

核心代码:

public class RedisLockWithRedisson {

    RedissonClient redissonClient = null;

    public RedisLockWithRedisson(String host, int port) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        config.setLockWatchdogTimeout(10 * 1000);//10s. 覆盖 watch log 默认30s 超时的配置
        redissonClient = Redisson.create(config);
    }

    public boolean lockWithWatchDog(String key,  int waitSecond) throws Exception {

        RLock lock = redissonClient.getLock(key);
        //尝试加锁,第一个参数表示最多等待多少秒。启动 watch log 续期
        boolean locked = lock.tryLock(waitSecond, TimeUnit.SECONDS);
        if (locked) {
            System.out.println("成功获取锁.key:" + key);
            System.out.println("业务处理...");
            //业务处理
            for (int i = 0; i < 10; i++) {
                System.out.println("业务处理中,剩余时间:" + lock.remainTimeToLive());
                Thread.sleep(1500);
            }
            System.out.println("处理业务完成后,锁是否存在:" + lock.isLocked());
            lock.unlock();
            System.out.println("解锁后,锁是否存在:" + lock.isLocked());
            return true;
        } else {
            System.out.println("获取锁失败在等待: " + waitSecond+ "秒,key:" + key);
            return false;
        }
    }
}

 

说明:redisson 对比上面几个方案,其实实现是类似的,只不过做了大量的封装,使用非常简单,而且内部增加了 watch dog 续期机制。我们看看上面的最核心代码:lock.tryLock(waitSecond, TimeUnit.SECONDS) ,其底层实现就是一个lua脚本,如下:

if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('hincrby', KEYS[1], ARGV[2], 1); 
redis.call('pexpire', KEYS[1], ARGV[1]); 
return nil; 
end; 
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
 redis.call('hincrby', KEYS[1], ARGV[2], 1); 
 redis.call('pexpire', KEYS[1], ARGV[1]); 
 return nil; 
end; 
return redis.call('pttl', KEYS[1]);

 

通过脚本可发现,其通过 hash结构来支持锁的可重入性,hash key 是给每个线程(客户端)分配的唯一ID,hash value 是同个线程重复成功加锁的次数。加锁成功后,开启一个定时任务每隔一段时间继续续期,默认是过期时间是30秒,每隔 1/3 超时时间进行1次续期,上面例子覆盖默认超时时间,改成10秒。例子中特意实现业务处理时间超过过期时间,但是由于续期机制,保证处理期间不过期。

测试代码:

@Test
    public void testLockWithWhatDogAndWait() throws Exception {
        String key = "javabk.cn";
        for (int i = 0; i < 2; i++) {
            Runnable runnable = () -> {
                try {
                    lockWithRedisson.lockWithWatchDog(key, 10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
            executor.submit(runnable);
        }
        Thread.currentThread().join();
    }

 

测试结果如下:由于设置watch dog 超时时间10秒,所以 3秒进行1次续期(1/3 * 10),所以从6970 ttl 变成 8834 主要是由于续期带来的

成功获取锁.key:javabk.cn
业务处理...
业务处理中,剩余时间:9993
业务处理中,剩余时间:8480
业务处理中,剩余时间:6970
业务处理中,剩余时间:8834
业务处理中,剩余时间:7323
业务处理中,剩余时间:9212
业务处理中,剩余时间:7701
获取锁失败在等待: 10秒,key:javabk.cn
业务处理中,剩余时间:9588
业务处理中,剩余时间:8082
业务处理中,剩余时间:9971
处理业务完成后,锁是否存在:true
解锁后,锁是否存在:false

 

以上就是一对一源码,基于Redis实现分布式锁的方式, 更多内容欢迎关注之后的文章

 

posted @ 2024-05-11 09:26  云豹科技-苏凌霄  阅读(111)  评论(0编辑  收藏  举报