Share|使用Java 进行 Redis事务开启

2022-05-311320

1、刀耕火种篇 2、工具篇 直接使用 RedisTemplate 完成(刀耕火种篇) V1.0 在用的时候,会首先翻一下 redisTemplate 里 面的API有什么玩意,然后拼凑在了一起。 public List<String> v1() { try { redisTemplate.multi(); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V1, "v1_1"); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V1, "v1_2"); redisTemplate.exec(); } catch (Exception e) { redisTemplate.discard(); log.error(e.getMessage()); } return redisTemplate.opsForList().range(REDIS_TEST_KEY_V1, 0, -1); } 这段代码执行的时候,会直接喜提报错 Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR DISCARD without MULTI 这时候就很疑惑了,我不是都抓住异常打印了嘛?为什么还会报错?而且这报错竟然还是 without MULTI,就让人很费解了。 而打印的异常日志里面,也记录了下面的异常 org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR DISCARD without MULTI debug进去到执行的源代码里面,就能够很快地发现 enableTransactionSupport 这个判断,里面也明明白白的写着了,只有满足了这个条件,发起的redis连接才能够使用redis事物 @Nullable public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) { // …… try { 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);
  // ……
    } finally {
        RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
    }
}

浅看一下,bindConnection() 这个方法里面,究竟是如何执行的,一路狂蹦之下,就能够看到这个注册同步事物的方法,里面就明明白白写着了 conn.multi() 这个方法,执行多个语句。 private static void potentiallyRegisterTransactionSynchronisation(RedisConnectionHolder connHolder, final RedisConnectionFactory factory) {

    if (isActualNonReadonlyTransactionActive()) {

        if (!connHolder.isTransactionSyncronisationActive()) {
            connHolder.setTransactionSyncronisationActive(true);

            RedisConnection conn = connHolder.getConnection();
            conn.multi();

            TransactionSynchronizationManager
                    .registerSynchronization(new RedisTransactionSynchronizer(connHolder, conn, factory));
        }
    }
}

问题就来了,我们不是在一开始就执行了这个方法的吗? 虽然看到了执行下来,确实isMutil() 为true,但是在执行execute()方法里面的finallly代码块里面的方法是,就会发现,直接就把当前连接给停掉了。原因也是因为 transactionSupport 为false。 public static void releaseConnection(@Nullable RedisConnection conn, RedisConnectionFactory factory, boolean transactionSupport) {

    if (conn == null) {
        return;
    }

    RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);

    if (connHolder != null && connHolder.isTransactionSyncronisationActive()) {
        if (log.isDebugEnabled()) {
            log.debug("Redis Connection will be closed when transaction finished.");
        }
        return;
    }

    if (isConnectionTransactional(conn, factory)) {

        // release transactional/read-only and non-transactional/non-bound connections.
        // transactional connections for read-only transactions get no synchronizer registered
        if (transactionSupport && TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            if (log.isDebugEnabled()) {
                log.debug("Unbinding Redis Connection.");
            }
            unbindConnection(factory);
        } else {

            // Not participating in transaction management.
            // Connection could have been attached via session callback.
            if (log.isDebugEnabled()) {
                log.debug("Leaving bound Redis Connection attached.");
            }
        }
    } else {
        doCloseConnection(conn);
    }
}

