当前位置:   article > 正文

记录redis连接被打满的踩坑之路_redis连接数满了

redis连接数满了

一、系统异常现象

系统有一个功能向别的系统多线程推送用户数据信息,前几天发现该推送功能报内部错误,经过查看后台日志文件,发现org.redisson.client.RedisConnectionException: Unable to connect to Redis server:,io.netty.channel.ChannelException: Unable to create Channel from class class io.netty.channel.socket.nio.NioSocketChannel。

错误日志输出特别清晰明了,显示redis的连接已经满了,不能创建新的NioSocketChannel了。

二、问题分析

在该功能用户redis的地方无非就两个。

  1. 第一个场景是多线程请求其他系统的token值,这个过程中加了锁,确保多线程只有一个线程拿到token值并且将该token写入redis,其他线程复用这个线程获取的token进行用户数据推送,这样可以降低双方系统的资源消耗。

2、第二个场景就是在redis里面取出上一步的token值,进行下一步的业务处理。

关键代码:

  1. public String generateToken(RedissonClient redissonClient) {
  2. RLock lock = redissonClient.getLock(InterfacesConstants.REDISSON_LOCK);
  3. try {
  4. lock.lock(30, TimeUnit.SECONDS);
  5. RBucket<String> testBucket = redissonClient.getBucket(InterfacesConstants.PUSH_USER_TOKEN);
  6. //获取推送用户的token值
  7. String pushUserToken = testBucket.get();
  8. log.info("线程名为:{}的线程获取到「pushUserToken」为:{}", Thread.currentThread().getName(), pushUserToken);
  9. if (EmptyUtil.isEmpty(pushUserToken)) {
  10. //获取token
  11. IDasTokenResultDto tokenFromIDas = getTokenFromIDas();
  12. //获取IAM平台的token值
  13. pushUserToken = tokenFromIDas.getAccess_token();
  14. if (EmptyUtil.isEmpty(pushUserToken)) {
  15. log.info("未接收到IAM返回的token值!!");
  16. return null;
  17. }
  18. //重置token过期时间
  19. testBucket.set(pushUserToken, (Integer.parseInt(tokenFromIDas.getExpires_in())) / 1000 - 5, TimeUnit.SECONDS);
  20. }
  21. //拼接IAM所需要的token值
  22. String finalAccessToken = "Bearer " + pushUserToken;
  23. return finalAccessToken;
  24. } catch (Exception e) {
  25. log.error("获取IAM平台的token值错误,error message:{}", e.getMessage());
  26. } finally {
  27. lock.unlock();
  28. }
  29. return null;
  30. }

仔细检查上述场景的业务代码,也没有发现类似的问题。。。。。。

回过头又仔细分析了整个业务链条的场景,终于发现了问题所在。问题出在了RedissonClient实例化上,一般的项目会把这种公共的类交给spring容器去自动实例化,这个老项目因为架构的原因开发者自己去实例的该对象。

所以每次触发这个功能都会实例一次RedissonClient对象,从而导致redis的连接越来越来多并没有释放,一直到沾满。所以只要在RedissonClient实例的时候做点小动作就可以填上这个坑了。

