之前使用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
, 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。這是因爲這幾個操作都不是在一個連接完成的。
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來進行非事務性操作時,有些地方要注意,比如要手工的關閉連接等,不然是會踩坑的!因爲我就是過來人!至於是什麼坑,留作下回分解~