RedisTemplate setEnableTransactionSupport cause Redis Pool Connections leak
前言
使用原因
业务中存在mysql CUD+redis CUD。想要保证原子性就需要使mysql和redis在同一个事务中,因此才打算开启enableTransactionSupport=true。
redis本身不支持事务,靠的是spring framework trnsaction框架等执行完业务后,一起提交指令。
遇到问题
先在测试环境中开启,但是运行一段时间后,发现报Redis Pool Connections leak
,发现没使用事务的情况下没有释放连接,由于之前在其他项目中线上就开启过,因此猜测大概率是低版本的bug。
寻找issues
在github中找到一个一样的issues,已经close了,在2.5.0中已经解决。
我们使用的版本是:
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
到这一步就可以通过升级spring-data-redis版本去解决问题了,但是我们还是来阅读下源码,看看为啥没有释放,已经新版本中是怎么处理的。
没开启事务,为啥没释放
跟着时序图,进行debug就能发现问题了。
可以看到,其实没有开启事务其实不应该使用TransactionSynchronizationManager#bindResource进行绑定,就算绑定了也没关系,在releaseConnection中可以使用isTransactionSyncronisationActive进行判断,如果没有活跃的事务就应该进行释放
如果是我的话应该会选择在开始就不使用TransactionSynchronizationManager#bindResource进行绑定。
开启事务,释放
下面再看看开启事务是否会释放:
可以看到加了事务之后多了层代理,执行redis set判断有事务进行就直接return,同时会注册synchronization可以用于最终事务提交后通过TractionAspectSupport#commitTransactionAfterReturning调用触发releaseConnection。
新版本中的处理
根据issues,我们升级下spring-data-redis
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
根据报错:
Caused by: java.lang.NoClassDefFoundError: redis/clients/jedis/DefaultJedisClientConfig
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.<init>(JedisConnectionFactory.java:94)
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.<init>(JedisConnectionFactory.java:226)
at com.onepiece.cache.config.BaseJedisConnectionConfiguration.createJedisConnectionFactory(BaseJedisConnectionConfiguration.java:53)
at com.onepiece.cache.config.OnePieceJedisConnectionConfig.createStringRedisTemplateMap(OnePieceJedisConnectionConfig.java:31)
at com.onepiece.cache.config.OnePieceJedisConnectionConfig$$EnhancerBySpringCGLIB$$e406299d.CGLIB$createStringRedisTemplateMap$0(<generated>)
at com.onepiece.cache.config.OnePieceJedisConnectionConfig$$EnhancerBySpringCGLIB$$e406299d$$FastClassBySpringCGLIB$$e3a3361c.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244)
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331)
at com.onepiece.cache.config.OnePieceJedisConnectionConfig$$EnhancerBySpringCGLIB$$e406299d.createStringRedisTemplateMap(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
... 169 common frames omitted
Caused by: java.lang.ClassNotFoundException: redis.clients.jedis.DefaultJedisClientConfig
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
... 183 common frames omitted
发现需要升级jedis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.0</version>
</dependency>
根据报错:
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [java.util.Map]: Factory method 'createStringRedisTemplateMap' threw exception; nested exception is java.lang.NoClassDefFoundError: org/springframework/data/mapping/model/EntityInstantiators
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:651)
... 168 common frames omitted
Caused by: java.lang.NoClassDefFoundError: org/springframework/data/mapping/model/EntityInstantiators
at org.springframework.data.redis.core.convert.MappingRedisConverter.<init>(MappingRedisConverter.java:165)
at org.springframework.data.redis.core.convert.MappingRedisConverter.<init>(MappingRedisConverter.java:148)
at org.springframework.data.redis.hash.ObjectHashMapper.<init>(ObjectHashMapper.java:117)
at org.springframework.data.redis.hash.ObjectHashMapper.<init>(ObjectHashMapper.java:82)
at org.springframework.data.redis.hash.ObjectHashMapper.getSharedInstance(ObjectHashMapper.java:142)
at org.springframework.data.redis.core.RedisTemplate.<init>(RedisTemplate.java:110)
at org.springframework.data.redis.core.StringRedisTemplate.<init>(StringRedisTemplate.java:42)
at com.onepiece.cache.config.BaseJedisConnectionConfiguration.createStringRedisTemplate(BaseJedisConnectionConfiguration.java:35)
at com.onepiece.cache.config.OnePieceJedisConnectionConfig.createStringRedisTemplateMap(OnePieceJedisConnectionConfig.java:32)
at com.onepiece.cache.config.OnePieceJedisConnectionConfig$$EnhancerBySpringCGLIB$$6d178c64.CGLIB$createStringRedisTemplateMap$1(<generated>)
at com.onepiece.cache.config.OnePieceJedisConnectionConfig$$EnhancerBySpringCGLIB$$6d178c64$$FastClassBySpringCGLIB$$be3d5cd2.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244)
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331)
at com.onepiece.cache.config.OnePieceJedisConnectionConfig$$EnhancerBySpringCGLIB$$6d178c64.createStringRedisTemplateMap(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
... 169 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.springframework.data.mapping.model.EntityInstantiators
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
... 188 common frames omitted
发现要升级spring-data-commons:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.5.0</version>
</dependency>
发现还是报错:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
org.springframework.data.repository.config.RepositoryConfigurationDelegate.getStartup(RepositoryConfigurationDelegate.java:271)
The following method did not exist:
org.springframework.beans.factory.config.ConfigurableBeanFactory.getApplicationStartup()Lorg/springframework/core/metrics/ApplicationStartup;
The method's class, org.springframework.beans.factory.config.ConfigurableBeanFactory, is available from the following locations:
jar:file:/Users/wanghaifeng/.m2/repository/org/springframework/spring-beans/5.2.4.RELEASE/spring-beans-5.2.4.RELEASE.jar!/org/springframework/beans/factory/config/ConfigurableBeanFactory.class
It was loaded from the following location:
file:/Users/wanghaifeng/.m2/repository/org/springframework/spring-beans/5.2.4.RELEASE/spring-beans-5.2.4.RELEASE.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of org.springframework.beans.factory.config.ConfigurableBeanFactory
感觉是要对整个spring版本升级了,项目有计划升级3.0,因此还是先简单的看看源码的处理:
RedisConnectionUtils#doGetConnection
public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
boolean transactionSupport) {
Assert.notNull(factory, "No RedisConnectionFactory specified");
RedisConnectionHolder conHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
log.debug("Fetching resumed Redis Connection from RedisConnectionFactory");
conHolder.setConnection(fetchConnection(factory));
}
return conHolder.getRequiredConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
if (!allowCreate) {
throw new IllegalArgumentException("No connection found and allowCreate = false");
}
log.debug("Fetching Redis Connection from RedisConnectionFactory");
RedisConnection connection = fetchConnection(factory);
boolean bindSynchronization = TransactionSynchronizationManager.isActualTransactionActive() && transactionSupport;
//可以看到绑定需要当前存在事务
if (bind || bindSynchronization) {
if (bindSynchronization && isActualNonReadonlyTransactionActive()) {
connection = createConnectionSplittingProxy(connection, factory);
}
try {
// Use same RedisConnection for further Redis actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
RedisConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new RedisConnectionHolder(connection);
} else {
holderToUse.setConnection(connection);
}
holderToUse.requested();
// Consider callback-scope connection binding vs. transaction scope binding
if (bindSynchronization) {
potentiallyRegisterTransactionSynchronisation(holderToUse, factory);
}
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(factory, holderToUse);
}
} catch (RuntimeException ex) {
// Unexpected exception from external delegation call -> close Connection and rethrow.
releaseConnection(connection, factory);
throw ex;
}
return connection;
}
return connection;
}
看看新增的注释。
RedisConnectionUtils#releaseConnection
public static void releaseConnection(@Nullable RedisConnection conn, RedisConnectionFactory factory) {
if (conn == null) {
return;
}
RedisConnectionHolder conHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
if (conHolder != null) {
if (conHolder.isTransactionActive()) {
if (connectionEquals(conHolder, conn)) {
if (log.isDebugEnabled()) {
log.debug("RedisConnection will be closed when transaction finished.");
}
// It's the transactional Connection: Don't close it.
conHolder.released();
}
return;
}
// release transactional/read-only and non-transactional/non-bound connections.
// transactional connections for read-only transactions get no synchronizer registered
unbindConnection(factory);
return;
}
doCloseConnection(conn);
}
因为get的时候没bind,因此直接走释放了。
转载自:https://juejin.cn/post/7215528978726944824