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, 可以使用共享连接, 经本地测试, 确实比非共享连接快一点.
四丶后记
每种实现都有它的适用场景和短板, 需要使用得当, 否则会出现问题.
学习资料: