RedisTemplate的切換庫實現
一丶緣由
一個Redis實例有[0-15]共16個database, 默認情況下, redisTemplate只能配置一個database, 當服務應用需要使用另外配置來配置另外的redisTemplate. 由於配置多, 容易出錯.這時就出現了"選庫"的需求.
二丶RedisTemaplte的執行邏輯
/** * Executes the given action object within a connection that can be exposed or not. Additionally, the connection can * be pipelined. Note the results of the pipeline are discarded (making it suitable for write-only scenarios). * * @param <T> return type * @param action callback object to execute * @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code * @param pipeline whether to pipeline or not the connection for the execution * @return object returned by the action */ @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 { 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 { RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport); } }
主要分3步:
1. 獲取連接
a. 如果開啟了事務, 則從事務管理器根據connectionFatory和當前線程獲取連接, 如果沒有, 則用connectionFactory獲取連接
b. 沒有開啟事務, 直接從connectionFactory獲取連接
2. 用connection執行命令
a. preProcessConnection connection前置處理, 一般用於初始化
b. action.doInRedis(connToExpose) 執行當前action
c. postProcessResult(result, connToUse, existingConnection); 后置處理結果
3. 釋放連接
RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
可以看出,redisTemplate執行主要是圍繞connection進行的, 主要是使用RedisConnectionFactory#getConnection()獲取連接, 如果每次調用該方法獲取到的都是不同的連接, 使用同一個RedisTemplate並不會出現線程安全問題, 然而接口定義並沒有指出不同, LettuceConnectionFactory可以創建共享連接, 所以使用同一個RedisTemaplte進行選庫操作有可能出現並發問題.LettuceConnection在同步狀態下禁止選庫操作(會拋出異常).
上圖中的配置, 每次進行一次不同的庫的操作, 就需要選擇一次庫, 因為操作一次前就移除了dbIndex
注意: spring boot 2.0 默認使用lettuce連接, 在共享連接狀態下不能進行select(dbIndex)操作, 可以先設置#setShareNativeConnection(false), 設置非共享連接
三丶實現二: 創建不同的redisTemplate
直接使用配置創建不同的redisTemplate, 會出現配置過多的問題, 可以利用反射, BeanUtils進行屬性復制, 達到相同配置的目的. 需要注意的是, 在RedisTemplate, ConnectionFactory等對象屬性, 需要創建不同的對象實例, 以避免並發問題, 因為BeanUtils屬性復制, 僅僅進行了引用復制, 還是可能會出現並發問題. 即, 可能出現並發問題的對象, 需要重新創建一份.
public class RedisDbSelectFactory { /** * 創建restTemplate相同配置,但dbIndex不同的RestTemplate, 可以理解為選庫 * * @param redisTemplate * @param dbIndex redis庫 * @return */ public static RedisTemplate selectDb(RedisTemplate redisTemplate, int dbIndex){ try { RedisTemplate dbSelectRedisTemplate=redisTemplate.getClass().getConstructor().newInstance(); BeanUtils.copyProperties(redisTemplate, dbSelectRedisTemplate); RedisConnectionFactory connectionFactory=dbSelectRedisTemplate.getConnectionFactory(); RedisConnectionFactory dbSelectConnectionFactory=createDbSelectConnectionFactory(connectionFactory, dbIndex); dbSelectRedisTemplate.setConnectionFactory(dbSelectConnectionFactory); dbSelectRedisTemplate.afterPropertiesSet(); return dbSelectRedisTemplate; } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } protected static RedisConnectionFactory createDbSelectConnectionFactory(RedisConnectionFactory connectionFactory, int dbIndex){ RedisConnectionFactory dbSelectConnectionFactory=null; if(connectionFactory instanceof LettuceConnectionFactory){ dbSelectConnectionFactory= createLettuceDbSelectFactory((LettuceConnectionFactory)connectionFactory, dbIndex); }else { // 由於通過創建一個連接工廠比較復雜(BeanUtils復制屬性有限制, 需要了解連接工廠內部構造), 暫不創建其他連接工廠 throw new RuntimeException("不能識別類型: "+connectionFactory.getClass()); } return dbSelectConnectionFactory; } // -------------------------------------- // lettuceConnectionFactory, 創建后的connection在共享連接下不支持選擇庫 (connection#select), // 調用#setShareNativeConnection(false)后可以選庫 // !!! 注意事項: 使用BeanUtils復制屬性, 屬性必須添加set,get方法,否則拷貝不成功,但是不報錯 // 由於創建一個相同配置但dbIndex不同的方法比較復雜, 使用前需要仔細測試 private static LettuceConnectionFactory createLettuceDbSelectFactory(LettuceConnectionFactory connectionFactory, int dbIndex){ LettuceConnectionFactory dbSelectConnectionFactory=new LettuceDbSelectConnectionFactory(dbIndex); BeanUtils.copyProperties(connectionFactory, dbSelectConnectionFactory); //構造參數傳入的屬性(因為沒有setter, BeanUtils不能復制的屬性) final String[] constructProperties=new String[]{"clientConfiguration", "configuration"}; MyBeanUtils.forceCopyProperties(connectionFactory, dbSelectConnectionFactory, constructProperties); dbSelectConnectionFactory.afterPropertiesSet(); final String[] equalProperties=new String[]{"clientConfiguration", "configuration"}; final String[] notEqualProperties=new String[]{"client","pool", "connectionProvider","reactiveConnectionProvider"}; final String[] sameTypeProperties=new String[]{"connectionProvider","reactiveConnectionProvider"}; MyBeanUtils.assertPropertiesEquals(connectionFactory, dbSelectConnectionFactory, equalProperties); MyBeanUtils.assertPropertiesNotEquals(connectionFactory, dbSelectConnectionFactory, notEqualProperties); MyBeanUtils.assertSameTypes(connectionFactory, dbSelectConnectionFactory, sameTypeProperties); return dbSelectConnectionFactory; } @Slf4j private static class LettuceDbSelectConnectionFactory extends LettuceConnectionFactory{ private int pointDbIndex; public LettuceDbSelectConnectionFactory(int pointDbIndex) { this.pointDbIndex = pointDbIndex; } /** * 替換原配置的dbIndex * @return */ @Override public int getDatabase() { log.debug("使用redis庫{}",pointDbIndex); return pointDbIndex; } } }
配置使用不同的庫:
/** * key, value 都是字符串 * 使用3號庫 * @param stringRedisTemplate 由{@link RedisAutoConfiguration}實例化stringRedisTemplate * @return */ @Bean("string3RedisManager") public RedisManager<String,String> string3RedisManager( @Qualifier("stringRedisTemplate") RedisTemplate<String,String> stringRedisTemplate){ RedisTemplate<String,String> string3RedisTemplate=RedisDbSelectFactory.selectDb(stringRedisTemplate, 3); return new RedisManager<>(string3RedisTemplate); }
上圖可以看出, keySerializer是同一個對象, connectionFactory則是不同的對象
注意事項, 使用BeanUtils復制屬性, 需要在屬性存在set,get方法的情況下才能成功復制, 沒有的話, 不會復制, 但也不會報錯.
所以構造一個相同配置但dbIndex的ConnectionFactory相對比較復雜, 需要了解connectionFactory的內部構造, 還要完整測試. 不是很建議使用.
構造新的LettuceConnectionFactory, 可以使用共享連接, 經本地測試, 確實比非共享連接快一點.
四丶后記
每種實現都有它的適用場景和短板, 需要使用得當, 否則會出現問題.
學習資料: