redis事務需要注意的坑------RedisConnectionFailureException


原文地址,轉載請注明出處:https://blog.csdn.net/qq_34021712/article/details/79606551   ©王賽超

之前spring整合redis開啟事務,在功能測試環境下跑了N天之后,突然發現服務異常,查看日志報異常的具體內容如下:

  1.  
    org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
  2.  
    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java: 204)
  3.  
    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java: 348)
  4.  
    at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java: 129)
  5.  
    at org.springframework.data.redis.core.RedisConnectionUtils.bindConnection(RedisConnectionUtils.java: 67)
  6.  
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java: 192)
  7.  
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java: 169)
  8.  
    at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java: 91)
  9.  
    at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java: 43)
  10.  
    at com.ruubypay.miss.global.utils.RedisUtil.get(RedisUtil.java: 80)
  11.  
    at com.ruubypay.miss.riskcontrol.interfaces.service.impl.LoginInfoServiceImpl.verifyAuthFalse(LoginInfoServiceImpl.java: 268)
  12.  
    at com.ruubypay.miss.riskcontrol.interfaces.service.impl.LoginInfoServiceImpl.getLoginStatus(LoginInfoServiceImpl.java: 81)
  13.  
    at com.ruubypay.miss.riskcontrol.interfaces.service.impl.LoginInfoServiceImpl$$FastClassBySpringCGLIB$$ 8a83fccf.invoke(<generated>)
  14.  
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java: 204)
  15.  
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java: 738)
  16.  
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java: 157)
  17.  
    at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java: 52)
  18.  
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java: 179)
  19.  
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java: 97)
  20.  
    at com.ruubypay.miss.riskcontrol.config.SpringAOP.timeAround(SpringAOP.java: 90)
  21.  
    at sun.reflect.GeneratedMethodAccessor82.invoke(Unknown Source)
  22.  
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java: 43)
  23.  
    at java.lang.reflect.Method.invoke(Method.java: 498)
  24.  
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java: 629)
  25.  
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java: 618)
  26.  
    at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java: 70)
  27.  
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java: 179)
  28.  
    at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java: 52)
  29.  
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java: 179)
  30.  
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java: 92)
  31.  
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java: 179)
  32.  
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java: 673)
  33.  
    at com.ruubypay.miss.riskcontrol.interfaces.service.impl.LoginInfoServiceImpl$$EnhancerBySpringCGLIB$$ 57c35955.getLoginStatus(<generated>)
  34.  
    at com.alibaba.dubbo.common.bytecode.Wrapper2.invokeMethod(Wrapper2.java)
  35.  
    at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$ 1.doInvoke(JavassistProxyFactory.java:46)
  36.  
    at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java: 72)
  37.  
    at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java: 53)
  38.  
    at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java: 64)
  39.  
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$ 1.invoke(ProtocolFilterWrapper.java:91)
  40.  
    at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java: 75)
  41.  
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$ 1.invoke(ProtocolFilterWrapper.java:91)
  42.  
    at com.alibaba.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java: 42)
  43.  
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$ 1.invoke(ProtocolFilterWrapper.java:91)
  44.  
    at com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java: 78)
  45.  
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$ 1.invoke(ProtocolFilterWrapper.java:91)
  46.  
    at com.alibaba.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java: 70)
  47.  
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$ 1.invoke(ProtocolFilterWrapper.java:91)
  48.  
    at com.alibaba.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java: 132)
  49.  
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$ 1.invoke(ProtocolFilterWrapper.java:91)
  50.  
    at com.alibaba.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java: 38)
  51.  
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$ 1.invoke(ProtocolFilterWrapper.java:91)
  52.  
    at com.alibaba.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java: 38)
  53.  
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$ 1.invoke(ProtocolFilterWrapper.java:91)
  54.  
    at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol$ 1.reply(DubboProtocol.java:113)
  55.  
    at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java: 84)
  56.  
    at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java: 170)
  57.  
    at com.alibaba.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java: 52)
  58.  
    at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java: 82)
  59.  
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java: 1149)
  60.  
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java: 624)
  61.  
    at java.lang.Thread.run(Thread.java: 748)
  62.  
    Caused by: redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
  63.  
    at redis.clients.util.Pool.getResource(Pool.java: 51)
  64.  
    at redis.clients.jedis.JedisPool.getResource(JedisPool.java: 226)
  65.  
    at redis.clients.jedis.JedisPool.getResource(JedisPool.java: 16)
  66.  
    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java: 194)
  67.  
    ... 59 common frames omitted
  68.  
    Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
  69.  
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java: 449)
  70.  
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java: 363)
  71.  
    at redis.clients.util.Pool.getResource(Pool.java: 49)
  72.  
    ... 62 common frames omitted

無法從連接池中獲取連接,環境配置連接數是1000,查了一下配置,發現跟別的項目唯一不同的地方就是該redis配置中對redis開啟了事務。

初步預想

難道是redis連接用完沒有釋放嗎?還是自己redis用的方式不對?

查看官方redis的文檔:https://docs.spring.io/spring-data/redis/docs/1.8.8.RELEASE/reference/html/#tx.spring

  1.  
    5.10.1. @Transactional Support
  2.  
    Transaction Support is disabled by default and has to be explicitly enabled for each RedisTemplate in use by setting setEnableTransactionSupport(true). This will force binding the RedisConnection in use to the current Thread triggering MULTI. If the transaction finishes without errors, EXEC is called, otherwise DISCARD. Once in MULTI, RedisConnection would queue write operations, all readonly operations, such as KEYS are piped to a fresh (non thread bound) RedisConnection.

文檔中也沒多少東西,redis開啟事務設置setEnableTransactionSupport(true).要在 添加了@Transactional注解的方法上,由於方法沒必要開啟事務,所以方法上沒有加事務注解。難道是因為這個原因嗎?

寫了一段代碼進行測試

在service的一個方法上沒有加@Transactional注解,然后在使用redisTemplate之后,搞一個異常,代碼如下:

  1.  
    Object o = redisTemplate.opsForValue().get( "6666");
  2.  
    int a = 10/0;

結果很快就報異常了,異常信息如下:

開啟debug過程

①首先我們定位到代碼RedisTemplate.execute方法,源代碼如下:

  1.  
    public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
  2.  
    Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
  3.  
    Assert.notNull(action, "Callback object must not be null");
  4.  
     
  5.  
     
  6.  
    RedisConnectionFactory factory = getConnectionFactory();
  7.  
    RedisConnection conn = null;
  8.  
    try {
  9.  
     
  10.  
     
  11.  
    //這里判斷redis的EnableTransactionSupport是否為true,如果為true將連接綁定到當前線程
  12.  
    if (enableTransactionSupport) {
  13.  
    // only bind resources in case of potential transaction synchronization
  14.  
    conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
  15.  
    } else {
  16.  
    //如果沒有開啟事務,直接獲取一個連接
  17.  
    conn = RedisConnectionUtils.getConnection(factory);
  18.  
    }
  19.  
     
  20.  
     
  21.  
    //獲取當前線程綁定的連接,如果開啟事務,也就是上面的bindConnection(factory, enableTransactionSupport)代碼執行時那個連接
  22.  
    boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
  23.  
     
  24.  
     
  25.  
    RedisConnection connToUse = preProcessConnection(conn, existingConnection);
  26.  
     
  27.  
     
  28.  
    boolean pipelineStatus = connToUse.isPipelined();
  29.  
    if (pipeline && !pipelineStatus) {
  30.  
    connToUse.openPipeline();
  31.  
    }
  32.  
     
  33.  
     
  34.  
    RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
  35.  
     
  36.  
     
  37.  
    //從redis中獲取值
  38.  
    T result = action.doInRedis(connToExpose);
  39.  
     
  40.  
     
  41.  
    // close pipeline
  42.  
    if (pipeline && !pipelineStatus) {
  43.  
    connToUse.closePipeline();
  44.  
    }
  45.  
     
  46.  
     
  47.  
    // TODO: any other connection processing?
  48.  
    return postProcessResult(result, connToUse, existingConnection);
  49.  
    } finally {
  50.  
    //最后釋放連接
  51.  
    RedisConnectionUtils.releaseConnection(conn, factory);
  52.  
    }
  53.  
    }

