@Transactional 竟也能解決分布式事務(wù)?
前天朋友咨詢過我一個問題,大致內(nèi)容如下:
這位讀者什么意思呢?簡單的總結(jié)下:在Sharding-JDBC中明明只是簡單的使用@Transactional這個本地事務(wù)注解,為什么在跨庫插入數(shù)據(jù)時候卻能夠同時回滾?
我們知道單數(shù)據(jù)節(jié)點(diǎn)的情況下保持事務(wù)是非常簡單的,只需要使用本地事務(wù)即可輕松解決,比如常用的注解:@Transactional。
但是在分庫后將會存在跨庫的事務(wù),此時本地事務(wù)還能保證事務(wù)嗎?
這篇文章就以球友的提問來聊一下Sharding-JDBC中的本地事務(wù)。
本地事務(wù)
Sharding-JDBC中的本地事務(wù)可能會讓大家有一個誤解,還是以商品表為例:將商品表根據(jù)商品ID進(jìn)行水平分庫,分為兩個庫,如下:
分庫的配置這里就不貼了,詳情看源碼。
此時向其中批量插入數(shù)據(jù),偽代碼如下:
@Transactional
public int insertBatch(){
for(int i=0;i<10;i++){
insert(product);
.......
}
}
上述案例中使用了@Transactional?開啟了本地事務(wù),但是內(nèi)部在插入數(shù)據(jù)時,Sharding-JDB會根據(jù)product_id?這個分片鍵進(jìn)行分庫,那么這個業(yè)務(wù)方法肯定是跨了DB1、DB2?這兩個庫,@Transactional這個注解能解決嗎?
假象:手動在內(nèi)部模擬拋出異常,還真的是都rollback了。
此時很多人都迷糊了,Sharding-JDBC中的本地事務(wù)真的是可以保證分布式事務(wù)?
真實(shí)結(jié)論:Sharding-JDBC中的本地事務(wù)無法保證分布式事。
Sharding-JDBC中的本地事務(wù)在以下兩種情況是完全支持的:
- 支持非跨庫事務(wù),比如僅分表、在單庫中操作。
- 支持因邏輯異常導(dǎo)致的跨庫事務(wù),比如上述的操作,跨兩個庫插入數(shù)據(jù),插入完成后拋出異常。
本地事務(wù)不支持的情況:
- 不支持因網(wǎng)絡(luò)、硬件異常導(dǎo)致的跨庫事務(wù);例如:同一事務(wù)中,跨兩個庫更新,更新完畢后、未提交之前,第一個庫宕機(jī),則只有第二個庫數(shù)據(jù)提交。
對于因網(wǎng)絡(luò)、硬件異常導(dǎo)致的跨庫事務(wù)無法支持很好理解,在分布式事務(wù)中無論是兩階段還是三階段提交都是直接或者間接滿足以下兩個條件:
- 有一個事務(wù)協(xié)調(diào)者
- 事務(wù)日志記錄
本地事務(wù)并未滿足上述條件,自然是無法支持
為什么邏輯異常導(dǎo)致的跨庫事務(wù)能夠支持?
Spring的本地事務(wù)大家都很了解,也經(jīng)常用,并不支持的跨庫事務(wù),那么為什么Sharding-JDBC中卻能支持呢?
想要了解其中的貓膩必然需要從Sharding-JDBC的源碼入手,下圖是在Sharding-JDBC一條SQL處理的流程:
Sharding-JDBC中的一條SQL會經(jīng)過改寫,拆分成不同數(shù)據(jù)源的SQL,比如一條select語句,會按照其中分片鍵拆分成對應(yīng)數(shù)據(jù)源的SQL,然后在不同數(shù)據(jù)源中的執(zhí)行,最終會提交或者回滾。
想要解釋上述的問題,只需要看ShardingConnection,這是Sharding-JDBC自定義實(shí)現(xiàn)的,繼承關(guān)系如下圖:
可以看到ShardingConnection?繼承了java.sql.Connection,這個類就不必多解釋了,在學(xué)習(xí)JDBC的時候應(yīng)該都有所接觸,直接和數(shù)據(jù)庫打交道的一個類。
想要知道為什么支持跨庫事務(wù)的回滾,肯定要找到其中的rollback方法,如下:
@Override
public void rollback() throws SQLException {
//① 本地事務(wù)
f (TransactionType.LOCAL == transactionType) {
super.rollback();
} else {
//② 非本地事務(wù)
shardingTransactionManager.rollback();
}
}
rollback?的方法中區(qū)分了本地事務(wù)和分布式事務(wù),如果是本地事務(wù)將調(diào)用父類的rollback方法,如下:
//父類:AbstractConnectionAdapter#rollback
@Override
public void rollback() throws SQLException {
//cachedConnections中存儲了數(shù)據(jù)源,這里是ds1/ds2
forceExecuteTemplate.execute(cachedConnections.values(), Connection::rollback);
}
這里是調(diào)用ForceExecuteTemplate#execute()?方法執(zhí)行,其實(shí)內(nèi)部就是遍歷數(shù)據(jù)源去執(zhí)行對應(yīng)的rollback方法,如下:
public void execute(final Collection<T> targets, final ForceExecuteCallback<T> callback) throws SQLException {
Collection<SQLException> exceptions = new LinkedList<>();
for (T each : targets) {
try {
callback.execute(each);
} catch (final SQLException ex) {
exceptions.add(ex);
}
}
throwSQLExceptionIfNecessary(exceptions);
}
看到這里已經(jīng)很明了了,rollback? 在各個數(shù)據(jù)源中回滾且未記錄任何事務(wù)日志,因此在非硬件、網(wǎng)絡(luò)的情況下都是可以正常回滾的,一旦因?yàn)榫W(wǎng)絡(luò)、硬件故障,可能導(dǎo)致某個數(shù)據(jù)源rollback失敗,這樣即使程序恢復(fù)了正常,也無undo日志繼續(xù)進(jìn)行rollback,因此這里就造成了數(shù)據(jù)不一致了。
總結(jié)
僅僅依靠Spring自帶的本地事務(wù)(@Transactional)是無法保證跨庫的分布式事務(wù),不要被Sharding-JDBC的假象迷惑了。
當(dāng)然Sharding-JDBC對于跨庫事務(wù)也是有一定的支持,大致分成三類:
- 強(qiáng)一致性的XA協(xié)議事務(wù)
- 基于Base的柔性事務(wù)
- 通過SPI機(jī)制自定義擴(kuò)展的分布式事務(wù)解決方案
本文只是拋磚引玉簡單的介紹下分庫分表后的事務(wù)處理,后文會針對以上三類方案詳細(xì)介紹一下。