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执行命令

preProcessConnection

action.doInRedis(connToExpose)

postProcessResult(result, connToUse, existingConnection);

  3. 释放连接

RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);

 

  可以看出,redisTemplate执行主要是围绕connection进行的,  主要是使用RedisConnectionFactory#getConnection()获取连接,  如果每次调用该方法获取到的都是不同的连接, 使用同一个RedisTemplate并不会出现线程安全问题, 然而接口定义并没有指出不同, LettuceConnectionFactory可以创建共享连接, 所以使用同一个RedisTemaplte进行选库操作有可能出现并发问题.LettuceConnection在同步状态下禁止选库操作(会抛出异常).

 

 

二丶实现一: 使用connection进行选库

  

redis-cli切换 redistemplate切换数据库_ide

 

   上图中的配置, 每次进行一次不同的库的操作, 就需要选择一次库, 因为操作一次前就移除了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);
    }

 

 

redis-cli切换 redistemplate切换数据库_redis-cli切换_02

 

   上图可以看出, keySerializer是同一个对象, connectionFactory则是不同的对象

 

  注意事项, 使用BeanUtils复制属性, 需要在属性存在set,get方法的情况下才能成功复制, 没有的话, 不会复制, 但也不会报错.

  所以构造一个相同配置但dbIndex的ConnectionFactory相对比较复杂, 需要了解connectionFactory的内部构造, 还要完整测试. 不是很建议使用.

  

  构造新的LettuceConnectionFactory, 可以使用共享连接,  经本地测试, 确实比非共享连接快一点.

 

  完整源码

 

 

四丶后记

  每种实现都有它的适用场景和短板, 需要使用得当, 否则会出现问题.