使用redission实现分布式信号量以及遇到的一些坑
1、项目背景
公司的缓存组件WRedis不再支持,所以需要将之前实现的WRedis迁移到新的缓存组件Redis中。Redisson基于java.utils提供了一系列分布式的工具类,比如Map、List、Lock等工具类。在redis和java增加了一层,让我们以更熟悉的方式操作Redis。RPermitExpirableSemaphore(可过期性信号量)是Redisson提供为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。
2、连接RedissonClient
redisson提供多种配置方式,程序式的、配置式的,本文选择json配置式。
1 | Config config = Config.fromJSON(redisCfgFile);<br>RedissonClient redissonClient = Redisson.create(config); |
创建RedissionClient,使用json的形式创建。
redis的配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | { "singleServerConfig" :{ "idleConnectionTimeout" : 10000 , "pingTimeout" : 1000 , "connectTimeout" : 10000 , "timeout" : 6000 , "retryAttempts" : 3 , "retryInterval" : 1500 , "reconnectionTimeout" : 3000 , "failedAttempts" : 3 , "password" : "xxxxx" , "subscriptionsPerConnection" : 5 , "clientName" : null , "address" : "redis://xxxx:000" , "subscriptionConnectionMinimumIdleSize" : 1 , "subscriptionConnectionPoolSize" : 50 , "connectionMinimumIdleSize" : 32 , "connectionPoolSize" : 64 , "database" : 0 }, "threads" : 0 , "nettyThreads" : 0 , "codec" :{ "class" : "org.redisson.codec.JsonJacksonCodec" }, "transportMode" : "NIO" } |
配置说明:https://yq.aliyun.com/articles/551640?spm=a2c4e.11153940.0.0.3cc221bavS8pf3(参考)
遇到的坑:
2.1、断开连接
redis使用5.0,redission使用3.10.7,出现经常client与server断开连接问题
追查问题,将线上redis版本降低,从5.0->4.0,不会出现断开连接的问题。但是redission不支持断开重连,使用定时任务去定时的ping server,断开后手动重连。
2.2、切换主从无法自动重连
public static void init() { SCHEDULED_EXECUTOR_SERVICE.scheduleWithFixedDelay(() -> { if (RUNNING) { try { NodesGroup nodesGroup = redissonClient.getNodesGroup(); Collection<Node> allNodes = nodesGroup.getNodes(); for (Node n : allNodes) { boolean ping = n.ping(); if (!ping) { //ping不通reload client reload(); } } } catch (Exception e) { //抛出异常reload client reload(); } } }, 0, 5, TimeUnit.SECONDS); }
2.3、线上经常出现timed out
修改配置文件中timeout 3000 -> 6000,因为有一个5s的定时任务在ping server,所以设置稍大一点,就不会出现了。
3、使用redission实现分布式信号量
Redisson自带一个RPermitExpirableSemaphore(有过期时间的分布式信号量)
官方的解释:
为每个申请对象提供参数化的释放时间的信号量,每个许可证可以被自己的id识别,而且可以被自己的id释放。这个许可证id是128b随机数。同时这个分布式信号量工作于非公平模式,因此申请的顺序是不可以预测。
具体实现如下:
3.1、申请许可证
/** * @param semaphoreName 信号量标识 * @param acquireId 获取者的标识 * @param limit 总量 * @return int 0-成功,其他-失败 * @Description: 指定名称和总量获取信号量 */ public static int acquireSemaphore(String semaphoreName, String acquireId, long limit) { try { RedissonClient client = RedisUtils.getRedissonClient(); RPermitExpirableSemaphore semaphore = client.getPermitExpirableSemaphore(semaphoreName); semaphore.trySetPermits((int) limit); //每申请一次信号量,expire信号量的生命SEMAPHORE_LIFE_EXPIRE秒 semaphore.expire(SEMAPHORE_LIFE_EXPIRE, TimeUnit.SECONDS); //尝试次数init int time = 0; while (MAX_TRY_ACQUIRE_TIME > time) { //尝试获取信号量 String permitId = semaphore.tryAcquire(getSemaphoreLifeExpire(), getSemaphoreAcquireExpire(), TimeUnit.MILLISECONDS); //获取信号量失败 if (null == permitId) { time++; continue; } //获取信号量成功,设置acquireId和permitId的映射关系 if (!RedisUtils.hset(getMapName(semaphoreName), acquireId, permitId, getSemaphoreAcquireExpire(), TimeUnit.MILLISECONDS)) { //如果失败,释放资源 semaphore.release(permitId); return acquireError(); } return 0; } } catch (Exception e) { e.printStackTrace(); LOG.error("exception for semaphoreName={}, acquireId={}, msg={}", semaphoreName, acquireId, e.getMessage()); } return acquireError(); }
将申请的的许可证id和acquireId放到hash结构中,做一个映射,因为需要这个许可证id去释放资源。
流程图:
3.2、释放信号量
/** * @param semaphoreName 信号量标识 * @param acquireId 获取者的标识 * @Description: 释放对应的信号量 * @return: int 0 成功 1 失败(超时的错误可能就无法成功释放) * @Author: chi.zhang * @Date: 2020/02/19 */ public static int releaseSemaphore(String semaphoreName, String acquireId) { try { RedissonClient client = RedisUtils.getRedissonClient(); RPermitExpirableSemaphore semaphore = client.getPermitExpirableSemaphore(semaphoreName); //根据映射关系找到permitId String permitId = RedisUtils.hgetStr(getMapName(semaphoreName), acquireId); if (StringUtils.isNotEmpty(permitId)) { //可能被释放,所以使用tryRelease if (semaphore.tryRelease(permitId)) { RedisUtils.hdel(getMapName(semaphoreName), acquireId); } } else { LOG.error("释放分布式信号量失败,semaphoreName:{},permitId{}", semaphoreName, permitId); return releaseError(); } } catch (Exception e) { e.printStackTrace(); LOG.error("exception for semaphoreName={}, acquireId={}, msg={}", semaphoreName, acquireId, e.getMessage()); return releaseError(); } return 0; }
流程图:
参考文档
https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器