原代码:

  1. @Slf4j
  2. public class RedissonConfig {
  3. private static final String REDIS_CONFIG_FILE = "Config/redis.properties";
  4. private static final String SINGLE = "standalone";
  5. private static final String CLUSTER = "cluster";
  6. private static final String HOST_PORT = "hostports";
  7. private RedissonConfig() {
  8. throw new IllegalStateException("Utility class");
  9. }
  10. public static RedissonClient initialRedissonClient() {
  11. Map<String, String> redisInfoMap = getHostAndPwd();
  12. if(EmptyUtil.isEmpty(redisInfoMap)){
  13. return null;
  14. }
  15. String runMode = redisInfoMap.get("runMode");
  16. RedissonClient redisson=null;
  17. //单机
  18. if(Objects.equals(SINGLE,runMode)){
  19. redisson = initialSingleRedisson();
  20. log.info("实例化单节点「RedissonClient」成功!!");
  21. //集群
  22. }else if (Objects.equals(CLUSTER,runMode)){
  23. redisson = initialClusterRedisson();
  24. log.info("实例集群「RedissonClient」成功!!");
  25. }
  26. return redisson;
  27. }
  28. public static RedissonClient initialSingleRedisson(){
  29. try {
  30. Map<String, String> redisInfoMap = getHostAndPwd();
  31. if(EmptyUtil.isEmpty(redisInfoMap)){
  32. return null;
  33. }
  34. String hostPort = redisInfoMap.get(HOST_PORT);
  35. String redisPwd = redisInfoMap.get("pwd");
  36. //单机
  37. Config config = new Config();
  38. config.useSingleServer().setAddress("redis://" + hostPort).setPassword(redisPwd);
  39. RedissonClient redisson = Redisson.create(config);
  40. log.info("「SingleRedisson」初始化成功!!");
  41. return redisson;
  42. }catch (Exception e){
  43. log.info("「SingleRedisson」初始化异常!!message:{}",e.getMessage());
  44. e.printStackTrace();
  45. }
  46. return null;
  47. }
  48. public static RedissonClient initialClusterRedisson(){
  49. //集群
  50. try {
  51. Map<String, String> redisInfoMap = getHostAndPwd();
  52. if(EmptyUtil.isEmpty(redisInfoMap)){
  53. return null;
  54. }
  55. String hostPort = redisInfoMap.get(HOST_PORT);
  56. String redisPwd = redisInfoMap.get("pwd");
  57. Config config = new Config();
  58. String[] hostPostArr = hostPort.split(",");
  59. for (String str : hostPostArr) {
  60. config.useClusterServers()
  61. //设置扫描间隔时间
  62. .setScanInterval(2000)
  63. .setPassword(redisPwd)
  64. .addNodeAddress("redis://" + str);
  65. }
  66. RedissonClient redisson = Redisson.create(config);
  67. return redisson;
  68. }catch (Exception e) {
  69. log.info("「ClusterRedisson」初始化异常!!message:{}",e.getMessage());
  70. e.printStackTrace();
  71. }
  72. return null;
  73. }
  74. public static Map<String,String> getHostAndPwd(){
  75. try {
  76. //加载连接池配置文件
  77. Properties props = PropsUtil.loadProps(REDIS_CONFIG_FILE);
  78. String runMode = props.getProperty("redis.runMode");
  79. String hostPort = props.getProperty("redis.hostports");
  80. String redisPwd = props.getProperty("redis.password");
  81. Map<String, String> map = new HashMap<>();
  82. map.put("runMode",runMode);
  83. map.put(HOST_PORT,hostPort);
  84. map.put("pwd",redisPwd);
  85. log.info("解析redis配置文件获取到的连接信息:{}",map);
  86. if(EmptyUtil.isEmpty(map)){
  87. log.warn("未解析到redis的配置信息!!");
  88. return Maps.newHashMap();
  89. }
  90. return map;
  91. }catch (Exception e) {
  92. log.info("redisson初始化获取结点和密码失败");
  93. e.printStackTrace();
  94. }
  95. return Maps.newHashMap();
  96. }
  97. }

三、问题解决

知道问题在哪了,剩下的就好说了,我们只要保证该Java进程里面实例化有且只有一个RedissonClient对象就可以,spring的单例bean还用不了,这就得造一波轮子了。都到这了,单例模式走起。。。

