使用jedis2.9.1的连接池的小伙伴可能存在一个问题,在程序高并发运行一段时间后会出现Could not get a resource from the pool的报错信息,并且在停止接口调用后查看到的redistemplate的连接工厂里面的空闲连接idelPool始终为0,,这部分连接不会再释放,初步怀疑是jedis连接池泄露问题,经过资料的查阅发现jedis2.9.2以下版本存在连接池泄露的问题,在官方可以看到在2.9.2的bug修复说明(经过我的全方位的测试发现这个bug是在2.9.1改出来的,2.9.0的版本也不存在这个问题的,真的坑啊)
如果版本不能升级可以采用开启jedis连接池巡检功能:jedis连接池巡检定时释放不可用资源
@Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private Integer port; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.database}") private Integer dataBase; @Value("${spring.redis.jedis.pool.max-wait}") private Integer MAX_WAIT_MILLIS; @Value("${spring.redis.jedis.pool.max-active}") private Integer MAX_TOTAL; @Value("${spring.redis.jedis.pool.max-idle}") private Integer MAX_IDLE; // @Bean(name = "redisTemplate") @Bean RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory(host, port, password, MAX_IDLE, MAX_TOTAL, MAX_WAIT_MILLIS, dataBase)); Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(jacksonSeial); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(jacksonSeial); template.afterPropertiesSet(); return template; } //配置工厂 public RedisConnectionFactory connectionFactory(String host, int port, String password, int maxIdle, int maxTotal, long maxWaitMillis, int index) { JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(); jedisConnectionFactory.setHostName(host); jedisConnectionFactory.setPort(port); if (!StringUtils.isEmpty(password)) { jedisConnectionFactory.setPassword(password); } if (index != 0) { jedisConnectionFactory.setDatabase(index); } jedisConnectionFactory.setPoolConfig(poolConfig(maxIdle, maxTotal, maxWaitMillis)); jedisConnectionFactory.afterPropertiesSet(); return jedisConnectionFactory; } //连接池配置 public JedisPoolConfig poolConfig(int maxIdle, int maxTotal, long maxWaitMillis) { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxIdle(maxIdle); poolConfig.setMaxTotal(maxTotal); poolConfig.setMaxWaitMillis(maxWaitMillis); // poolConfig.setTestOnBorrow(true); // poolConfig.setTestOnReturn(true); // //Idle时进行连接扫描 // poolConfig.setTestWhileIdle(true); // //表示idle object evitor两次扫描之间要sleep的毫秒数 // poolConfig.setTimeBetweenEvictionRunsMillis(30000); // //表示idle object evitor每次扫描的最多的对象数 // poolConfig.setNumTestsPerEvictionRun(10); // //表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义 // poolConfig.setMinEvictableIdleTimeMillis(60000); return poolConfig; } }
不过jedis的团队规模也比较小,spring框架默认以及推荐的又是lettuce,lettuce有个很大的优点就是连接在线程之间共享,哪怕不用连接池性能都非常优秀,lettuce的社区比较活跃,所以还可以采用lettuce的方式解决连接池泄露问题
lettuce原理:Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例,lettuce连接池配置如下
spring: redis: host: 192.168.1.1 # 服务器地址 port: 6379 # 服务端口号 database: 0 # 数据库索引 password: 123 # 服务器连接密码默认为空 lettuce: pool: max-active: 1000 # 连接池最大连接数 max-wait: 3000 # 连接池最大阻塞等待时间 max-idle: 500 # 连接池中最大的空闲连接 min-idle: 100 # 连接池中最小的空闲连接 time-between-eviction-runs: 60000 #此项配置必须,lettuce最小空闲连接会根据是否有配置这个值生效,默认-1 timeout: 200 # 连接超时时间
不过lettuce存在集群检测以及偶现超时的问题
lettuce不会自动扫描集群节点是否正常,当cluster模式存在redis单点故障后,一直会有调用不同报错的情况,要解决这个问题有两种方式:
1、使用springboot2.3.x以后的版本并同时加上拓扑刷新的配置
spring: redis: xxx... lettuce: cluster: refresh: adaptive: true #10秒自动刷新一次 period: 10
此方法会每10秒检测一次节点是否可用,不过当节点真出问题的时候还是存在10秒的空档期,这期间会不断的请求失败,显然不符合预期,如果通过调短扫描间隔又会增加系统负担,也不会根本解决空档期的问题,所以还需要加上cluster的重定向次数的配置(max-redirects),当节点不可连接的时候剔除节点并连接到可用的节点,拓扑刷新也同时运行,当节点恢复的时候,节点重新加入客户端的连接中
spring: redis: cluster: nodes: 192.168.1.1:8001,192.168.1.1:8002,192.168.1.1:8003 max-redirects: 2 lettuce: cluster: refresh: adaptive: true #10秒自动刷新一次 period: 10
当然拓扑刷新需要springboot升级,对现网的影响比较大,为了保障系统的稳定性可能还是不会大动干戈,所以此时采用jedis连接池的方案可能更好一点,jedis会自动拓扑刷新不用任何配置的,不过也需要加上cluster的重定向次数的配置(max-redirects)
spring: redis: cluster: nodes: 192.168.1.1:8001,192.168.1.1:8002,192.168.1.1:8003 max-redirects: 2 jedis: pool: max-active: 100 # 连接池最大连接数 max-wait: 3000 # 连接池最大阻塞等待时间 max-idle: 50 # 连接池中最大的空闲连接 min-idle: 10 # 连接池中最小的空闲连接 #time-between-eviction-runs: 60000 timeout: 500 # 连接超时时间