②因為將EnableTransactionSupport設置為true,所以我們定位到conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);這行代碼,源代碼如下:

  1.  
    public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
  2.  
    boolean enableTransactionSupport) {
  3.  
     
  4.  
     
  5.  
    Assert.notNull(factory, "No RedisConnectionFactory specified");
  6.  
     
  7.  
     
  8.  
    //這里拿到的是一個null值
  9.  
    RedisConnectionUtils.RedisConnectionHolder connHolder = (RedisConnectionUtils.RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
  10.  
     
  11.  
     
  12.  
    if (connHolder != null) {
  13.  
    if (enableTransactionSupport) {
  14.  
    potentiallyRegisterTransactionSynchronisation(connHolder, factory);
  15.  
    }
  16.  
    return connHolder.getConnection();
  17.  
    }
  18.  
     
  19.  
     
  20.  
    if (!allowCreate) {
  21.  
    throw new IllegalArgumentException("No connection found and allowCreate = false");
  22.  
    }
  23.  
     
  24.  
     
  25.  
    if (log.isDebugEnabled()) {
  26.  
    log.debug( "Opening RedisConnection");
  27.  
    }
  28.  
     
  29.  
     
  30.  
    //在這里獲取一個連接
  31.  
    RedisConnection conn = factory.getConnection();
  32.  
     
  33.  
     
  34.  
    if (bind) {
  35.  
     
  36.  
     
  37.  
    RedisConnection connectionToBind = conn;
  38.  
    if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
  39.  
    connectionToBind = createConnectionProxy(conn, factory);
  40.  
    }
  41.  
     
  42.  
     
  43.  
    connHolder = new RedisConnectionUtils.RedisConnectionHolder(connectionToBind);
  44.  
     
  45.  
     
  46.  
    //這里將代碼綁定到當前線程,內部使用ThreadLocal實現
  47.  
    TransactionSynchronizationManager.bindResource(factory, connHolder);
  48.  
    if (enableTransactionSupport) {
  49.  
    //這個方法是如果加了@Transactional注解,幫你開啟 multi
  50.  
    potentiallyRegisterTransactionSynchronisation(connHolder, factory);
  51.  
    }
  52.  
     
  53.  
     
  54.  
    //這里將當前線程綁定的連接返回
  55.  
    return connHolder.getConnection();
  56.  
    }
  57.  
     
  58.  
     
  59.  
    return conn;
  60.  
    }

③將代碼定位到TransactionSynchronizationManager.bindResource(factory, connHolder);這一行,內部使用ThreadLocal實現,源代碼如下:

  1.  
    public static void bindResource(Object key, Object value) throws IllegalStateException {
  2.  
    Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
  3.  
    Assert.notNull(value, "Value must not be null");
  4.  
    Map<Object, Object> map = resources.get();
  5.  
    // set ThreadLocal Map if none found
  6.  
    if (map == null) {
  7.  
    map = new HashMap<Object, Object>();
  8.  
    resources.set(map);
  9.  
    }
  10.  
    Object oldValue = map.put(actualKey, value);
  11.  
    // Transparently suppress a ResourceHolder that was marked as void...
  12.  
    if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
  13.  
    oldValue = null;
  14.  
    }
  15.  
    if (oldValue != null) {
  16.  
    throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
  17.  
    actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
  18.  
    }
  19.  
    if (logger.isTraceEnabled()) {
  20.  
    logger.trace( "Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
  21.  
    Thread.currentThread().getName() + "]");
  22.  
    }
  23.  
    }

這里的resources就是ThreadLocal

