• Redis 事务在 SpringBoot 中的应用 (io.lettuce.core.RedisCommandExecutionException: ERR EXEC without MULTI)


    我们在 SpringBoot 中使用 Redis 时,会引入如下的 redis starter
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

    这个 starter 引入了 jedis 和 spring-data-redis 两个与 redis 核心的包。

    Redis 事务相关的命令参考

    Redis 事务在 SpringBoot 中的应用

    说明:下面以测试用例的形式说明 Redis 事务在 SpringBoot 中正确与错误的用法。首先,看一看当前测试用例的主体代码:

    package com.imooc.ad.service;
    
    import com.imooc.ad.Application;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.dao.DataAccessException;
    import org.springframework.data.redis.core.RedisOperations;
    import org.springframework.data.redis.core.SessionCallback;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.test.context.junit4.SpringRunner;
    
    /**
     * <h1>Redis 事务测试</h1>
     * Created by Qinyi.
     */
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
    public class RedisTransTest {
    
        /** 注入 StringRedisTemplate, 使用默认配置 */
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
    • 错误的用法
    /**
     * <h2>没有开启事务支持: 事务执行会失败</h2>
     * */
    @Test
    public void testMultiFailure() {
    
      stringRedisTemplate.multi();
      stringRedisTemplate.opsForValue().set("name", "qinyi");
      stringRedisTemplate.opsForValue().set("gender", "male");
      stringRedisTemplate.opsForValue().set("age", "19");
      System.out.println(stringRedisTemplate.exec());
    }
    

    执行以上测试用例,会抛出如下的异常信息:

    Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR EXEC without MULTI
    

    这里给出的错误信息显示:在执行 EXEC 命令之前,没有执行 MULTI 命令。这很奇怪,我们明明在测试方法的第一句就执行了 MULTI。通过追踪 multi、exec 等方法,我们可以看到如下的执行源码(spring-data-redis):

    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 {
        // RedisTemplate 的 enableTransactionSupport 属性标识是否开启了事务支持,默认是 false
    	   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);
    

    源码中已经给出了答案:由于 enableTransactionSupport 属性的默认值是 false,导致了每一个 RedisConnection 都是重新获取的。所以,我们刚刚执行的 MULTI 和 EXEC 这两个命令不在同一个 Connection 中。

    • 设置 enableTransactionSupport 开启事务支持

    解决上述示例的问题,最简单的办法就是让 RedisTemplate 开启事务支持,即设置 enableTransactionSupport 为 true 就可以了。测试代码如下:

    /**
     * <h2>开启事务支持: 成功执行事务</h2>
     * */
    @Test
    public void testMultiSuccess() {
      // 开启事务支持,在同一个 Connection 中执行命令
      stringRedisTemplate.setEnableTransactionSupport(true);
    
      stringRedisTemplate.multi();
      stringRedisTemplate.opsForValue().set("name", "qinyi");
      stringRedisTemplate.opsForValue().set("gender", "male");
      stringRedisTemplate.opsForValue().set("age", "19");
      System.out.println(stringRedisTemplate.exec());     // [true, true, true]
    }
    
    • 通过 SessionCallback,保证所有的操作都在同一个 Session 中完成

    更常见的写法仍是采用 RedisTemplate 的默认配置,即不开启事务支持。但是,我们可以通过使用 SessionCallback,该接口保证其内部所有操作都是在同一个Session中。测试代码如下:

    /**
     * <h2>使用 SessionCallback, 在同一个 Redis Connection 中执行事务: 成功执行事务</h2>
     * */
    @Test
    @SuppressWarnings("all")
    public void testSessionCallback() {
    
        SessionCallback<Object> callback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                operations.opsForValue().set("name", "qinyi");
                operations.opsForValue().set("gender", "male");
                operations.opsForValue().set("age", "19");
                return operations.exec();
            }
        };
    
        // [true, true, true]
        System.out.println(stringRedisTemplate.execute(callback));
    }
    

    总结:我们在 SpringBoot 中操作 Redis 时,使用 RedisTemplate 的默认配置已经能够满足大部分的场景了。如果要执行事务操作,使用 SessionCallback 是比较好,也是比较常用的选择。

    原文链接:http://www.imooc.com/article/281310?block_id=tuijian_wz#

  • 相关阅读:
    字符串内部查找函数
    vs2005 编译zlib 1.2.3 小记
    ies4linux 安装
    详述IP数据包的转发流程
    看源代码
    091213
    值得你记住并受用一生的Word XP/2003快捷键
    java开源框架的源代码怎么读?
    excel中的EMBED域介绍
    如何用c语言实现CString的构造函数、析构函数和赋值函数?
  • 原文地址:https://www.cnblogs.com/LX51/p/12161720.html
Copyright © 2020-2023  润新知