最近做项目使用Spring自己封装的spring-data-redis(版本:spring-3.2,spring-data-redis-1.5,jedis-2.6),当使用redis的事务时,一直报"ERR EXEC without MULTI"错误,经过两天多的源码研究,发现问题在于redis的事务配置和源码实现上。

在使用redis配置的时候,如果需要开始redis的事务,一般会遇到以下两个属性的配置。然而事实上,当使用spring自己封装的RedisCacheManager时,是不需要配置RedisTemplate的enableTransactionSupport属性的,即RedisCacheManager的transactionAware=true,但RedisTemplate的enableTransactionSupport要等于默认值false,原因在下面分析中。

首先需要了解redis事务的机制是和数据库(如mysql)不一样的,具体网上有很多解释,借鉴http://www.runoob.com/redis/redis-transactions.html中的说明:

接下来看两个变量配置的具体执行过程:
1.RedisCacheManager.transactionAware=true:在操作cache(RedisCache对象)的时候,会将cache进行装饰:

操作过程会变为以下,这里的targetCache为Spring封装的RedisCache对象:

即当前线程有事务绑定的时候(如mysql的数据操作),会在当前线程中注入一个事务对象,用于整体事务提交的时候执行afterCommit中的方法操作redisCache,如果当前线程没有事务,直接操作redisCache。
2.RedisTemplate.enableTransactionSupport=true:在获取redis连接时,会将连接绑定到当前线程,并且自动执行MULTI命令:

绑定过程如下:

如果外部有其他事务(如mysql),则在事务结束后调用afterCompletion方法执行EXEC命令结束redis的事务:

需要注意的是,在spring中,多次执行MULTI命令不会报错,因为第一次执行时,会将其内部的一个isInMulti变量设为true,后续每次执行命令是都会检查这个变量,如果为true,则不执行命令。而多次执行EXEC命令则会报开头说的"ERR EXEC without MULTI"错误。
分析完以上两个变量的执行过程,好像并没有什么会报错的原因,但其实真正导致开头错误的是Spring自己封装的RedisCache类导致的:

以put操作为例,RedisCache会将key-value最终封装为一个RedisCachePutCallback对象,最后redisTemplate操作这个对象时,同时执行了MULTI和EXEC命令:

对于put操作,因为在这里执行过一次EXEC命令,所以在最后事务提交的时候,再执行EXEC命令就会报错,虽然这个错误并不会影响程序运行,也不会影响redis的性能,redis的连接也会被正常关闭。而对于evict操作,源码中只有DEL指令,并没有和put操作一样的一套MULTI-EXEC指令,所以只有put操作会报错。
总结:如果所有功能都使用Spring自己封装的东西(注解机制,RedisCacheManager,RedisCache等等):
1.同时启用transactionAware&enableTransactionSupport是没有功能上的问题的,唯一的问题就是由于两次EXEC命令导致报错,但这个错误会被catch住不会往上传递,所以不用担心,至于为什么会这样设计(难道设计RedisCacheManeger的人没发现redisTemplate会自动执行MULTI和EXEC命令,所以自己又跑了一遍?)我也不清楚,反正用了没大问题就行。
2.只启用transactionAware变量,redis实现事务时,其实是将redis操作放到最后所有事务一起提交的时候(如果有其他事务如MYSQL,redis事务排在MYSQL后面,若没有其他事务,则按实际操作redis),在业务程序执行过程中不操作redis缓存(读除外),而且当EXEC执行前遇到错误时,redisTemplate的execute方法在finally块中释放连接的同时会检查是否处于MULTI中,如果是,会先执行DISCARD命令,然后进行关闭,所以不用担心MULTI出错而redis不会回滚的问题。
3.只启用enableTransactionSupport变量,redis会在每次操作时进行MULTI-put/evict-EXEC操作,而这些操作是在整个事务提交之前和业务代码一起进行的,即如果最后数据库提交事务时发生如网络问题导致数据库的事务失败,那么redis的事务是已经生效了的,此时会出现redis的数据和数据库的数据不一致的情况。
补充:由于RedisCache内部类RedisCachePutCallback自己有一套MULTI-EXEC,所以当存在两个以上的redis操作时,如果第二个失败了,那么第一个也是有效的。而对于RedisCacheEvictCallback没有MULTI-EXEC,这里会有一个问题,就是当启用enableTransactionSupport,并且使用Caching注解先evict后put保存业务数据时,就会出现不会保存到缓存里的情况,因为spring封装的redis默认每个连接都是新连接(这个不知道为什么这么设计),对于evict和put操作使用不同的连接,这样就会出现这样的流程:evict连接1multi——evict操作——put连接2multi——put操作——put连接2exec——evict连接2exec,所以在这种情况下,虽然用Caching注解的初衷是先删除缓存再保存缓存(实际上spring的代理会在保存操作的时候自动更新缓存,这里主要是为了将bug展现出来才这么用),但是最后生效的evict事务导致数据反而被清除了。
如果想避免以上情况出现,那么可能就需要自己封装spring的redis实现了,或者对已有实现进行继承改进,具体暂未研究。
以上只是个人的一个学习所得,欢迎指正!