关于RedisTemplate的ERR EXEC without MULTI错误

  • Post author:
  • Post category:其他




问题

在看[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;
	}

说明:

  1. 这里有一个新的东西:

    TransactionSynchronizationManager

    ,这是由

    spring

    提供的,他里面有一个叫

    resources

    的成员,他是一个

    ThreadLocal

    。所以这一行代码,就很清楚了,他是去拿到跟当前线程绑定的连接。
  2. 这里就是判断啊,当前线程是否绑定了这么一个连接。
  3. 如果拿到了跟当前线程绑定的连接,且

    enableTransactionSupport

    的值是

    true

    ,那么需要做一些操作~ 不过这些操作是同

    spring

    的事务相关的,在我们的代码中,不会执行~
  4. 但是,我们第一次执行啊,好像没有给当前线程绑定过连接,所以上一步是执行不到的~ 这里创建一个连接~
  5. 然后,在这里,我们把当前线程和连接绑定起来~

所以,综上,为啥我们的代码不对呢,因为

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

说明:

  1. 初始化一条数据~
  2. 开始事务支持

  3. watch

    一个

    key

    ,同时在这一步执行时,会创建一个新的连接并与当前线程绑定~
  4. 执行

    multi

    ,这里会拿到上一步与当前线程绑定的连接,并通过该连接调用

    multi

    方法~
  5. 再加一条数据~
  6. 执行

    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);

来解除执行过程中与当前线程绑定的连接,并在随后关闭连接。



版权声明:本文为Yihchu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。