RedisTemplate 理解分析
前言
最近在使用springboot整合redis的时候遇到了一些问题,问题大致可以分为
- redisTemplate如何去得到连接然后操作redis,每次调用是否使用了不同的连接
- redisTemplate支持开启事务,但是开启了事务之后它如何保证都是使用一个连接,因为我们在redisTemplate中的操作是每次都使用
redisTemplate.opsForXxx.xxx()
由于是第一次使用,对里面的实现比较有兴趣所以就翻了翻源码。而且下面就先来看rediTemplate中的操作:
redisTemplate.opsForXxx.xxx()
- 例如通过redisTemplate对redis中的hash进行put操作的过程中我们会调用redisTemplate.opsForHash.put()方法,这个opsForHash()方法实际上为我们返回了一个DefaultHashOperations对象(工厂方法),然后通过这个Operations对象再去进行各种redis操作。
Operations对象
- 工厂方法会默认为我们生成一个对应redis数据结构的例如DefaultHashOperations的一个实例,DefaultHashOperations类继承自AbstractOperations,这个抽象类中有一个excute(),他会在具体的Operations对象中被调用来执行各种不同的操作。例如DefaultHashOperations中的put方法:
这里调用的是Operations中的excute()方法。方法的参数是一个RedisCallback对象还有一个布尔值,但是这里使用了一个lambda表达式,而且返回值是null,有点看不懂所以选择debug一下看看这个参数运行后的值是什么。@Override public void put(K key, HK hashKey, HV value) { byte[] rawKey = rawKey(key); byte[] rawHashKey = rawHashKey(hashKey); byte[] rawHashValue = rawHashValue(value); execute(connection -> { connection.hSet(rawKey, rawHashKey, rawHashValue); return null; }, true); }
发现运行的时候这个callback其实就是[email protected]对象,excute将lambda表达式整个作为参数当作一个RedisCallback对象传进execute中。在lambda表达式中就书写了我们对应需要的redis操作,这里就是hSet,后面拿到连接connection之后会由connection对象再去调用redisHashCommond的hSet方法。而这个lambda表达式在execute方法中形参名称也被改成action。下面具体看一下RedisTemplate中execute方法的源码。@Nullable <T> T execute(RedisCallback<T> callback, boolean exposeConnection) { return template.execute(callback, exposeConnection); }
RedisTemplate中execute方法
@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 {
// 判断是否处于事务状态,这个boolean值可以在配置RedisTemplate时设置开启事务
if (enableTransactionSupport) {
// 这个方法会将连接绑定到当前这个事务中,后面再分析
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}
//这边又是一些判断,没有仔细去看,推测就是判断这个事务的连接是否正确有效之类的
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
RedisConnection connToUse = preProcessConnection(conn, existingConnection);
//判断是否是管道操作,这个变量再connection中可以拿到
boolean pipelineStatus = connToUse.isPipelined();
if (pipeline && !pipelineStatus) {
connToUse.openPipeline();
}
// 这里判断是否是暴露连接,如果不是就创建代理什么的,这个布尔值默认是穿了true,也没有再仔细去看,等有机会再看看
RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
// 这里就是调用到lambda表达式了,在这一步会通过connection去操作redis然后返回结果
T result = action.doInRedis(connToExpose);
// close pipeline
if (pipeline && !pipelineStatus) {
connToUse.closePipeline();
}
// TODO: any other connection processing?
return postProcessResult(result, connToUse, existingConnection);
} finally {
// 释放连接 releaseConnection 后面会再看这个操作
RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
}
}
- 这段代码中重点就是这一句
这里对传进来的lambda表达式做了调用。(lambda函数不会再传进去的时候马上做调用)。而lambda是非常灵活的,对应任何操作都可以在Operations里面编写不同的lambda表达式,然后都来调用相同的接口,实现不同的操作。这里可以来复习(预习)一下函数式接口。T result = action.doInRedis(connToExpose);
函数式接口(参考 java 核心技术 卷I)
- 对于只有一个抽象方法的接口,需要使用这种接口的对象时,就可以提供一个lambda表达式。这种皆空称为函数式接口。为了展示如何转化为函数式接口,下面考虑Array.sort 方法。它的第二个参数需要一个comparator 实例, Comparator就是只有一个抽象方法的函数式接口,所以可以提供一个lambda表达式
在底层,Arrays.sort方法会接收实现了Compatator的某个对象。在这个对象上调用compare方法会执行这个lambda表达式的体。Arrays.sort(words, (first,second)->first.length()-second.length());
- 看到这里大概解答了自己的疑惑,对应RedisTemplate中就成了:redisTemplate.execute()方法会接收一个实现了RedisCallback的某个对象。在这个对象上调用doInRedis方法会执行lambda表达式的体。
public interface RedisCallback<T> { /** * Gets called by {@link RedisTemplate} with an active Redis connection. Does not need to care about activating or * closing the connection or handling exceptions. * * @param connection active Redis connection * @return a result object or {@code null} if none * @throws DataAccessException */ @Nullable T doInRedis(RedisConnection connection) throws DataAccessException; }
- 所以,
就会得到表达式运行之后的值,我们调用的式set方法,它就返回一个null给了result。T result = action.doInRedis(connToExpose);
redisTemplate事务
- 最后来看下redisTemplate是怎么支持事务的,要支持事务,在我们进入multi()到exec()提交事务之间,肯定要使用一个连接操作。在execute()中刚才看过这段代码块
// 判断是否处于事务状态,这个boolean值可以在配置RedisTemplate时设置开启事务 if (enableTransactionSupport) { conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport); } else { conn = RedisConnectionUtils.getConnection(factory); }
显然,bindConnection()就是他做的一个绑定操作了。再看下它内部是怎么实现的。
RedisConnectionUtils.bindConnection()会调用一个方法是doGetConnection(), 部分代码如下:
可以看到这里就是使用了public static RedisConnection bindConnection(RedisConnectionFactory factory, boolean enableTranactionSupport) { return doGetConnection(factory, true, true, enableTranactionSupport); } public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind, boolean enableTransactionSupport) { Assert.notNull(factory, "No RedisConnectionFactory specified"); RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory); if (connHolder != null) { if (enableTransactionSupport) { potentiallyRegisterTransactionSynchronisation(connHolder, factory); } return connHolder.getConnection(); }
,这个TransactionSynchronizationManager是spring中对事务的支持。再点进去,会看到调用了这个方法:TransactionSynchronizationManager.getResource(factory)
这里由一个map,这个map肯定就是能将我们的事务跟这个连接绑定映射,再点进去看其实就是用了ThreadLocal将连接绑定到线程中:@Nullable private static Object doGetResource(Object actualKey) { Map<Object, Object> map = (Map)resources.get(); if (map == null) { return null; } else { Object value = map.get(actualKey); if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) { map.remove(actualKey); if (map.isEmpty()) { resources.remove(); } value = null; } return value; } }
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
后言
在使用过程中也查阅了一下博客,看到有个博主中说当redisTemplate开启了事务支持后,会导致使用这个redisTemplate中调用非事务操作无法解绑和关闭连接。看了下源码好像确实是如此,而查阅的博客中也建议将事务跟非事务的redisTemplate分开。而且在exec()之后也还是不会解绑连接,由于我是开启了连接池的,所以只要在事务操作后是用
RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
接触绑定就好。
刚才说的博客:https://blog.csdn.net/qq_34021712/article/details/79606551
看这部分源码的时候也是感觉很复杂,这里面它每个方法的调用栈都比较深,debug也不太能整,而且一开始对lambda并不了解,一开始也看不懂。后续有时间可以再了解下函数编程以及redis的设计与实现。
如果有大佬发现错误,希望可以帮忙指出~