• Redis


    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进行选库操作

    人生没有彩排,每一天都是现场直播
  • 相关阅读:
    awk命令
    计算机基础
    python基础-条件判断
    jmeter分布式负载
    jmeter之JSON Path Extractor取值关联
    3.regsvr32 使用说明
    2.NPS代理
    1.正向代理和反向代理
    Calendar 获取指定日期所在月份的第一天、最后一天、下个月第一天等
    mysql 数据库 小知识
  • 原文地址:https://www.cnblogs.com/timfruit/p/12148677.html
Copyright © 2020-2023  润新知