优化后代码:

  1. @Slf4j
  2. public class RedissonConfig {
  3. private static final String REDIS_CONFIG_FILE = "Config/redis.properties";
  4. private static final String SINGLE = "standalone";
  5. private static final String CLUSTER = "cluster";
  6. private static final String HOST_PORT = "hostports";
  7. /**
  8. * volatile修饰(防止指令重排序)
  9. **/
  10. private volatile static RedissonClient instance;
  11. private RedissonConfig() {
  12. throw new IllegalStateException("Utility class");
  13. }
  14. public static RedissonClient initialRedissonClient() {
  15. //单例模式的双重校验
  16. if(instance == null){
  17. log.info("「RedissonClient」实例instance为空,进行实例化操作");
  18. synchronized (RedissonConfig.class) {
  19. //如果是空,就实例化对象
  20. if (instance == null) {
  21. Map<String, String> redisInfoMap = getHostAndPwd();
  22. if(EmptyUtil.isEmpty(redisInfoMap)){
  23. return null;
  24. }
  25. String runMode = redisInfoMap.get("runMode");
  26. //单机
  27. if(Objects.equals(SINGLE,runMode)){
  28. instance = initialSingleRedisson();
  29. log.info("实例化单节点「RedissonClient」成功!!");
  30. //集群
  31. }else if (Objects.equals(CLUSTER,runMode)){
  32. instance = initialClusterRedisson();
  33. log.info("实例集群「RedissonClient」成功!!");
  34. }
  35. }
  36. }
  37. }else {
  38. log.info("「RedissonClient」实例instance为:{},无需重新实例化「RedissonClient」",instance);
  39. }
  40. return instance;
  41. }
  42. public static RedissonClient initialSingleRedisson(){
  43. try {
  44. Map<String, String> redisInfoMap = getHostAndPwd();
  45. if(EmptyUtil.isEmpty(redisInfoMap)){
  46. return null;
  47. }
  48. String hostPort = redisInfoMap.get(HOST_PORT);
  49. String redisPwd = redisInfoMap.get("pwd");
  50. //单机
  51. Config config = new Config();
  52. config.useSingleServer().setAddress("redis://" + hostPort).setPassword(redisPwd);
  53. RedissonClient redisson = Redisson.create(config);
  54. log.info("「SingleRedisson」初始化成功!!");
  55. return redisson;
  56. }catch (Exception e){
  57. log.info("「SingleRedisson」初始化异常!!message:{}",e.getMessage());
  58. e.printStackTrace();
  59. }
  60. return null;
  61. }
  62. public static RedissonClient initialClusterRedisson(){
  63. //集群
  64. try {
  65. Map<String, String> redisInfoMap = getHostAndPwd();
  66. if(EmptyUtil.isEmpty(redisInfoMap)){
  67. return null;
  68. }
  69. String hostPort = redisInfoMap.get(HOST_PORT);
  70. String redisPwd = redisInfoMap.get("pwd");
  71. Config config = new Config();
  72. String[] hostPostArr = hostPort.split(",");
  73. for (String str : hostPostArr) {
  74. config.useClusterServers()
  75. //设置扫描间隔时间
  76. .setScanInterval(2000)
  77. .setPassword(redisPwd)
  78. .addNodeAddress("redis://" + str);
  79. }
  80. RedissonClient redisson = Redisson.create(config);
  81. return redisson;
  82. }catch (Exception e) {
  83. log.info("「ClusterRedisson」初始化异常!!message:{}",e.getMessage());
  84. e.printStackTrace();
  85. }
  86. return null;
  87. }
  88. public static Map<String,String> getHostAndPwd(){
  89. try {
  90. //加载连接池配置文件
  91. Properties props = PropsUtil.loadProps(REDIS_CONFIG_FILE);
  92. String runMode = props.getProperty("redis.runMode");
  93. String hostPort = props.getProperty("redis.hostports");
  94. String redisPwd = props.getProperty("redis.password");
  95. Map<String, String> map = new HashMap<>();
  96. map.put("runMode",runMode);
  97. map.put(HOST_PORT,hostPort);
  98. map.put("pwd",redisPwd);
  99. log.info("解析redis配置文件获取到的连接信息:{}",map);
  100. if(EmptyUtil.isEmpty(map)){
  101. log.warn("未解析到redis的配置信息!!");
  102. return Maps.newHashMap();
  103. }
  104. return map;
  105. }catch (Exception e) {
  106. log.info("redisson初始化获取结点和密码失败");
  107. e.printStackTrace();
  108. }
  109. return Maps.newHashMap();
  110. }
  111. }

对比以前的代码只需要实例化的时候改成单例模式就行了。

四、总结

出现类似问题的原因可能一般开发者潜移默化的依赖spring架构的便利性,以至于一些特殊场景的实现不知觉中就按照以前的编码风格去开发,前期简单测试不会发现问题所在,系统压力上来了以后才能暴露出来。程序猿:我不管,反正是测试的锅,谁让他们压测做的不到位呢!!测试:我尼玛。。。。。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/857694
推荐阅读
相关标签
  

闽ICP备14008679号