天天看點

Sping Data Redis 使用事務時,不關閉連接配接的問題

 項目中使用到了Redis,架構為springMVC+tomcat+Redis+Mysql

最後決定用spring-data-redis來開發,配置好連接配接池,進入使用,似乎一切正常。

 配置了兩塊redis,一個專門做讀,一個專門做些, 配置的XML檔案如下,這是一個專做寫的redis配置:

<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxIdle" value="${redis.maxIdle}" />
		<property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
		<property name="maxTotal" value="${redis.maxTotal}"></property>
		<property name="testOnBorrow" value="${redis.testOnBorrow}" />
		<property name="minIdle" value="${redis.minIdle}"></property>
		<property name="timeBetweenEvictionRunsMillis" value="60000"></property>
		<property name="minEvictableIdleTimeMillis" value="1800000"></property>
		<property name="numTestsPerEvictionRun" value="3"></property>
	</bean>
	<bean id="connectionFactory"
		class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
		p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}"
		p:pool-config-ref="poolConfig">
	</bean>

	<!-- 配置redisTemplate -->
	<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
		<property name="connectionFactory" ref="connectionFactory" />
		<property name="keySerializer">
			<bean
				class="org.springframework.data.redis.serializer.StringRedisSerializer" />
		</property>
		<!-- 采用json序列化 -->
		<property name="valueSerializer">
			<bean
				class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer">
			</bean>
		</property>
		<!-- 開啟REIDS事務支援 -->
		<property name="enableTransactionSupport" value="true" /> 
	</bean>
           

配置好以後編碼使用,發現一切正常。但是有一次一個現象引起了注意,同僚在編碼的時候,出現了“無法從連接配接池擷取redis連接配接“的錯誤(Could not get a resource conneciton from the pool)。 于是調用端口檢視,發現連接配接池被占滿了,redisTemplate似乎沒有釋放掉連接配接,于是進一步檢視,發現隻有啟用了事務的連接配接沒有被釋放,記得以前聽人說過 exec和discard會自動關閉連接配接,于是往配置上去尋找問題,多番尋找和嘗試無果,後來又有人說據說用@transactional或spring的事務AOP可以控制和傳播事務,并控制連接配接,但是我們的需求是自己做,是以隻好去看redisTemplate源碼。從exec處入手,很快找到了最終執行的源碼,RedisTemplate的所有操作執行,大部分都是通過這個方法來回調action執行的,大家可以感受感受:

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 = getConnectionFactory();
		RedisConnection conn = null;
		try {

			if (enableTransactionSupport) {
				// only bind resources in case of potential transaction synchronization
				conn = 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 pipeline
			if (pipeline && !pipelineStatus) {
				connToUse.closePipeline();
			}

			// TODO: any other connection processing?
			return postProcessResult(result, connToUse, existingConnection);
		} finally {
<span style="white-space:pre">			</span>//這裡是控制是否釋放或歸還連接配接的代碼
			if (!enableTransactionSupport) {
				RedisConnectionUtils.releaseConnection(conn, factory);
			}
		}
	}
           

關鍵代碼是這一句,RedisConnectionUtils.releaseConnection裡是歸還/釋放連接配接的代碼,很顯然,隻要你設定了enableTransactionSupport為true,或用mulit開啟了事務,調用redisTemplate的回調函數時是永遠不會歸還/釋放連接配接。

Sping Data Redis 使用事務時,不關閉連接配接的問題

也不要嘗試設定enableTransactionSupport為false去釋放連接配接,因為他隻會擷取一個新的連接配接然後關閉,代碼是這句:

if (enableTransactionSupport) {
				conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
			} else {//這裡會擷取一個新的連接配接
           
conn = RedisConnectionUtils.getConnection(factory);
			}
           

但知道連接配接是怎麼處理的就好辦了,我們手動釋放即可,好在redisTemplate提供了擷取getFactory的方法,但是還需要擷取到目前線程的connection,這個不難,追蹤RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);可以發現,這句話就是綁定/傳回目前線程的connection的,如果目前線程還不存在connection,則建立一個。于是以下代碼就可以做到傳回連接配接給連接配接池。

redisTemplate.exec();	
// 擷取目前線程綁定的連接配接
RedisConnection conn = RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory(), true);
//返還給連接配接池
conn.close();
           

但是很快又發現另外一個問題,嘗試大量通路的時候會報出這個異常:

Could not release connection to pool 

很明顯是無法将連接配接返還給連接配接池,一番追蹤後發現,第一次是永遠不會報這個錯的,隻在通路達到一定次數的情況下才會比較頻繁的出現,這個不難了解,應該與會話緩存。顯然是我們歸還過一次連接配接池,第二次歸還時,連接配接池抛出了錯誤,也就是說,被返還的連接配接還綁定在會話緩存上,于是去看底層是如何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;
	}
           

注意看我的注釋。内部是通過TransactionSynchronizationManager.bindResource的,這下就好處理了,TransactionSynchronizationManager裡還提供了一個unbindResource的方法,調用方法是這樣的: TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());

于是修改代碼為以下:

redisTemplate.exec();	
// 擷取目前線程綁定的連接配接
RedisConnection conn = RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory(), true);
//返還給連接配接池
conn.close();
           
//解綁線程連接配接
           
TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());
           
</pre><pre>
           

再次測試,沒問題,再沒有任何錯誤,連接配接數與響應速度也非常快。最後 修改代碼,通過一個AOP來進行處理。

2016-8-10更新。 在重新看代碼的時候,發現了一個更簡單的處理。 在這個方法裡已經做了上面的事情,調用這個解綁的方式,更簡潔。 修改代碼 redisTemplate.exec();

RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());

兩句就可以了。

2017-8-16更新

收到了很多朋友的私信,都在問我關于redis連接配接的問題,發現有不少朋友實際上是不存在這個問題的,請不用擔心。

是以我在文章最後再說明一下,以免造成誤解:

如果采用spring的事務管理的話,spring的事務管理會處理好連結的問題。

我們因為沒有使用spring的事務管理,是以才需要自己編碼去解決.

繼續閱讀