Spring Data Redis事务的正确使用姿势

之前使用Spring Data Redis的时候,由于使用不当,导致redis连接不释放的血案。现在来总结下Spring Data Redis事务的两种使用方式。

1、execute(SessionCallback session)方法

我们都知道Redis的事务的命令主要有multi、exec、discard和watch,在RedisTemplate中也是对应的有这几种方法

  public interface RedisOperations<K, V> {
      ....
    void watch(K key);
	void watch(Collection<K> keys);
	void unwatch();
	void multi();
	void discard();
	List<Object> exec();
      ....
  }

1.1 错误示例

然而,它们是不能单独直接被调用的,Spring Data官网有句话说:

Redis provides support for transactions through the multi, exec, and discard commands. These operations are available on RedisTemplate. However, RedisTemplate is not guaranteed to execute all operations in the transaction with the same connection.

大概意思是直接单独调用的话,RedisTemplate是不会保证这些操作在一个连接内完成,

//错误示例
public void testRedisTx0() {
	template.multi();
	template.opsForValue().set("hxm", "9999");
	System.out.println(template.opsForValue().get("hxm"));
	List<Object> result = template.exec();//此处抛出异常
	System.out.println(result);
}
//此处会导致一个异常
org.springframework.dao.InvalidDataAccessApiUsageException: No ongoing transaction. Did you forget to call multi?

上面的错误实例执行到exec方法时,会抛出以上的异常,可以看出程序认为你还没有执行过multi操作,所以不能执行exec。这是因为这几个操作都不是在一个连接完成的。

1.2 正确使用姿势

RedisTemplate提供了一种正确的使用方式,那就是execute(SessionCallback session)方法:

<T> T execute(SessionCallback<T> session);

SessionCallback包含了一个回调函数execute(RedisOperations operations),在这个函数里实现以上的操作,就可以保证事务的正常使用。

	public void testRedisTx1() {
		List<Object> r = template.execute(new SessionCallback<List<Object>>() {

			@Override
			public List<Object> execute(RedisOperations operations) throws DataAccessException {
				operations.multi();
				operations.opsForValue().set("hxm", "9999");
                  //此处打印null,因为事务还没真正执行
				System.out.println(operations.opsForValue().get("hxm"));
				return operations.exec();
			}
		});
		
		System.out.println(r);
	}

我们可以看看这个execute(SessionCallback session)方法的内部实现机理,可以看到该方法中先把当前建立的连接绑定在当前的线程中,确保之后的redis操作可以在一个连接内进行,而redis操作过后,会把当前连接在线程中解绑,并且释放这个连接。

public <T> T execute(SessionCallback<T> session) {
	RedisConnectionFactory factory = getRequiredConnectionFactory();
	//绑定连接在当前线程,可实现下面execute方法内的redis操作保持在一个连接内完成
	RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
	try {
		return session.execute(this);
	} finally {
		RedisConnectionUtils.unbindConnection(factory);//把连接从当前线程中解绑、释放连接
	}
}

2、@Transactional的支持

另一种实现事务的是@Transactional注解,这种方法是把事务交由给spring事务管理器进行自动管理。使用这种方法之前,跟jdbc事务一样,要先注入事务管理器,如果工程中已经有配置了事务管理器,就可以复用这个事务管理器,不用另外进行配置。另外,需要注意的是,跟第一种事务操作方法不一样的地方就是RedisTemplate的setEnableTransactionSupport(boolean enableTransactionSupport)方法要set为true,此处贴出官方的配置框架:

@Configuration
@EnableTransactionManagement                                 
public class RedisTxContextConfiguration {

  @Bean
  public StringRedisTemplate redisTemplate() {
    StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory());
    // explicitly enable transaction support
    template.setEnableTransactionSupport(true);//此处必须设置为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 {
    // ...
  }
}

这种方法的使用方法比较简单,就在要使用事务的方法注解@Transactional,这跟jdbc事务使用是一样的,这样就不用手工的执行multi、exec方法了,这些事务控制方法会由spring事务管理器自动完成。实例如下:

	@Transactional
	public void testRedisTx2() {

		template.opsForValue().set("hxm", "9999");
		System.out.println(template.opsForValue().get("hxm"));//此处打印为null

	}

然而,这种便利的使用方法有局限性,就是不支持只读操作,如果执行get之类的操作,将会返回null,所以使用的时候要多加注意!

3、两种方法的对比

3.1 execute(SessionCallback session)方法

  • 事务代码块的范围灵活可控
  • 支持读操作
  • 代码略显臃肿

3.2 @Transactional注解方法

  • 使用方便,代码比较优雅
  • 不够灵活,事务控制范围是整个方法
  • 不支持读操作

综合了以上的对比,两种方法各有优缺点,但个人更偏向于使用execute方法。如果使用@Transactional这种注解式方法,有个建议是初始化两个RedisTemplate,一个支持事务的,一个不支持事务的,即enableTransactionSupport一个设为true,一个设为false(默认是false)。不然,如果用支持事务的RedisTemplate来进行非事务性操作时,有些地方要注意,比如要手工的关闭连接等,不然是会踩坑的!因为我就是过来人!至于是什么坑,留作下回分解~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章