Redis的事务及Spring Data Redis的实现

Redis的事务及Spring Data Redis的实现

Redis的事务

简介

Redis的事务基于MULTI,EXEC,DISCARD和WATCH这四个命令实现。这四个命令组合允许在一步中同时执行多个命令。
Redis的事务主要保证了一下两个方面:

  • 事务中的多个命令是按照添加到队列中的顺序执行的。并且在事务进行中,不会被另一个客户端的请求打断。也就是说,事务中的多个命令可以看作单个隔离的操作。
  • 事务可以保证原子性。事务的执行由EXEC命令触发,当客户端在发送EXEC命令前丢失了该事务的连接,该事务中的命令将不会被执行。如果在客户端在调用EXEC命令后丢失连接,事务的命令队列依然会被执行。Redis事务会写入磁盘文件。若Redis在事务中异常停止,在重启时会报错,可以使用redis-check-aof工具修复后再重新启动。
  • 从2.2版本开始,可以使用乐观锁对上述两种情况添加额外保证。类似与CAS操作。

事务的使用

  1. 通过MULTI进入一个事务。这个命令总是返回OK。
  2. 客户端发起多个命令。Redis将这些命令依次放入队列,并不会执行。所有命令都会返回字符串QUEUED回复。
  3. 当客户端执行EXEC命令后,开始执行事务。EXEC命令会返回多个结果的数组,每个元素对应事务中的一个命令,且顺序与命令顺序一致。
  4. 当客户端调用DISCARD命令后,清空事务队列并退出当前事务。

事务对于错误的处理

事务中可能会碰到的错误如下:

  • 命令排队失败。例如:命令的语法错误或者该命令可能导致一些危险,例如当设置Redis可以使用的最大内存时可能碰到内存不足的情况。
  • 调用EXEC后命令可能会执行失败。例如,因为我们对一个具有错误值的键执行了一个操作(比如对一个字符串值调用一个列表操作)。

对于第一种错误,客户端应该判断返回值是否返回字符串QUEUED来处理,例如中止并丢弃该事务。
从Redis 2.6.5开始,Redis服务器会记住在命令排队时的错误,并且拒绝执行事务,在EXEC期间返回错误,并自动丢弃事务。
在2.6.5之前,如果客户端不处理错误,继续执行EXEC,服务器将会仅执行命令队列中的正确命令。
对于第二种错误,Redis服务器不会做特殊处理,所有的命令都将会执行,即使某些命令执行错误。对于出错命令返回-ERR。

重要:在EXEC之后,事务开始执行队列中所有命令,即使碰到错误也不会中止!只会对发生执行错误的命令返回-ERR

重要:Redis事务不支持回滚!

check-and-set 乐观锁

乐观锁通过WATCH命令实现。WATCH可以用来监视一个key的变化。如果在EXEC执行前,至少一个被监视的key被修改,事务将会中止,EXEC命令返回Null来通知事务失败。
当返回事务失败时可以重复操作,直到成功。这种锁即为乐观锁。

Redis脚本和事务

Redis脚本是事务性的,因此可以使用脚本执行操作,通常脚本将更简单,更快速。

Spring Data Redis(2.1)对Redis事务的支持

RedisTemplate提供了对multi、exec和discard命令的支持。但是,RedisTemplat不会在一个连接上执行多个redis命令。Spring Data Redis提供了SessionCallback接口,支持一个连接执行多个redis命令。
因此,Spring Data Redis支持redis的事务需要依赖SessionCallback接口。


List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
  public List<Object> execute(RedisOperations operations) throws DataAccessException {
    operations.multi();
    operations.opsForSet().add("key", "value1");

    // This will contain the results of all operations in the transaction
    return operations.exec();
  }
});
System.out.println("Number of items added to set: " + txResults.get(0));

使用@Transactional

默认情况下,事务支持是禁用的,必须通过设置setEnableTransactionSupport(true)为正在使用的每个RedisTemplate显式启用。这样做会强制将当前RedisConnection绑定到触发MULTI的当前线程。如果事务顺利完成,则调用EXEC。否则调用DISCARD。进入MULTI后,RedisConnection会将写操作排队。所有只读操作(例如KEYS)都传递到新的(非线程绑定)RedisConnection。
以下示例显示如何配置事务管理:

@Configuration
@EnableTransactionManagement
public class RedisTxContextConfiguration {

  @Bean
  public StringRedisTemplate redisTemplate() {
    StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory());
    // 启用事务
    template.setEnableTransactionSupport(true);
    return template;
  }

  @Bean
  public RedisConnectionFactory redisConnectionFactory() {
    // jedis || Lettuce
  }

  @Bean
  public PlatformTransactionManager transactionManager() throws SQLException {
    return new DataSourceTransactionManager(dataSource());
  }

  @Bean
  public DataSource dataSource() throws SQLException {
    // ...
  }
}

配置说明:

  • @EnableTransactionManagement: 注解开启了声明式事务管理
  • template.setEnableTransactionSupport(true): 明确开启redis事务
  • return new DataSourceTransactionManager(dataSource()): 事务管理需要一个PlatformTransactionManager。Spring Data Redis不包含PlatformTransactionManager实现。假设应用程序中使用了JDBC,Spring Data Redis可以使用现有的事务管理器参与事务。

需要注意的是,通过这种方式使用事务,所有的只读操作会在另一个不与线程绑定的连接中完成。并且这种方式使用的是复合事务