之前使用Spring Data Redis的时候,由于使用不当,导致redis连接不释放的血案。现在来总结下Spring Data Redis事务的两种使用方式。
我们都知道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();
....
}
然而,它们是不能单独直接被调用的,Spring Data官网有句话说:
Redis provides support for transactions through the
multi
,exec
, anddiscard
commands. These operations are available onRedisTemplate
. 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。这是因为这几个操作都不是在一个连接完成的。
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);//把连接从当前线程中解绑、释放连接
}
}
另一种实现事务的是@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,所以使用的时候要多加注意!
综合了以上的对比,两种方法各有优缺点,但个人更偏向于使用execute方法。如果使用@Transactional这种注解式方法,有个建议是初始化两个RedisTemplate,一个支持事务的,一个不支持事务的,即enableTransactionSupport一个设为true,一个设为false(默认是false)。不然,如果用支持事务的RedisTemplate来进行非事务性操作时,有些地方要注意,比如要手工的关闭连接等,不然是会踩坑的!因为我就是过来人!至于是什么坑,留作下回分解~