④再將代碼定位到第二步中的potentiallyRegisterTransactionSynchronisation(connHolder, factory);內部會判斷是不是在一個事務中,如果在一個事務中,它幫你開啟multi,但是這個方法里面的代碼並沒有執行,因為沒有加@Transactional注解,根本就沒在一個事務中,源代碼如下:

  1.  
    private static void potentiallyRegisterTransactionSynchronisation(RedisConnectionUtils.RedisConnectionHolder connHolder,
  2.  
    final RedisConnectionFactory factory) {
  3.  
     
  4.  
     
  5.  
    //這里的isActualNonReadonlyTransactionActive()方法判斷是否在一個事務中,debug並沒有進這段代碼,因為沒有加@Transactional注解
  6.  
    if (isActualNonReadonlyTransactionActive()) {
  7.  
     
  8.  
     
  9.  
    if (!connHolder.isTransactionSyncronisationActive()) {
  10.  
    connHolder.setTransactionSyncronisationActive( true);
  11.  
     
  12.  
     
  13.  
    RedisConnection conn = connHolder.getConnection();
  14.  
    conn.multi();
  15.  
     
  16.  
     
  17.  
    TransactionSynchronizationManager.registerSynchronization( new RedisConnectionUtils.RedisTransactionSynchronizer(connHolder, conn,
  18.  
    factory));
  19.  
    }
  20.  
    }
  21.  
    }

到這里,以上就是開啟連接,並從redis中獲取屬性的過程,因為沒有加@Transactional注解,所以並沒有開啟multi,命令也沒有進入Queue中,可以直接拿到redis中的值。但是, 還是進行了 將連接綁定到當前線程的操作。下面我們看一下關閉連接的過程。

