RedisTemplate 了解分析
前言
最近在使用springboot整合redis的時候遇到了一些問題,問題大緻可以分為
- redisTemplate如何去得到連接配接然後操作redis,每次調用是否使用了不同的連接配接
- redisTemplate支援開啟事務,但是開啟了事務之後它如何保證都是使用一個連接配接,因為我們在redisTemplate中的操作是每次都使用
redisTemplate.opsForXxx.xxx()
由于是第一次使用,對裡面的實作比較有興趣是以就翻了翻源碼。而且下面就先來看rediTemplate中的操作:
redisTemplate.opsForXxx.xxx()
- 例如通過redisTemplate對redis中的hash進行put操作的過程中我們會調用redisTemplate.opsForHash.put()方法,這個opsForHash()方法實際上為我們傳回了一個DefaultHashOperations對象(工廠方法),然後通過這個Operations對象再去進行各種redis操作。
Operations對象
- 工廠方法會預設為我們生成一個對應redis資料結構的例如DefaultHashOperations的一個執行個體,DefaultHashOperations類繼承自AbstractOperations,這個抽象類中有一個excute(),他會在具體的Operations對象中被調用來執行各種不同的操作。例如DefaultHashOperations中的put方法:
這裡調用的是Operations中的excute()方法。方法的參數是一個RedisCallback對象還有一個布爾值,但是這裡使用了一個lambda表達式,而且傳回值是null,有點看不懂是以選擇debug一下看看這個參數運作後的值是什麼。@Override public void put(K key, HK hashKey, HV value) { byte[] rawKey = rawKey(key); byte[] rawHashKey = rawHashKey(hashKey); byte[] rawHashValue = rawHashValue(value); execute(connection -> { connection.hSet(rawKey, rawHashKey, rawHashValue); return null; }, true); }
發現運作的時候這個callback其實就是[email protected]對象,excute将lambda表達式整個作為參數當作一個RedisCallback對象傳進execute中。在lambda表達式中就書寫了我們對應需要的redis操作,這裡就是hSet,後面拿到連接配接connection之後會由connection對象再去調用redisHashCommond的hSet方法。而這個lambda表達式在execute方法中形參名稱也被改成action。下面具體看一下RedisTemplate中execute方法的源碼。@Nullable <T> T execute(RedisCallback<T> callback, boolean exposeConnection) { return template.execute(callback, exposeConnection); }
RedisTemplate中execute方法
@Nullable
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 {
// 判斷是否處于事務狀态,這個boolean值可以在配置RedisTemplate時設定開啟事務
if (enableTransactionSupport) {
// 這個方法會将連接配接綁定到目前這個事務中,後面再分析
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}
//這邊又是一些判斷,沒有仔細去看,推測就是判斷這個事務的連接配接是否正确有效之類的
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
RedisConnection connToUse = preProcessConnection(conn, existingConnection);
//判斷是否是管道操作,這個變量再connection中可以拿到
boolean pipelineStatus = connToUse.isPipelined();
if (pipeline && !pipelineStatus) {
connToUse.openPipeline();
}
// 這裡判斷是否是暴露連接配接,如果不是就建立代理什麼的,這個布爾值預設是穿了true,也沒有再仔細去看,等有機會再看看
RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
// 這裡就是調用到lambda表達式了,在這一步會通過connection去操作redis然後傳回結果
T result = action.doInRedis(connToExpose);
// close pipeline
if (pipeline && !pipelineStatus) {
connToUse.closePipeline();
}
// TODO: any other connection processing?
return postProcessResult(result, connToUse, existingConnection);
} finally {
// 釋放連接配接 releaseConnection 後面會再看這個操作
RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
}
}
- 這段代碼中重點就是這一句
這裡對傳進來的lambda表達式做了調用。(lambda函數不會再傳進去的時候馬上做調用)。而lambda是非常靈活的,對應任何操作都可以在Operations裡面編寫不同的lambda表達式,然後都來調用相同的接口,實作不同的操作。這裡可以來複習(預習)一下函數式接口。T result = action.doInRedis(connToExpose);
函數式接口(參考 java 核心技術 卷I)
- 對于隻有一個抽象方法的接口,需要使用這種接口的對象時,就可以提供一個lambda表達式。這種皆空稱為函數式接口。為了展示如何轉化為函數式接口,下面考慮Array.sort 方法。它的第二個參數需要一個comparator 執行個體, Comparator就是隻有一個抽象方法的函數式接口,是以可以提供一個lambda表達式
在底層,Arrays.sort方法會接收實作了Compatator的某個對象。在這個對象上調用compare方法會執行這個lambda表達式的體。Arrays.sort(words, (first,second)->first.length()-second.length());
- 看到這裡大概解答了自己的疑惑,對應RedisTemplate中就成了:redisTemplate.execute()方法會接收一個實作了RedisCallback的某個對象。在這個對象上調用doInRedis方法會執行lambda表達式的體。
public interface RedisCallback<T> { /** * Gets called by {@link RedisTemplate} with an active Redis connection. Does not need to care about activating or * closing the connection or handling exceptions. * * @param connection active Redis connection * @return a result object or {@code null} if none * @throws DataAccessException */ @Nullable T doInRedis(RedisConnection connection) throws DataAccessException; }
- 是以,
就會得到表達式運作之後的值,我們調用的式set方法,它就傳回一個null給了result。T result = action.doInRedis(connToExpose);
redisTemplate事務
- 最後來看下redisTemplate是怎麼支援事務的,要支援事務,在我們進入multi()到exec()送出事務之間,肯定要使用一個連接配接操作。在execute()中剛才看過這段代碼塊
// 判斷是否處于事務狀态,這個boolean值可以在配置RedisTemplate時設定開啟事務 if (enableTransactionSupport) { conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport); } else { conn = RedisConnectionUtils.getConnection(factory); }
顯然,bindConnection()就是他做的一個綁定操作了。再看下它内部是怎麼實作的。
RedisConnectionUtils.bindConnection()會調用一個方法是doGetConnection(), 部分代碼如下:
可以看到這裡就是使用了public static RedisConnection bindConnection(RedisConnectionFactory factory, boolean enableTranactionSupport) { return doGetConnection(factory, true, true, enableTranactionSupport); } 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(); }
,這個TransactionSynchronizationManager是spring中對事務的支援。再點進去,會看到調用了這個方法:TransactionSynchronizationManager.getResource(factory)
這裡由一個map,這個map肯定就是能将我們的事務跟這個連接配接綁定映射,再點進去看其實就是用了ThreadLocal将連接配接綁定到線程中:@Nullable private static Object doGetResource(Object actualKey) { Map<Object, Object> map = (Map)resources.get(); if (map == null) { return null; } else { Object value = map.get(actualKey); if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) { map.remove(actualKey); if (map.isEmpty()) { resources.remove(); } value = null; } return value; } }
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
後言
在使用過程中也查閱了一下部落格,看到有個部落客中說當redisTemplate開啟了事務支援後,會導緻使用這個redisTemplate中調用非事務操作無法解綁和關閉連接配接。看了下源碼好像确實是如此,而查閱的部落格中也建議将事務跟非事務的redisTemplate分開。而且在exec()之後也還是不會解綁連接配接,由于我是開啟了連接配接池的,是以隻要在事務操作後是用
RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
接觸綁定就好。
剛才說的部落格:https://blog.csdn.net/qq_34021712/article/details/79606551
看這部分源碼的時候也是感覺很複雜,這裡面它每個方法的調用棧都比較深,debug也不太能整,而且一開始對lambda并不了解,一開始也看不懂。後續有時間可以再了解下函數程式設計以及redis的設計與實作。
如果有大佬發現錯誤,希望可以幫忙指出~