浅谈Redis客户端Redisson


Redisson

1.分布式锁

(1) 单应用使用锁

说明:使用 Java 的 Synchronized 或者 ReentrantLock 关键字加锁

缺点:当用户扩大并发飙升时,单应用扛不住流量出现宕机从而影响整个项目,如果选择用多加机器的方式来解决,此加锁方式无法解决超卖等问题(库存为1,两个应用分别从两台服务器中请求进来)

各个服务器加的锁只对属于自己 JVM 里面的线程有效,对于其他 JVM 的线程是无效的,所以只适合并发量不大的单应用场景

(2) 分布式锁

原理:为整个集群或者系统中提供一个全局唯一能获取锁的应用(redis、ZK等),不同服务器在同一业务场景下获取的锁保证为同一个锁

传统redis分布式锁

获取锁: Redis SETNX命令(后期redis版本使用set命令同步设置过期时间即可)

// 获取锁 
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间 
SET anyLock unique_value NX PX 30000 

释放锁:通过执行一段lua脚本

1.释放锁涉及到两条指令,这两条指令不是原子性的 
2.需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的 
Lua脚本代码:
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

关键点
1.必须使用set命令(附上NX),单独使用set和设置过期时间不为原子操作
2.set命令(附上NX)设置的value必须具有唯一性,例如随机字符串(防止A获取锁因为时间超时导致锁释放后B获取锁,A再释放B刚获取到的锁的问题)

redis部署方式弊端

  • 单机模式:当这台redis宕机之后就无法提供加锁导致业务血崩
  • 主从\集群模式:当master节点宕机,哨兵sentinel选举后发生主从切换时,原先的从节点可能丢失锁

红锁RedLock

无法保证加锁的过程一定正确

Redisson分布式锁

传统redis分布式锁的弊端

  使用setnx命令设置超时时间为30s,但是当30s过后业务A未执行完但是另外一个线程\请求B已进来也获取到该锁,而后业务A执行结束释放锁,就会出现超卖等线程安全问题;
PS:所以需要人为根据业务来维护这个超时时间

redisson优点

1.传统分布式锁弊端解决 - watchDog看门狗

  看门狗作用:监控锁

1:当获取锁没有设置key超时时间时,会设置默认超时时间为30s,之后看门狗会每隔10s把key的超时时间延长至30s,保证锁一直被当前线程占用直至业务结束释放锁
2:Redisson 的“看门狗”逻辑保证了没有死锁发生。(如果机器宕机了,看门狗也就没了。此时就不会延长 Key 的过期时间,到了 30s 之后就会自动过期了,其他线程可以获取到锁)
3:当设置了key超时时间时,则超时key会自动释放
4:个人思考:加锁是加在了当前redis实例中,当实例redis宕机时,看门狗消失就不会延长时间而自动过期(当为redis主从集群例如三主三从时,每个key根据算法(相关:只分配给master的槽节点)分配到对应redis中)

2.使用简单
Config config = new Config(); 
config.useSingleServer()
.addNodeAddress("redis:\\192.168.31.101:7001")
.setPassword(password)
.setDatabase(0); 

RedissonClient redissonClient = Redisson.create(config); 

RLock lock = redissonClient.getLock("anyLock"); 
lock.lock(); 
lock.unlock(); 

// 尝试加锁,最多等待lockTime毫秒,上锁以后leaseTime毫秒自动解锁
if(!lock.tryLock(waitTime,leaseTime, TimeUnit.MILLISECONDS)){
    log.warn("lockKey = {},重复提交!",lockKey);
    throw new ServiceException(201,"请勿重复提交!");
}

1:Redisson 所有指令都通过 Lua 脚本执行,Redis 支持 Lua 脚本原子性执行。

2.发布订阅实现配置缓存相关操作

说明:redisson简单的api + springboot CommandLineRunner实现启动时执行配置初始化和发布订阅的功能,当redis客户端发布命令时,进行相应的操作

@Bean
public CommandLineRunner init(ApplicationContext ctx) {
    return args ->{
        // RedissonClient在config中已配置
        RedissonClient redissonClient = ctx.getBean(RedissonClient.class);
        SysTenantServiceImpl sysTenantService = ctx.getBean(SysTenantServiceImpl.class);
        // 初始化配置
        sysTenantService.initSysTenant();
        // redis发布订阅
        RTopic<String> topic = redissonClient.getTopic(KEY, StringCodec.INSTANCE);
        // 添加监听器
        topic.addListener((channel, message) -> {
            JSONObject msg = JSONUtil.parseObj(message);
            if("DEL".equals(MapUtil.getStr(msg, "action"))) {
                sysTenantService.removeByOrgId(MapUtil.getInt(msg, "Id"));
            } else if("ALL".equals(MapUtil.getStr(msg, "action"))) {
                sysTenantService.initSysTenant();
            } else if("UPDATE".equals(MapUtil.getStr(msg, "action"))) {
                sysTenantService.updateByOrgId(MapUtil.getInt(msg, "Id"));
            }
        });
        log.info("TOPIC-BILL-BOOK-CONFIG-REFRESH Listener创建成功");
    };
}

redis 客户端命令例子:
(1) publish KEY "{\"action\":\"ALL\"}"
(2) publish KEY "{\"action":\"DEL\", \"Id\":\"1\"}"

3.布隆过滤器 - bloomfilter

PS:创建布隆过滤器需要根据业务场关注 期望插入值判断失误的概率
详细了解布隆过滤器可参考笔者写的5分钟搞懂布隆过滤器 | 寒暄

# 1.从redis获取布隆过滤器
RBloomFilter<String> filter = redissonClient.getBloomFilter("testBloomFilter");
# 2.不存在则初始化
filter.tryInit(10000, 0.01);
# 3.新增
for (int i=0;i<20;i++){
    filter.add(StrUtil.toString(i));
}

# 4.判断是否存在
for (int i=0;i<20;i++){
    log.info("key:{},是否存在:{}",i,filter.contains(StrUtil.toString(i)));
}

4.redis集群配置 - redis cluster

集群初始化配置

List<String> nodes = Lists.newArrayList();
nodes.add("redis:\\192.168.10.139:6379");
nodes.add("redis:\\192.168.10.140:6379");
nodes.add("redis:\\192.168.10.141:6379");
nodes.add("redis:\\192.168.10.142:6379");
nodes.add("redis:\\192.168.10.143:6379");
nodes.add("redis:\\192.168.10.144:6379");
String[] array = Convert.toStrArray(nodes);

Config config = new Config();
ClusterServersConfig clusterServersConfig = config.useClusterServers();
clusterServersConfig
        .setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
        .addNodeAddress(array);

redissonClient = Redisson.create(config);

【参考链接】
1:分布式锁
2:Redis解析:SET复合命令和简易的分布式锁优化
3:redis官方文档 – set 命令
4:redisson官方文档
5:布隆过滤器
6:细说Redis分布式锁


评论
  目录