Redis - RedisTemplate的切換庫實現


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在同步狀態下禁止選庫操作(會拋出異常).

 

 

二丶實現一: 使用connection進行選庫

  

 

   上圖中的配置, 每次進行一次不同的庫的操作, 就需要選擇一次庫, 因為操作一次前就移除了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, 可以使用共享連接,  經本地測試, 確實比非共享連接快一點.

 

  完整源碼

 

 

四丶后記

  每種實現都有它的適用場景和短板, 需要使用得當, 否則會出現問題.

 

學習資料:

  spring-data-redis進行選庫操作


免責聲明!

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



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