• Redis系列之客户端Redisson


    概述

    官方推荐的客户端,支持Redis单实例、Redis哨兵、Redis Cluster、Redis master-slave等各种部署架构。
    GitHub

    功能:

    • 分布式锁

    分布式锁

    使用Redisson提供的分布式锁的一个最常见场景,应用部署为多个节点,然后使用Spring提供的原生@Scheduled任务调度功能;而没有使用xxl-job等轻量级分布式任务调度系统(底层基于数据库悲观锁)

    @Scheduled(cron = "0 0 8 * * ?")
    public void execute() {
        RLock lock = redissonClient.getLock("myLock");
        try {
            boolean isLock = lock.tryLock(1, 5, TimeUnit.MINUTES);
            if (!isLock) {
                log.warn("job正在执行!");
                return;
            }
            log.info("任务开始执行!");
        } catch (Exception e) {
            log.error("执行失败:", e);
            lock.unlock();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    通过lock.tryLock()查看源码,一步步往里看:

    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', 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(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }
    
    • 1
    • 2
    • 3
    • 4

    稍微格式化一下,方便阅读:

    if (redis.call('exists', KEYS[1]) == 0) 
    	then redis.call('hset', 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]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    也就是说,需要执行一段Lua脚本:

    • KEYS[1]代表加锁的Key,即myLock
    • ARGV[1]代表加锁Key的生存时间,默认30秒
    • ARGV[2]代表加锁客户端ID,格式UUID:n,如:e197fb92-deeb-4f9d-9d34-51b9b09f0bd7:1,其中n表示Redis Cluster集群节点

    第一段if判断语句,用exists myLock判断一下,如果要加锁的Key不存在,则通过命令hset myLock e197fb92-deeb-4f9d-9d34-51b9b09f0bd7:1 1加锁,即设置一个Hash数据结构。命令执行后会生成类似如下数据结构:

    myLock:
    {
    "e197fb92-deeb-4f9d-9d34-51b9b09f0bd7:1": 1
    }
    
    • 1
    • 2
    • 3
    • 4

    接着执行pexpiremyLock 30000命令,设置myLock这个锁Key的生存时间是30秒,加锁完成。

    watch dog自动延期机制
    客户端1加锁的Key默认过期时间30秒,客户端1只要加锁成功,就会启动一个watchdog后台线程,每隔10秒检查一下,如果客户端1还持有锁Key,就会不断的延长锁Key的生存时间。

    释放锁
    执行lock.unlock(),即释放分布式锁,执行一次lock.unlock(),对myLock数据结构中的加锁次数减1。
    加锁次数未0,说明此客户端已经不再持有锁,触发删除所del myLock命令。其他客户端即可尝试加锁。

    缺点

    上面那种方案最大的问题,就是如果你对某个Redis Master实例,写入myLock这种锁Key的Value,此时会异步复制给对应的Master Slave实例。

    但是这个过程中一旦发生Redis Master宕机,主备切换,Redis Slave变为Redis Master。

    会导致客户端2尝试加锁时,在新的Redis Master上完成加锁,客户端1也以为自己成功加锁。

    此时就会导致多个客户端对一个分布式锁完成加锁。这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。

    所以这个就是Redis Cluster,或是redis master-slave架构的主从异步复制导致的Redis分布式锁的最大缺陷:在Redis Master实例宕机的时候,可能导致多个客户端同时完成加锁。

    在基于NIO的Netty框架上,充分利用Redis提供的一系列优势,

    问题

    ClassNotFoundException: org.nustaq.serialization.FSTConfiguration

    Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.redisson.api.RedissonClient]: Factory method 'redisson' threw exception; nested exception is java.lang.NoClassDefFoundError: Lorg/nustaq/serialization/FSTConfiguration;
    Caused by: java.lang.ClassNotFoundException: org.nustaq.serialization.FSTConfiguration
    
    • 1
    • 2

    出现上述报错的原因在于,配置文件里有Fst相关引用:

    threads: 0
    nettyThreads: 0
    codec: ! {}
    transportMode: "NIO"
    
    • 1
    • 2
    • 3
    • 4

    解决方案,pom.xml文件里新增:

    <dependency>
    	<groupId>de.ruedigermoellergroupId>
    	<artifactId>fstartifactId>
    	<version>2.57version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    attempt to unlock lock, not locked by current thread by node id

    java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: 633dfc8a-b388-4ba1-ad64-75b491d0c5f2 thread-id: 118
        at org.redisson.misc.RedissonPromise.trySuccess(RedissonPromise.java:88)
        at org.redisson.command.CommandAsyncService.handleReference(CommandAsyncService.java:1067)
        at org.redisson.command.CommandAsyncService.handleSuccess(CommandAsyncService.java:1059)
        at org.redisson.command.CommandAsyncService.checkAttemptFuture(CommandAsyncService.java:1041)
        at org.redisson.command.CommandAsyncService$12.operationComplete(CommandAsyncService.java:805)
        at org.redisson.misc.RedissonPromise.trySuccess(RedissonPromise.java:88)
        at org.redisson.client.handler.CommandDecoder.completeResponse(CommandDecoder.java:448)
        at org.redisson.client.handler.CommandDecoder.handleResult(CommandDecoder.java:443)
        at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:354)
        at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:128)
        at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:108)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    解决方案:

    finally {
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Command (SET), params [] succesfully sent, but channel [] has been closed

    详细的报错信息:

    org.springframework.data.redis.RedisConnectionFailureException: Command (SET), params [] succesfully sent, but channel [] has been closed!
    at org.redisson.spring.data.connection.RedissonExceptionConverter.convert(RedissonExceptionConverter.java:40)
    	at org.redisson.spring.data.connection.RedissonExceptionConverter.convert(RedissonExceptionConverter.java:35)
    	at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
    	at org.redisson.spring.data.connection.RedissonConnection.transform(RedissonConnection.java:237)
    	at org.redisson.spring.data.connection.RedissonConnection.syncFuture(RedissonConnection.java:232)
    	at org.redisson.spring.data.connection.RedissonConnection.sync(RedissonConnection.java:462)
    	at org.redisson.spring.data.connection.RedissonConnection.write(RedissonConnection.java:828)
    	at org.redisson.spring.data.connection.RedissonConnection.set(RedissonConnection.java:596)
    	at org.springframework.data.redis.connection.DefaultStringRedisConnection.set(DefaultStringRedisConnection.java:946)
        at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224)
    	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184)
    	at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:95)
    	at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:236)	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    参考GitHub-issue

    Redis connection is closed for some reason. Try to set pingConnectionInterval: 60000.

    解决方法:在redisson.yml文件里新增配置:pingConnectionInterval: 60000

    RedisResponseTimeoutException: Redis server response timeout occured after 3 retry attempts. Command: params:

    解决方法同上,新增配置。

      参考

    • 相关阅读:
      MobileNetV1架构解析
      tiup cluster display
      使用 TensorFlow.js 在浏览器中进行自定义对象检测
      c++::作用域符解析
      HTTP 参数污染 (HPP) 和 HTTP 参数碎片 (HPF)
      python 文件查找性能对比 python与powershell
      阿里后端开发:抽象建模经典案例【文末送书】
      数组去重的六种方法
      Java中java.util.Arrays参考指南
      Java高级技术之Gradle
    • 原文地址:https://blog.csdn.net/lonelymanontheway/article/details/115363360