1 问题
Redis多线程读取数据,使用Jedis连接池并正常关闭Redis实例,
当获取Redis实例时,服务“卡死”,即无法从Redis正常获取数据。
2 场景复现
2.1 准备条件
- Jedis连接池最大连接数设置为1;
- 延迟10秒释放Redis实例资源;
- 通过getResource获取Redis实例;
- 开启10个线程从Redis读取数据。
2.2 测试样例
package com.monkey.java_study.redis;
import com.monkey.java_study.common.config.ThreadPoolConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* Redis测试样例.
*
* @author xindaqi
* @date 2021-07-09 23:45
*/
public class RedisPoolTest {
private static final Logger logger = LogManager.getLogger(RedisPoolTest.class);
public static String readRedis(JedisPool jedisPool) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.set("uid", "123456");
String value = jedis.get("uid");
// 延迟10秒释放实例资源
Thread.sleep(10000);
return value;
} catch (Exception e) {
logger.info("Redis处理异常:", e);
return null;
}
}
public static void main(String[] args) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// Jedis池:最大连接数
jedisPoolConfig.setMaxTotal(1);
// Jedis池:最大空闲连接数
jedisPoolConfig.setMaxIdle(10);
// Jedis池:连接Redis超时时间
int connectTimeout = 2000;
// 创建连接池
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "localhost", 6379, connectTimeout, "123456", 0);
try {
for (int i = 0; i < 10; i++) {
ThreadPoolConfig.threadPoolExecutorGenerate.submit(() -> {
String value = readRedis(jedisPool);
logger.info(">>>>>>>>Value from redis:{}", value);
});
}
} catch (Exception ex) {
throw new RuntimeException(ex);
} finally {
ThreadPoolConfig.threadPoolExecutorGenerate.shutdown();
}
}
}
2.3 测试结果
测试结果如图2.1所示。Jedis连接池配置1个实例,开启10个线程从Redis读取数据,每次读取间隔10秒释放实例资源,因此,每次获取结果是串行的,一个线程读取Redis时,耗时10秒钟释放资源,另外一个线程才能获取Redis实例,即复现了多线程阻塞,在后台服务中,并发时出现接口无响应的问题。
3 方案
配置连接池的最大等待时间,当超过最大等待时间后,抛出异常,捕获异常后,返回提示信息,并释放Redis实例,为下一次查询提供释放资源。
3.1 测试样例
package com.monkey.java_study.redis;
import com.monkey.java_study.common.config.ThreadPoolConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* Redis测试样例.
*
* @author xindaqi
* @date 2021-07-09 23:45
*/
public class RedisPoolTest {
private static final Logger logger = LogManager.getLogger(RedisPoolTest.class);
public static String readRedis(JedisPool jedisPool) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.set("uid", "123456");
String value = jedis.get("uid");
// 延迟10秒释放实例资源
Thread.sleep(10000);
return value;
} catch (Exception e) {
logger.info("Redis处理异常:", e);
return null;
}
}
public static void main(String[] args) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// Jedis池:最大连接数
jedisPoolConfig.setMaxTotal(1);
// Jedis池:最大空闲连接数
jedisPoolConfig.setMaxIdle(10);
// Jedis池:等待时间
jedisPoolConfig.setMaxWaitMillis(3000);
// Jedis池:连接Redis超时时间
int connectTimeout = 2000;
// 创建连接池
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "localhost", 6379, connectTimeout, "123456", 0);
try {
for (int i = 0; i < 10; i++) {
ThreadPoolConfig.threadPoolExecutorGenerate.submit(() -> {
String value = readRedis(jedisPool);
logger.info(">>>>>>>>Value from redis:{}", value);
});
}
} catch (Exception ex) {
throw new RuntimeException(ex);
} finally {
ThreadPoolConfig.threadPoolExecutorGenerate.shutdown();
}
}
}
3.2 测试结果
测试结果如3.1所示。当出现阻塞时,抛出异常,捕获异常,进一步处理,保证服务在配置的时间内可以拿到可读的数据。
4 异常信息
redis.clients.jedis.exceptions.JedisExhaustedPoolException: Could not get a resource since the pool is exhausted
at redis.clients.jedis.util.Pool.getResource(Pool.java:53) ~[jedis-3.5.1.jar:?]
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:330) ~[jedis-3.5.1.jar:?]
无法获取Redis实例资源。
- 原因:
资源耗尽。 - 方案:
等待下一次,或者提高连接池最大连接数。
5 小结
多线程使用Redis,避免阻塞:
- 使用连接池;
- 配置连接池最大等待时间;
- 及时释放Redis实例资源:try-with-resource;
- 捕获异常,并返回可读的提示信息。