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分布式锁