⑤ 將代碼定位到第一步RedisConnectionUtils.releaseConnection(conn, factory);源代碼如下:

  1.  
    public static void releaseConnection(RedisConnection conn, RedisConnectionFactory factory) {
  2.  
     
  3.  
     
  4.  
    if (conn == null) {
  5.  
    return;
  6.  
    }
  7.  
     
  8.  
     
  9.  
    RedisConnectionUtils.RedisConnectionHolder connHolder = (RedisConnectionUtils.RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
  10.  
     
  11.  
     
  12.  
    //可以獲取到connHolder 但是 connHolder.isTransactionSyncronisationActive() 卻是false,因為之前綁定連接的時候,並沒有在一個事務中,連接綁定了,但是isTransactionSyncronisationActive屬性並沒有給值
  13.  
    //忘記的朋友可以看一下 第四步 potentiallyRegisterTransactionSynchronisation 中的代碼,其實 是沒有執行的,所以 isTransactionSyncronisationActive 的默認值是false
  14.  
    if (connHolder != null && connHolder.isTransactionSyncronisationActive()) {
  15.  
    if (log.isDebugEnabled()) {
  16.  
    log.debug( "Redis Connection will be closed when transaction finished.");
  17.  
    }
  18.  
    return;
  19.  
    }
  20.  
     
  21.  
     
  22.  
    // release transactional/read-only and non-transactional/non-bound connections.
  23.  
    // transactional connections for read-only transactions get no synchronizer registered
  24.  
    //第一個條件判斷為true 但是第二個條件判斷為false 不是一個只讀事務,所以unbindConnection(factory) 代碼沒有執行
  25.  
    if (isConnectionTransactional(conn, factory)
  26.  
    && TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
  27.  
    unbindConnection(factory);
  28.  
    //然后 else if 也是返回false 因為 isConnectionTransactional(conn, factory)返回的是true 內部代碼判斷連接這個連接和 線程中綁定的連接是不是同一個,是同一個 由於前面加了一個 ! 號 所以結果為false
  29.  
    } else if (!isConnectionTransactional(conn, factory)) {
  30.  
    if (log.isDebugEnabled()) {
  31.  
    log.debug( "Closing Redis Connection");
  32.  
    }
  33.  
    conn.close();
  34.  
    }
  35.  
    }

 

 

第一個if中,條件(isConnectionTransactional(conn, factory)判斷為true 但是第二個條件

TransactionSynchronizationManager.isCurrentTransactionReadOnly()判斷為false 不是一個只讀事務,所以unbindConnection(factory) 代碼沒有執行

第二個else if中 isConnectionTransactional(conn, factory) 判斷 傳過來的這個連接是不是 線程綁定的連接,如果是返回true 結果是同一個連接,返回了 true  但是由於前面加了一個 ! 號 true變 false  代碼conn.close(); 並沒有執行,然后你會很操.蛋的發現,方法就這樣過去了,結束了,連接沒有關閉,也沒有從當前線程上釋放。

解決方案

以上是debug的過程,那既然發現了問題,就要解決問題既然它沒有執行釋放的動作,那我們幫他執行就好了。繼續閱讀TransactionSynchronizationManager的源碼,發現有TransactionSynchronizationManager.unbindResource(factory);這個方法,這個方法的內部就是將資源釋放,最后執行方法的源代碼如下:

  1.  
    private static Object doUnbindResource(Object actualKey) {
  2.  
    Map<Object, Object> map = resources.get();
  3.  
    if (map == null) {
  4.  
    return null;
  5.  
    }
  6.  
    Object value = map.remove(actualKey);
  7.  
    // Remove entire ThreadLocal if empty...
  8.  
    if (map.isEmpty()) {
  9.  
    resources.remove();
  10.  
    }
  11.  
    // Transparently suppress a ResourceHolder that was marked as void...
  12.  
    if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
  13.  
    value = null;
  14.  
    }
  15.  
    if (value != null && logger.isTraceEnabled()) {
  16.  
    logger.trace( "Removed value [" + value + "] for key [" + actualKey + "] from thread [" +
  17.  
    Thread.currentThread().getName() + "]");
  18.  
    }
  19.  
    return value;
  20.  
    }

如果你的redisTemplate開啟了事務,在未標明@Transactional的方法內使用時,可以在redisTemplate操作redis之后立馬調用該方法,具體代碼如下:

  1.  
    /**
  2.  
    * 普通緩存獲取
  3.  
    * @param key 鍵
  4.  
    * @return 值
  5.  
    */
  6.  
    public Object get(String key){
  7.  
    Object o = redisTemplate.opsForValue().get(key);
  8.  
    TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());
  9.  
    return o;
  10.  
    }

到這里可能有個疑問,那@Transactional注解是怎么釋放連接的呢?

不妨在方法上添加該注解之后,再次debug查看代碼,這里就不發各個方法的源代碼了,有興趣的朋友自己debug一下立馬明白,我們發現最終的代碼執行流程如下:

  1.  
    org.springframework.data.redis.core.RedisConnectionUtils$RedisTransactionSynchronizer.afterCompletion(RedisConnectionUtils.java: 292)
  2.  
    org.springframework.transaction.support.TransactionSynchronizationUtils.invokeAfterCompletion(TransactionSynchronizationUtils.java: 165)
  3.  
    org.springframework.transaction.support.AbstractPlatformTransactionManager.invokeAfterCompletion(AbstractPlatformTransactionManager.java: 1002)
  4.  
    org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerAfterCompletion(AbstractPlatformTransactionManager.java: 968)
  5.  
    org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java: 741)
  6.  
    org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java: 703)
  7.  
    org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java: 500)
  8.  
    org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java: 282)
  9.  
    org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java: 96)
  10.  
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java: 179)
  11.  
    org.springframework.aop.framework.CglibAopProxy.intercept(CglibAopProxy.java: 673)

可以直接按照我說的方法處打斷點驗證一下,最后我們發現TransactionSynchronizationUtils.invokeAfterCompletion 這個方法,這個方法是關鍵啊,我們可以看一下它的源碼,源碼如下:

  1.  
    public static void invokeAfterCompletion(List<TransactionSynchronization> synchronizations, int completionStatus) {
  2.  
    if (synchronizations != null) {
  3.  
    for (TransactionSynchronization synchronization : synchronizations) {
  4.  
    try {
  5.  
    synchronization.afterCompletion(completionStatus);
  6.  
    }
  7.  
    catch (Throwable tsex) {
  8.  
    logger.error( "TransactionSynchronization.afterCompletion threw exception", tsex);
  9.  
    }
  10.  
    }
  11.  
    }
  12.  
    }

