问题
   
    在看[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);
    
    来解除执行过程中与当前线程绑定的连接,并在随后关闭连接。
   
 
