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在同步状态下禁止选库操作(会抛出异常).
上图中的配置, 每次进行一次不同的库的操作, 就需要选择一次库, 因为操作一次前就移除了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, 可以使用共享连接, 经本地测试, 确实比非共享连接快一点.
四丶后记
每种实现都有它的适用场景和短板, 需要使用得当, 否则会出现问题.