T31-DAY25(Redisson分布式锁)

29 篇文章 1 订阅

作为 Java 开发人员,我们若想在程序中集成 Redis,必须使用 Redis 的第三方库。目前大家使用的最多的第三方库是jedis。

和SpringCloud gateway一样,Redisson也是基于Netty实现的,是更高性能的第三方库。 所以,这里推荐大家使用Redission替代 jedis。

Redisson简介

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。
在这里插入图片描述

Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
基本用法

@Slf4j
@SpringBootTest
public class RedissionTest {

    @Resource
    RedissonClient client;

    @Test
    public void testRBucketExamples() {
        // RList 继承了 java.util.List 接口
        RBucket<String> rstring = client.getBucket("redission:test:bucket:string");
        rstring.set("this is a string");

        RBucket<UserDTO> ruser = client.getBucket("redission:test:bucket:user");
        UserDTO dto = new UserDTO();
        dto.setToken(UUID.randomUUID().toString());
        ruser.set(dto);
        System.out.println("string is: " + rstring.get());
        System.out.println("dto is: " + ruser.get());

        client.shutdown();
    }


    @Test
    public void testListExamples() {
        // 默认连接上 127.0.0.1:6379
        // RList 继承了 java.util.List 接口
        RList<String> nameList = client.getList("redission:test:nameList");
        nameList.clear();
        nameList.add("张三");
        nameList.add("李四");
        nameList.add("王五");
        nameList.remove(-1);


        System.out.println("List size: " + nameList.size());


        boolean contains = nameList.contains("李四");
        System.out.println("Is list contains name '李四': " + contains);

        nameList.forEach(System.out::println);


        client.shutdown();
    }


    @Test
    public void testMapExamples() {
        // 默认连接上 127.0.0.1:6379
        // RMap 继承了 java.util.concurrent.ConcurrentMap 接口
        RMap<String, Object> map = client.getMap("redission:test:personalMap");
        map.put("name", "张三");
        map.put("address", "北京");
        map.put("age", new Integer(50));

        System.out.println("Map size: " + map.size());

        boolean contains = map.containsKey("age");
        System.out.println("Is map contains key 'age': " + contains);
        String value = String.valueOf(map.get("name"));
        System.out.println("Value mapped by key 'name': " + value);

        client.shutdown();
    }

    @Test
    public void testLuaExamples() {
        // 默认连接上 127.0.0.1:6379

        client.getBucket("redission:test:foo").set("bar");
        String r = client.getScript().eval(RScript.Mode.READ_ONLY,
                "return redis.call('get', 'redission:test:foo')", RScript.ReturnType.VALUE);
        System.out.println("foo: " + r);

        // 通过预存的脚本进行同样的操作
        RScript s = client.getScript();
        // 首先将脚本加载到Redis
        String sha1 = s.scriptLoad("return redis.call('get', 'redission:test:foo')");
        // 返回值 res == 282297a0228f48cd3fc6a55de6316f31422f5d17
        System.out.println("sha1: " + sha1);
        // 再通过SHA值调用脚本
        Future<Object> r1 = client.getScript().evalShaAsync(RScript.Mode.READ_ONLY,
                sha1,
                RScript.ReturnType.VALUE,
                Collections.emptyList());

        try {
            System.out.println("res: " + r1.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }


        client.shutdown();
    }


    @Test
    public void testRAtomicLongExamples() {
        // 默认连接上 127.0.0.1:6379
        RAtomicLong atomicLong = client.getAtomicLong("redission:test:myLong");
        // 线程数
        final int threads = 10;
        // 每条线程的执行轮数
        final int turns = 1000;
        ExecutorService pool = Executors.newFixedThreadPool(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {
                    for (int j = 0; j < turns; j++) {
                        atomicLong.incrementAndGet();
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        ThreadUtil.sleepSeconds(5);
        long sum = atomicLong.get();
        System.out.println("atomicLong: " + sum);
        //输出统计结果
        float time = System.currentTimeMillis() - start;

        System.out.println("运行的时长为:" + time);
        System.out.println("每一次执行的时长为:" + time / sum);
        client.shutdown();
    }

    @Test
    public void testRLongAdderExamples() {
        // 默认连接上 127.0.0.1:6379
        RLongAdder longAdder = client.getLongAdder("redission:test:myLongAdder");
        // 线程数
        final int threads = 10;
        // 每条线程的执行轮数
        final int turns = 1000;
        ExecutorService pool = Executors.newFixedThreadPool(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {
                    for (int j = 0; j < turns; j++) {
                        longAdder.increment();
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        ThreadUtil.sleepSeconds(5);
        System.out.println("longAdder: " + longAdder.sum());
        long sum = longAdder.sum();
        //输出统计结果
        float time = System.currentTimeMillis() - start;

        System.out.println("运行的时长为:" + time);
        System.out.println("每一次执行的时长为:" + time / sum);
        client.shutdown();

    }


}

Redisson分布式锁

redisson实现分布式锁

    @Test
    public void testLockDemo() {
        // 默认连接上 127.0.0.1:6379
        // RLock 继承了 java.util.concurrent.locks.Lock 接口
        RLock disLock = client.getLock("DISLOCK");
        boolean isLock = false;
        try {
            //disLock.lock();
            isLock = disLock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
            if (isLock) {

                //TODO if get lock success, do something;
                Thread.sleep(15000);
            }
        } catch (Exception e) {
        } finally {
            // 无论如何, 最后都要解锁
            disLock.unlock();
        }
    }

通过代码可知,经过Redisson的封装,实现Redis分布式锁非常方便,和显式锁的使用方法是一样的。RLock接口继承了 Lock接口。

分布式锁在redis中的数据结构是Hash结构:
● key:所得的名字
● 字段:UUID+threadId
● 值:表示重入的次数
在这里插入图片描述
加锁原理
tryLockInnerAsync是Redission加锁的关键方法
在这里插入图片描述

    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                "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]);",
                Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }

代码得知:KEYS[1]就是getRawName(),ARGV[2]是getLockName(threadId)
● KEYS[1]=DISLOCK: 你加锁的那个key
● ARGV[1]=30秒(默认): 锁key的默认生存时间
● ARGV[2]=01a6d806-d282-4715-9bec-f51b9aa98110:1 : 加锁的客户端的ID

在这里插入图片描述
脚本逻辑:
● 加锁
○ 判断有没有一个叫“DISLOCK”的key
○ 如果没有,则在其下设置一个字段为“01a6d806-d282-4715-9bec-f51b9aa98110:1”,值为“1”
○ 设置它的过期时间
● 锁互斥
○ 客户端2来尝试加锁
○ 判断有没有一个叫“DISLOCK”的key,返回key已存在
○ 接着第二个判断: DISLOCK锁key的hash数据结构中,是否包含客户端2的ID ,返回没有,因为存在客户端1的ID
○ 客户端2会获取到pttl DISLOCK返回的一个数字,这个数字代表了DISLOCK 这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间
○ 此时客户端2会进入一个while循环,不停的尝试加锁
● 锁重入
○ 第一个if判断肯定不成立,“exists DISLOCK”会显示锁key已经存在了
○ 第二个if判断会成立,因为DISLOCK的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”
○ 此时就会执行可重入加锁的逻辑,他会用:incrby DISLOCK 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1,通过这个命令对于客户端1的锁,累加1
○ 此时DISLOCKD数据接口如下:

DISLOCK:
    {
        8743c9c0-0795-4907-87fd-6c719a6b4586:1 2
    }

锁重入代码

RLock lock = redisson.getLock("DISLOCK")
lock.lock();
//业务代码
lock.lock();
//业务代码
lock.unlock();
lock.unlock();

释放锁原理
如果执行lock.unlock(),就可以释放分布式锁,就是每次都对DISLOCK数据结构中的那个加锁次数减1。
如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del DISLOCK”命令,从redis里删除这个key。然后呢,另外的客户端2就可以尝试完成加锁了。

    @Override
    public void unlock() {
        try {
            get(unlockAsync(Thread.currentThread().getId()));
        } catch (RedisException e) {
            if (e.getCause() instanceof IllegalMonitorStateException) {
                throw (IllegalMonitorStateException) e.getCause();
            } else {
                throw e;
            }
        }

核心代码
在这里插入图片描述

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                        "return nil;" +
                        "end; " +
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                        "if (counter > 0) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                        "return 0; " +
                        "else " +
                        "redis.call('del', KEYS[1]); " +
                        "redis.call('publish', KEYS[2], ARGV[1]); " +
                        "return 1; " +
                        "end; " +
                        "return nil;",
                Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

调试可知:
● KEYS[1]:getRawName(),即KEYS[1]=DISLOCK
● KEYS[2]:getChannelName(),即KEYS[2]=redisson_lock__channel:{DISLOCK}
● ARGV[1]:LockPubSub.unlockMessage,即ARGV[1]=0
● ARGV[2]:生存时间
● ARGV[3]:getLockName(threadId),即ARGV[3]=8743c9c0-0795-4907-87fd-6c719a6b4586:1
上述脚本逻辑

  1. 判断是否存在一个叫“DISLOCK”的key
  2. 如果不存在,返回nil
  3. 如果存在,使用Redis Hincrby 命令用于为哈希表中的字段值加上指定增量值 -1 ,代表减去1
  4. 若减完以后,counter > 0 值仍大于0,则返回0
  5. 减完后,若字段值小于或等于0,则用 publish 命令广播一条消息,广播内容是0,并返回1;
    可以猜测,广播0表示资源可用,即通知那些等待获取锁的线程现在可以获得锁了

在这里插入图片描述
解锁订阅源码
在这里插入图片描述
watch dog自动延期
假如我的业务操作比有效时间长,我的业务代码还没执行完,就自动给我解锁了,这个问题的解决方案

  1. 预估执行时间

预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。
但是这并不是万全之策,比如网络抖动这种情况是无法预测的,也有可能导致业务代码执行的时间变长,所以并不安全。

  1. watch dog 机制

当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁

Redisson框架实现的分布式锁,就使用了watchDog机制实现锁的续期,这种方法比较靠谱,无业务代码入侵
在这里插入图片描述

注意:
● watchDog 只有在未显示指定加锁时间时才会生效。(这点很重要)
● lockWatchdogTimeout:可以设置超时时间

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值