likes
comments
collection
share

RedisTemplate setEnableTransactionSupport cause Redis Pool Connections leak

作者站长头像
站长
· 阅读数 15

前言

使用原因

业务中存在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版本去解决问题了,但是我们还是来阅读下源码,看看为啥没有释放,已经新版本中是怎么处理的。

没开启事务,为啥没释放

RedisTemplate setEnableTransactionSupport cause Redis Pool Connections leak 跟着时序图,进行debug就能发现问题了。

可以看到,其实没有开启事务其实不应该使用TransactionSynchronizationManager#bindResource进行绑定,就算绑定了也没关系,在releaseConnection中可以使用isTransactionSyncronisationActive进行判断,如果没有活跃的事务就应该进行释放

如果是我的话应该会选择在开始就不使用TransactionSynchronizationManager#bindResource进行绑定。

开启事务,释放

下面再看看开启事务是否会释放:

RedisTemplate setEnableTransactionSupport cause Redis Pool Connections leak

可以看到加了事务之后多了层代理,执行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
评论
请登录