在最近的项目当中因为使用了类似字典表的数据所以我使用的 Spring Farmework 当中的 Cache 抽象,通过 Redis 来做为缓存。因为原有项目当中配置了 Redis,而且项目是基于 Spring Boot 构建的,并没有去除 Redis 的自动依赖(RedisAutoConfiguration
)。导致有些 Redis 的有些类是基于项目中自己配置的 RedisTemplate,而有些又是引用的 Spring Boot 中 Redis 的自动配置生成的 RedisTemplate。所以我就把 Spring Boot 中的 Redis 自动配置去除了。
去除 Spring Boot Redis 自动配置
@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
上午进行项目发布,在下午的时候运维就找到说今天上午发布的项目 Redis 连接数一直在添加。(平时这个项目的 Redis 连接数也小)。因为使用的是 AWS ,所以在 CloudWatch 上面可以看到 Redis 的连接数:
并且登陆跳板机到 Linux 服务器上也看到连接 Redis 的服务是早上我新发布的服务:
并且在 Redis 中使用 client list
也看到是上午启动的服务。
并且可以看到客户端请求的命令。而且服务当中也在报无法获取到 Redis 连接。
redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
这个很明显就是 Redis 连接无法获取,并且设置的 Redis 的最大连接数是 1000 所以 Redis 的连接数一直在增加。当在网上搜索 Redis 的连接没有被释放的时候搜索到了 RedisTemplate 设置了支持事务(enableTransactionSupport
) 为 true。那么这个 Redis 请求必须在 JDBC 的事务当中,要么就不会释放连接。果然手动配置的 RedisTemplate
设置了支持事务(默认为 false,不支持)。然后在测试环境把这个值设置成 false 并且压力测试如果没有导致 Redis 请求数飙升。
所以一直没有产生这种情况的原因是手动配置 Redis 配置文件并没有生效,使用的是 Spring Boot 的 Redis 的自动依赖。自动依赖并没有配置这个值为 true(默认为 false),所以连接就会释放。RedisTemplate
使用 Redis 连接池释放连接的 2 种情况是:
RedisTemplate
不支持事务,当使用完连接之后就会释放连接到Pool
当中RedisTemplate
支持事务,并且必须和 JDBC 在一个事务当中,标注了@Transactional
注解
下面我们就通过源码来分析,RedisTemplate
通过 opsForXXX
支持 Redis 中的各种数据操作,比如: opsForValue 操作 String 类型
、opsForSet 操作 Set 类型
等待。其实它最终会调用到:RedisTemplate#execute
。
RedisTemplate#execute
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");Assert.notNull(action, "Callback object must not be null");RedisConnectionFactory factory = getRequiredConnectionFactory();RedisConnection conn = null;try {
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronizationconn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);} else {
conn = RedisConnectionUtils.getConnection(factory);}boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);RedisConnection connToUse = preProcessConnection(conn, existingConnection);boolean pipelineStatus = connToUse.isPipelined();if (pipeline && !pipelineStatus) {
connToUse.openPipeline();}RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));T result = action.doInRedis(connToExpose);// close pipelineif (pipeline && !pipelineStatus) {
connToUse.closePipeline();}// TODO: any other connection processing?return postProcessResult(result, connToUse, existingConnection);} finally {
RedisConnectionUtils.releaseConnection(conn, factory);}}
上面代码的执行逻辑为:
- 获取 Redis 的连接工厂
RedisConnectionFactory
- 通过 boolean 类型值
enableTransactionSupport
判断是否支持事务,如果支持事务通过方法RedisConnectionUtils.bindConnection
获取并绑定RedisConnectionHolder
到本地线程,如果不支持事务就通过方法RedisConnectionUtils#getConnection
获取RedisConnectionHolder
。 - 调用 RedisTemplate 的前置方法
RedisTemplate#preProcessConnection
- 判断 RedisTemplate 是否使用了 pipeline,如果使用了调用
RedisConnection#openPipeline
- 如果
exposeConnection
直接使用创建的RedisConnection
,否则创建代理对象CloseSuppressingInvocationHandler
- 调用回调方法
RedisCallback#doInRedis
执行 Redis 命令 - 如果是
pipeline
就调用RedisConnection#closePipeline
关闭pipeline
。 - 最后在 finally 方法中调用
RedisConnectionUtils.releaseConnection
释放连接
RedisConnectionUtils#releaseConnection
RedisConnectionUtils#releaseConnection
public static void releaseConnection(@Nullable RedisConnection conn, RedisConnectionFactory factory) {
if (conn == null) {
return;}RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);if (connHolder != null && connHolder.isTransactionSyncronisationActive()) {
if (log.isDebugEnabled()) {
log.debug("Redis Connection will be closed when transaction finished.");}return;}// release transactional/read-only and non-transactional/non-bound connections.// transactional connections for read-only transactions get no synchronizer registeredif (isConnectionTransactional(conn, factory) && TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
unbindConnection(factory);} else if (!isConnectionTransactional(conn, factory)) {
if (log.isDebugEnabled()) {
log.debug("Closing Redis Connection");}conn.close();}}
上面的代码逻辑如下:
TransactionSynchronizationManager#getResource
获取RedisConnectionHolder
- 如果
RedisConnectionHolder
不为空并在事务处理当中直接返回 - 如果传入的
RedisConnection
在事务当中,并且事务为只读就释放连接 - 如果传入的
RedisConnection
不在事务当中,就调用RedisConnection#close
释放连接
可以看到如果 Redis 里面没有事务的时候就会直接调用 RedisConnection#close
把连接释放掉。当配置 RedisTemplate
的时候我看网上很多就是说在上面的第三步释放连接,这个说法是错误的。我把事务的条件都满足了都不会进行到第三步。这个问题困扰了我很久。ts
后面我自己 debug 解决了这个问题,答案就是在绑定并获取连接的时候会添加一个事务完成的回调方法,在这个回调方法当中释放 Redis 连接。
RedisConnectionUtils#bindConnection
在 RedisTemplate#execute 中,如果设置了 enableTransactionSupport 为 true,就会通过 RedisConnectionUtils#bindConnection
方法获取并绑定 Redis 连接。
RedisConnectionUtils#bindConnection
public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,boolean enableTransactionSupport) {
Assert.notNull(factory, "No RedisConnectionFactory specified");RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);if (connHolder != null) {
if (enableTransactionSupport) {
potentiallyRegisterTransactionSynchronisation(connHolder, factory);}return connHolder.getConnection();}if (!allowCreate) {
throw new IllegalArgumentException("No connection found and allowCreate = false");}if (log.isDebugEnabled()) {
log.debug("Opening RedisConnection");}RedisConnection conn = factory.getConnection();if (bind) {
RedisConnection connectionToBind = conn;if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
connectionToBind = createConnectionProxy(conn, factory);}connHolder = new RedisConnectionHolder(connectionToBind);TransactionSynchronizationManager.bindResource(factory, connHolder);if (enableTransactionSupport) {
potentiallyRegisterTransactionSynchronisation(connHolder, factory);}return connHolder.getConnection();}return conn;}
最终会调用到方法 RedisConnectionUtils#potentiallyRegisterTransactionSynchronisation
:
RedisConnectionUtils#potentiallyRegisterTransactionSynchronisation
private static void potentiallyRegisterTransactionSynchronisation(RedisConnectionHolder connHolder,final RedisConnectionFactory factory) {
if (isActualNonReadonlyTransactionActive()) {
if (!connHolder.isTransactionSyncronisationActive()) {
connHolder.setTransactionSyncronisationActive(true);RedisConnection conn = connHolder.getConnection();conn.multi();TransactionSynchronizationManager.registerSynchronization(new RedisTransactionSynchronizer(connHolder, conn, factory));}}}
上面的代码逻辑如下:
- 判断当前方法是否在一个数据库 JDBC 事务当中
- 判断当前
Redis 连接
是否新开启一个事务 - 调用
RedisConnection#multi
标记一个 Redis 事务块的开始 - 注册添加一个事务回调类
RedisTransactionSynchronizer
,它实现了TransactionSynchronization#afterCompletion
当数据库事务完成的时候会调用这个回调方法
TransactionSynchronization#afterCompletion
@Overridepublic void afterCompletion(int status) {
try {
switch (status) {
case TransactionSynchronization.STATUS_COMMITTED:connection.exec();break;case TransactionSynchronization.STATUS_ROLLED_BACK:case TransactionSynchronization.STATUS_UNKNOWN:default:connection.discard();}} finally {
if (log.isDebugEnabled()) {
log.debug("Closing bound connection after transaction completed with " + status);}connHolder.setTransactionSyncronisationActive(false);connection.close();TransactionSynchronizationManager.unbindResource(factory);}}
- 当事务完成之后会调用
RedisConnection#exec()
,用于执行所有事务块内的命令。 - 调用
RedisConnection#close
释放 Redis 连接 - 调用
TransactionSynchronizationManager.unbindResource
解绑ThreadLocal
中的 Redis 连接
下面就是我写的一个支持事务的 RedisTemplate
,它必须和数据 JDBC 事务一起使用。
Redis 相关的配置:
@Configuration
public class RedisConfig {@Bean(name = "redisConnectionFactory")public RedisConnectionFactory redisConnectionFactory() {JedisConnectionFactory factory = new JedisConnectionFactory(generatePoolConfig());factory.setHostName("localhost");factory.setPort(6379);factory.setUsePool(true);factory.setConvertPipelineAndTxResults(true);factory.afterPropertiesSet();return factory;}@Bean(name = "redisTemplate")public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {StringRedisTemplate template = new StringRedisTemplate();StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();template.setEnableTransactionSupport(true);template.setKeySerializer(stringRedisSerializer);template.setHashKeySerializer(stringRedisSerializer);template.setValueSerializer(stringRedisSerializer);template.setDefaultSerializer(stringRedisSerializer);template.setConnectionFactory(redisConnectionFactory);return template;}private JedisPoolConfig generatePoolConfig() {JedisPoolConfig poolConfig = new JedisPoolConfig();poolConfig.setMaxTotal(1000);poolConfig.setTestOnBorrow(true);return poolConfig;}}
JDBC 与 Redis 事务使用:
@Service("userService")
public class UserServiceImpl implements UserService {
@Resource(name = "redisTemplate")private RedisTemplate<String, String> redisTemplate;@Resourceprivate JdbcTemplate jdbcTemplate;@Override@Transactionalpublic void hello() {
jdbcTemplate.execute("select 1");redisTemplate.opsForValue().get("test");}
}
必须满足以下几个条件:
RedisTemplate
设置enableTransactionSupport
为true
,使用 Spring Redis 操作支付事务- 在方法上面添加
@Transactional
,并且配置@EnableTransactionManagement
支持数据库事务 - 使用数据连接操作,在这里面我使用的是最简单的
jdbcTemplate.execute("select 1")
执行数据库 ping 操作 - 使用支持事务的
RedisTemplate
进行 Redis 命令操作
满足上面的 4 点条件才会释放 Spring RedisTemplate
支持事务操作并且释放 Redis 连接,不然就不会释放 Redis 操作。
平常情况下不需要使用 RedisTemplate 的事务,直接使用默认值(不支持事务即可)
参考文章
- RedisTemplate开始事务支持后,调用没用注解@Transaction的方法,连接未释放(接上篇)