zoukankan      html  css  js  c++  java
  • 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进行选库操作

    人生没有彩排,每一天都是现场直播
  • 相关阅读:
    hdu 4521 小明系列问题——小明序列(线段树 or DP)
    hdu 1115 Lifting the Stone
    hdu 5476 Explore Track of Point(2015上海网络赛)
    Codeforces 527C Glass Carving
    hdu 4414 Finding crosses
    LA 5135 Mining Your Own Business
    uva 11324 The Largest Clique
    hdu 4288 Coder
    PowerShell随笔3 ---别名
    PowerShell随笔2---初始命令
  • 原文地址:https://www.cnblogs.com/timfruit/p/12148677.html
Copyright © 2011-2022 走看看