遍歷得到每一個TransactionSynchronization,然后調用它的afterCompletion方法,TransactionSynchronization是一個接口,實現它的有很多,像jdbc數據源釋放 什么的都是通過該方法一次性釋放

我們可以看一下RedisTransactionSynchronizer 的 afterCompletion方法,在此方法內提交或回滾事務,並最終釋放連接,源代碼如下:

  1.  
    public void afterCompletion(int status) {
  2.  
     
  3.  
    try {
  4.  
    switch (status) {
  5.  
    //如果事務正常,最終提交事務
  6.  
    case TransactionSynchronization.STATUS_COMMITTED:
  7.  
    connection.exec();
  8.  
    break;
  9.  
    //如果有異常,事務回滾
  10.  
    case TransactionSynchronization.STATUS_ROLLED_BACK:
  11.  
    case TransactionSynchronization.STATUS_UNKNOWN:
  12.  
    default:
  13.  
    connection.discard();
  14.  
    }
  15.  
    } finally {
  16.  
     
  17.  
    if (log.isDebugEnabled()) {
  18.  
    log.debug( "Closing bound connection after transaction completed with " + status);
  19.  
    }
  20.  
     
  21.  
    connHolder.setTransactionSyncronisationActive( false);
  22.  
    //關閉連接
  23.  
    connection.close();
  24.  
    //從當前線程釋放連接
  25.  
    TransactionSynchronizationManager.unbindResource(factory);
  26.  
    }
  27.  
    }

redisTemplate開啟事務的另一個坑

也就是上面提到過得,將命令放到Queue中,代碼片段如下:

  1.  
    Long increment = redisTemplate.opsForValue().increment( "6666", 1);
  2.  
    System.out.println(increment);

是無法拿到返回值的,是因為redis在MULTI/EXEC代碼塊中,命令都會被delay,放入Queue中,而不會直接返回對應的值。即例子中的increment自增,自增命令執行完成,默認是會返回自增之后的值,但是卻是返回了null.

redisTemplate正確使用
其實,別人之前也有遇到過這個問題,A transaction pitfall in Spring Data Redis
我在 Spring 的 RedisTemplate 類 redisTemplate.setEnableTransactionSupport(true); 中啟用 Redis 事務時得到一個慘痛的教訓:Redis 會在運行幾天后開始返回垃圾數據,導致數據嚴重損壞。StackOverflow上也報道了類似情況。
在運行一個 monitor 命令后,我的團隊發現,在進行 Redis 操作或 RedisCallback 后,Spring 並沒有自動關閉 Redis 連接,而事實上它是應該關閉的。如果再次使用未關閉的連接,可能會從意想不到的 Redis 密鑰返回垃圾數據。有意思的是,如果在 RedisTemplate 中把事務支持設為 false,這一問題就不會出現了。
我們發現,我們可以先在 Spring 語境里配置一個 PlatformTransactionManager(例如 DataSourceTransactionManager),然后再用 @Transactional 注釋來聲明 Redis 事務的范圍,讓 Spring 自動關閉 Redis 連接。
根據這一經驗,我們相信,在 Spring 里配置兩個單獨的 RedisTemplate 是很好的做法:其中一個 RedisTemplates 的事務設為 false,用於大多數 Redis 操作,另一個 RedisTemplates 的事務已激活,僅用於 Redis 事務。當然必須要聲明 PlatformTransactionManager 和 @Transactional,以防返回垃圾數值。
另外,我們還發現了 Redis 事務和關系數據庫事務(在本例中,即 JDBC)相結合的不利之處。混合型事務的表現和預想的不太一樣。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM