问题
在看[Redis in Action]这本书的时候,官方虽然提供了java
代码,但是他是用jedis
实现的。本着练手和学习的目的打算在spring boot
中使用spring-boot-starter-data-redis
重新写一遍。然而在进行到第四章讲到multi
和exec
的时候就出现了问题,举个简单的例子:
redisTemplate.opsForHash().put("joker", "age", "27");
redisTemplate.watch("joker");
redisTemplate.multi();
redisTemplate.opsForHash().put("joker", "pet", "beibei");
redisTemplate.exec();
运行这段代码,程序就会给出Caused by: org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR EXEC without MULTI
错误,但是我明明执行multi()
了呀~
原因
遇到问题,第一部当然是去问google
,但是现在搜出来的结果很多都是抄的,而且很多抄的还是驴唇不对马嘴~
也不知道咋回事,我记得以前google
的搜索结果不是这样的~
我们一层一层的剥开,可以找到这么一个干实事的函数:
/**
* 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 {
// 1
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);
}
}
在代码1
处,可以看到有enableTransactionSupport
这么一个参数,看一下他的值是false
的话,那么会重新拿一个连接(而且他的默认值还就是false
),这也就解释了为啥我们明明执行multi
了,但是还没说我们在exec
前没有multi
~
但是,如果enableTransactionSupport
的值是true
呢,他又干了啥呢?我们一路点进去,找到了这么一个函数:
/**
* Gets a Redis connection. Is aware of and will return any existing corresponding connections bound to the current
* thread, for example when using a transaction manager. Will create a new Connection otherwise, if
* {@code allowCreate} is <tt>true</tt>.
*
* @param factory connection factory for creating the connection.
* @param allowCreate whether a new (unbound) connection should be created when no connection can be found for the
* current thread.
* @param bind binds the connection to the thread, in case one was created-
* @param transactionSupport whether transaction support is enabled.
* @return an active Redis connection.
*/
public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
boolean transactionSupport) {
Assert.notNull(factory, "No RedisConnectionFactory specified");
// 1
RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
if (connHolder != null) { // 2
if (transactionSupport) {
potentiallyRegisterTransactionSynchronisation(connHolder, factory); // 3
}
return connHolder.getConnection();
}
if (!allowCreate) {
throw new IllegalArgumentException("No connection found and allowCreate = false");
}
if (log.isDebugEnabled()) {
log.debug("Opening RedisConnection");
}
RedisConnection conn = factory.getConnection(); // 4
if (bind) {
RedisConnection connectionToBind = conn;
if (transactionSupport && isActualNonReadonlyTransactionActive()) {
connectionToBind = createConnectionProxy(conn, factory);
}
connHolder = new RedisConnectionHolder(connectionToBind);
TransactionSynchronizationManager.bindResource(factory, connHolder);// 5
if (transactionSupport) {
potentiallyRegisterTransactionSynchronisation(connHolder, factory);
}
return connHolder.getConnection(); // 8
}
return conn;
}
说明:
- 这里有一个新的东西:
TransactionSynchronizationManager
,这是由spring
提供的,他里面有一个叫resources
的成员,他是一个ThreadLocal
。所以这一行代码,就很清楚了,他是去拿到跟当前线程绑定的连接。 - 这里就是判断啊,当前线程是否绑定了这么一个连接。
- 如果拿到了跟当前线程绑定的连接,且
enableTransactionSupport
的值是true
,那么需要做一些操作~ 不过这些操作是同spring
的事务相关的,在我们的代码中,不会执行~ - 但是,我们第一次执行啊,好像没有给当前线程绑定过连接,所以上一步是执行不到的~ 这里创建一个连接~
- 然后,在这里,我们把当前线程和连接绑定起来~
所以,综上,为啥我们的代码不对呢,因为RedisTemplate
默认是不开启事务支持的,而且在执行exec
方法时,会重新创建一个连接对象(或者从当前线程的ThreadLocal
中拿到上一次绑定的连接)。所以,我们在不开启事务的情况下,自己在外面执行的multi
方法时完全不会生效的(因为连接对象都换了)~
解决
看到这,原因既然已经知道了,那么自然就迎刃而解了~
最简单的方式,既然默认是不开启事务支持的,那么我们手动把他打开不就好了~
执行: redisTemplate.setEnableTransactionSupport(true);
即可~
可能有些地方描述的不是很清楚,我们还是拿我们的例子来说,还是上面那段代码:
redisTemplate.opsForHash().put("joker", "age", "27"); // 1
redisTemplate.setEnableTransactionSupport(true); // 2
redisTemplate.watch("joker"); // 3
redisTemplate.multi(); // 4
redisTemplate.opsForHash().put("joker", "pet", "beibei"); // 5
redisTemplate.exec(); // 6
说明:
- 初始化一条数据~
- 开始事务支持
watch
一个key
,同时在这一步执行时,会创建一个新的连接并与当前线程绑定~- 执行
multi
,这里会拿到上一步与当前线程绑定的连接,并通过该连接调用multi
方法~ - 再加一条数据~
- 执行
exec
方法,同样是拿到与线程绑定的连接后,通过该连接执行exec
方法~ 因为该连接已经执行了watch
和multi
,所以在此之前,对应的key
如果发生变化,那么,不会执行成功,我们的目的也就达到了~
不过,这种方法还有一个问题,大家可以顺着源代码继续往下捋~ 会发现,与当前线程绑定的连接不会解绑,更不会被close
~
所以,感觉RedisTemplate
提供的SessionCallback
才是正解~
redisTemplate.execute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.watch("joker");
operations.multi();
operations.opsForHash().put("joker", "pet", "beibei");
return operations.exec();
}
});
RedisTemplate
的public <T> T execute(SessionCallback<T> session)
方法,会在finally
中调用RedisConnectionUtils.unbindConnection(factory);
来解除执行过程中与当前线程绑定的连接,并在随后关闭连接。