所以很明显,我们开启事务是失败的,原因就在于我们连接redis的时候,没有更改默认为false的enableTransactionSupport 于是乎,就找到了下面的设置。 public List<String> v1() { try { redisTemplate.setEnableTransactionSupport(true); redisTemplate.multi(); redisTemplate.opsForList().range(REDIS_TEST_KEY_V1,0,-1); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V1, "v1_1"); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V1, "v1_2"); redisTemplate.exec(); } catch (Exception e) { redisTemplate.discard(); log.error(e.getMessage()); } return redisTemplate.opsForList().range(REDIS_TEST_KEY_V1, 0, -1); } 加上之后就能够正常执行,并且还会得到很多的结果,原因就在于之前虽然都是在抛出异常,但是 redis 还是勤勤恳恳地在工作,事务也没有执行到,即使抛出了异常还是没有完成回滚的动作。 事务内并不能成功读取到 redis 内的值。 { "code": 200, "msg": null, "data": [ "v1_2", "v1_1", "v1_2", "v1_1", "v1_2", "v1_1", "v1_2", "v1_1", "v1_2", "v1_1", "v1_2", "v1_2", "v1_1" ] } 接下来我们就试一下是否有执行到事务 public List<String> v1() { try { redisTemplate.setEnableTransactionSupport(true); redisTemplate.multi(); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V1, "v1_1"); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V1, "v1_2"); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V1, "v1_3"); NumberUtil.div(5,0); redisTemplate.exec(); } catch (Exception e) { redisTemplate.discard(); log.error(e.getMessage()); } return redisTemplate.opsForList().range(REDIS_TEST_KEY_V1, 0, -1); } 目前为止就能够成功完成事务,并通过测试能够做到回滚。 Spring Data Redis 提供的接口实现(工具篇) 从官网上面借鉴了一下demo,这个方法能够快速解决事务的问题。 ● 能够批量获取执行之后的结果,用于后续的使用,这个点感觉也是十分方便的。 ● 缺点:不能够在事务里面读取到值。 当然,可能看到的时候,会有一个臃肿的感觉,每次实现这个方法都写开始和结束,着实让人烦躁。 public void v2() throws Exception { List<Object> txResult = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() { @Override public List<Object> execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_1"); operations.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_2"); operations.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_3"); return operations.exec(); } }); System.out.println(txResult.stream().map(result -> String.valueOf(result)).collect(Collectors.toList()));

}

官网也提供了一种和 @Transcation注解一起食用的方法,能够快速完成 Redis 事务的方法 里面的两次打印值都相等,均为未执行事务的值,也就能够解决前一个版本的痛点——不能够在事务里面读取到值。读取的大概逻辑就是读写分离,具体的实现原理还需要深究。 同时也就意味着我们并不能读取到同一个事务内未提交的值。 @Transactional(rollbackFor = Exception.class) public void v2() throws Exception { System.out.println(redisTemplate.opsForList().range(REDIS_TEST_KEY_V2,0,-1).size()); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_1"); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_2"); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_3"); System.out.println(redisTemplate.opsForList().range(REDIS_TEST_KEY_V2,0,-1).size()); } @Configuration @EnableCaching @EnableTransactionManagement public class RedisConfig extends CachingConfigurerSupport { @Bean @SuppressWarnings(value = {"unchecked", "rawtypes", "deprecation"}) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); // 开启事务支持 template.setEnableTransactionSupport(true); // …… return template; } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) throws SQLException { return new DataSourceTransactionManager(dataSource); } }

接下来就有一个小问题了:如果我不想通过拼接获得 key 里面提交后的结果,要怎么完成呢? 于是我尝试了一下骚操作 @Transactional(rollbackFor = Exception.class) public void v2() throws Exception { System.out.println(redisTemplate.opsForList().range(REDIS_TEST_KEY_V2,0,-1).size()); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_1"); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_2"); redisTemplate.opsForList().leftPush(REDIS_TEST_KEY_V2, "v2_3"); redisTemplate.exec(); System.out.println(redisTemplate.opsForList().range(REDIS_TEST_KEY_V2,0,-1).size()); } 结果:org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR EXEC without MULTI 其实也就是因为这个事务里面提交了两次 总的来说,这个结合 V2.1版本 的方法虽然写法简单,但是不能够在方法内获取事务提交后的结果,和V2.0相互解决互相之间的痛点吧。 最后,在此次分享实践中,如果是 redis 的语法有问题导致问题,事务是不会回滚的,并且会继续执行之后的 redis 语句。

分享
点赞0
打赏
上一篇:Docker常用命令笔记(一)
下一篇:XSS攻击"实战"