前言
博主github
博主個人博客http://blog.healerjean.com
1、解釋
我們知道互聯網是由非常龐大的用戶組成,所以肯定有非常絕大的請求,這些請求又會產生非常巨大的信息存儲在數據庫中,由於數據量非常巨大,單個數據庫的表示很難容納所有數據,所以就有了分庫分表的需求。 對於數據的拆分主要有兩個方面 :垂直拆分和水平拆分
1.1、垂直拆分
垂直拆分: 根據業務的維度,將原本的一個庫(表)拆分爲多個庫(表〉,每個庫(表)
與原有的結構不同。
1.1.1、垂直分表
也就是“大表拆小表”,基於列字段進行的。一般是表中的字段較多,將不常用的, 數據較大,長度較長(比如text類型字段)的拆分到“擴展表“。 一般是針對那種幾百列的大表,也避免查詢時,數據量太大造成的“跨頁”問題。
1.1.2、垂直分庫
垂直分庫針對的是一個系統中的不同業務進行拆分,按照業務把不同的數據放到不同的庫中。其實在一個大型而且臃腫的數據庫中表和表之間的數據很多是沒有關係的,比如用戶User一個庫,商品Producet一個庫,訂單Order一個庫。 切分後,要放在多個服務器上,而不是一個服務器上
1.2、水平拆分
水平拆分: 根據分片(sharding )算法,將一個庫(表)拆分爲多個庫(表),每個庫(表)依舊保留原有的結構。
1.2.1、水平分表
針對數據量巨大的單張表(比如訂單表),按照某種規則(
Hash取模
、地理區域
、時間
等),切分到多張表裏面去。 但是這些表還是在同一個庫中
**結果:分表能解決數據量過大造成的查詢效率低下的問題 **
問題:但是無法有效解決數據的併發訪問能力。,所以庫級別的數據庫操作還是有IO瓶頸。不建議採用。
1.2.2、水平分庫+分表
將數據庫拆分,提高數據庫的寫入能力就是所謂的分庫。將單張表的數據切分到多個數據庫中,表的結構是一樣的 。
結果: 水平分庫分表能夠有效的緩解單機和單庫的性能瓶頸和壓力,突破IO、連接數、硬件資源等的瓶頸。
1.3、水平分庫分表的規則
路由:通過分庫分表規則查找到對應的表和庫的過程叫作路由。例如,分庫分表的規則是user_id % 4,當用戶新註冊了一個賬號時,假設用戶的ID是123,我們就可以通過123 % 4 = 3確定此賬號應該被保存在User3表中。當ID爲123的用戶登錄時,我們可通過123 % 4 = 3計算後,確定其被記錄在User3中。
1.3.1 、Hash取模
對hash結果取餘數 (hash() mod N):對機器編號從0到N-1,按照自定義的hash()算法,對每個請求的hash()值按N取模,得到餘數i,然後將請求分發到編號爲i的機器
使用場景:哈希分片常常應用於數據沒有時效性的情況
有一家公司在一年內能做10億條交易,假設每個數據庫分片能夠容納5000萬條數據,則至少需要20個表才能容納10億條交易。在路由時,我們根據交易ID進行哈希取模來找到數據屬於哪個分片,因此,在設計系統時要充分考慮如何設計數據庫的分庫分表的路由規則。
1.3.2 、地理區域
比如按照華東,華南,華北這樣來區分業務
使用場景:比如我們購買ECS服務器數據,以及阿里雲圖片服務器等。
1.3.3 、時間
按照時間切分,就是將6個月前,甚至一年前的數據切出去放到另外的一張表,因爲隨着時間流逝,這些表的數據 被查詢的概率變小,所以沒必要和“熱數據”放在一起,這個也是“冷熱數據分離”。
使用場景 :切片方式適用於有明顯時間特點的數據,
比如一個用戶的訂單交易數據,我們可以根據月或者季度進行切片,具體由交易數據量來決定以什麼樣的時間週期進行切割
2、分庫分表後的問題
2.1、分頁問題
分庫後,有些分頁查詢需要遍歷所有庫。 舉個分頁的例子,比如要求按時間順序展示某個商家的訂單,每頁100條記錄,假設庫數量是8,我們來看下分頁處理邏輯:
2.1.1、全局視野法:
如果取第1頁數據,則需要從每個庫裏按時間順序取前100條記錄,8個庫彙總後有800條,然後對這800條記錄在應用裏進行二次排序,最後取前100條。
如果取第10頁數據,則需要從每個庫裏取前1000(100*10)條記錄,彙總後有8000條記錄,然後對這8000條記錄二次排序後取(900,1000)條記錄。
分庫情況下,對於第k頁記錄,每個庫要多取100*(k-1)條記錄,所有庫加起來,多取的記錄更多,所以越是靠後的分頁,系統要耗費更多內存和執行時間。
優點:對比沒分庫的情況,無論取那一頁,都只要從單個DB裏取100條記錄,而且無需在應用內部做二次排序,非常簡單。
缺點:每個分庫都需要返回更多的數據,增大網絡傳輸量;除了數據庫要按照time排序,服務層也需要二次排序,損耗性能;隨着頁碼的增大,性能極具下降,數據量和排序量都將大增
2.1.2、業務折中
禁止跳頁查詢,不提供“直接跳到指定頁面”的功能,只提供下一頁的功能。正常來講,不管哪一個分庫的第3頁都不一定有全局第3頁的所有數據,例如一下三種情況:
1、先找到上一頁的time的最大值(可從前臺傳入),作爲第二頁數據拉去的查詢條件,只取每頁的記錄數
2、這樣服務層還是獲得兩頁數據,再做一次排序,獲取一頁數據。
3、改進了不會因爲頁碼增大而導致數據的傳輸量和排序量增大
2.1.3、允許數據精度丟失:
需要考慮業務員上是否接受在頁碼較大是返回的數據不是精準的數據。
在數據量較大,且ID映射分佈足夠隨機的話,應該是滿足等概率分佈的情況的,所以取一頁的數據,我們在每個數據庫中取(每頁數據/數據庫數量)個數據。 當然這樣的到的結果並不是精準的,但是當實際業務可以接受的話, 此時的技術方案的複雜度變大大降低。也不需要服務層內存排序了。
2.1.4、二次查詢法
2 個數據庫,假設一頁只有5條數據,查詢第200頁的SQL語句爲
select * from T order by time limit 1000 5;
- 講sql改寫爲
select * from T order by time limit 500 5;
注意這裏的500=1000/分表數量,並將這個sql下發至每個分庫分表中執行,每個分庫返回這個sql執行的結果。
- 找到所有分庫返回結果的time的最小值
第一個庫,5條數據的time最小值是1487501123
第二個庫,5條數據的time最小值是1487501223
故,三頁數據中,time最小值來自第一個庫,time_min=1487501123,這個過程只需要比較各個分庫第一條數據,時間複雜度很低
- 查詢二次改寫,第二次要改寫成一個between語句,between的起點是time_min,between的終點是原來每個分庫各自返回數據的最大值:
第一個分庫,第一次返回數據的最大值是1487501523
所以查詢改寫爲select * from T order by time where time between time_min and 1487501523
第二個分庫,第一次返回數據的最大值是1487501699
所以查詢改寫爲select * from T order by time where time between time_min and 1487501699
從上面圖片可以看出,DB1比第一次查出來的數據多了兩行,應爲查詢的範圍擴大了
- 計算time_min這條記錄在全局的偏移量
從而我們得知time_min這條記錄在全局的偏移量值=500+497=997,其實也就是說,我們的第1000條記錄的終點是time=1487501128
- 獲取最終結果,講第二次查詢出的進行排序,最終獲得結果
**優點:可以精確的返回業務所需數據,每次返回的數據量都非常小,不會隨着翻頁增加數據的返回量。 **
缺點:需要進行兩次數據庫查詢
2.2、Join問題
互聯網公司的業務,往往是併發場景多,DB查詢頻繁,有一定用戶規模後,往往要做分庫分表。 分庫分表Join肯定是不行的
2.2.1、不使用join的原因:
1、join
的話,是走嵌套查詢的。小表驅動大表,且通過索引字段進行關聯。如果表記錄比較少的話,還是OK的當表處於百萬級別後,join導致性能下降;
2、分佈式的分庫分表。這種時候是不建議跨庫join的。目前mysql的分佈式中間件,跨庫join表現不良。
3、join
寫的sql語句要修改,不容易發現,成本比較大,當系統比較大時,不好維護。
4、 數據庫是最底層的,一個系統性能好壞的瓶頸往往是數據庫。建議數據庫只是作爲數據存儲的工具,而不要添加業務上去。
2.2.2、不使用join的解決方法:
應用層面解決 :可以更容易對數據進行分庫,更容易做到高性能和可擴展。(記得在小米金融供應鏈關聯查詢賣方,賣方 核心企業,授信企業的使用,就是這樣,本來其實是兩個企業,但是卻有4個子段表示join查詢肯定是不好的)
緩存的效率更高。許多應用程序可以方便地緩存單表查詢對應的結果對象。,如果某個表很少改變,那麼基於該表的查詢就可以重複利用查詢緩存結果了。單表查詢出數據後,作爲條件給下一個單表查詢
查詢本身效率也可能會有所提升。查詢id集的時候,使用IN()代替關聯查詢,可以讓MySQL按照ID順序進行查詢,這可能比隨機的關聯要更高效。mysql對in的數量沒有限制,mysql限制整條sql語句的大小。通過調整參數max_allowed_packet ,可以修改一條sql的最大值。建議在業務上做好處理,限制一次查詢出來的結果集是能接受的,但是最好不要超過500條(小米規範)
可以減少多次重複查詢。在應用層做關聯查詢,意味着對於某條記錄應用只需要查詢一次,而在數據庫中做關聯查詢,則可能需要重複地訪問一部分數據。從這點看,這樣的重構還可能會減少網絡和內存的消豔。
Map<Long, CompanyDTO> map = companyDTOS.stream().collect(
Collectors.toMap(item -> item.getCompanyId(), item -> item));
Map<Long, Integer> poolCountMap = scfLoanCreditPoolMatchManager.countGroupByPoolId(scfLoanCreditPoolMatchQuery);
collect = data.stream().map(temp -> {
LoanCreditPoolDTO item = BeanUtils.loanCreditPoolToDTO(temp);
item.setCoreCompanyName(map.get(item.getCoreCompanyId()).getCompanyName()) ;
item.setCreditCompanyName(map.get(item.getCreditCompanyId()).getCompanyName()) ;
) );
return item ;
}).collect(Collectors.toList());
}
2.3、分組 :查出來再計算
分組實現較簡單,只需對128張表各自進行group by ,將128張表的結果,全都取到內存中,進行合併,如果有having條件再根據合併的結果進行篩選。
2.4、其他如sum,avg,max等方法
查出來再計算
avg :在分片的環境中,以
avg1
+avg2
+avg3
/3計算平均值並不正確,需要改寫爲(sum1+sum2+sum3)/(count1+count2+ count3
)。這就需要將包含avg的SQL
改寫爲sum
和count
,然後再結果歸併時重新計算平均值。
3、事務
分庫分表後,就成了分佈式事務了。
如果依賴數據庫本身的分佈式事務管理功能去執行事務,將付出高昂的性能代價;
如果由應用程序去協助控制,形成程序邏輯上的事務,又會造成編程方面的負擔。
3.1、傳統事務
3.1.1、特性
(1) 原子性(Atomicity)
原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾,這和前面兩篇博客介紹事務的功能是一樣的概念,因此事務的操作如果成功就必須要完全應用到數據庫,如果操作失敗則不能對數據庫有任何影響。
(2)一致性(Consistency)
一致性是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行 之後都必須處於一致性狀態。
拿轉賬來說,假設用戶A和用戶B兩者的錢加起來一共是5000,那麼不管A和B之間如何轉賬,轉幾次賬,事務結束後兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。
(3)隔離性(Isolation)
隔離性是當多個用戶併發訪問數據庫時,比如操作同一張表時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
即要達到這麼一種效果:對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始之前就已經結束,要麼在T1結束之後纔開始,這樣每個事務都感覺不到有其他事務在併發地執行。
(4)持久性(Durability)
持久性是指一個事務一旦被提交了,那麼對數據庫中的數據的改變就是永久性的,即便是在數據庫系統遇到故障的情況下也不會丟失提交事務的操作。
3.1.2、事務方法回顧
我們知道,當
dbTransactional
執行的時候,不管是userService.insert
還是companyService.insert
出現了異常,dbTransactional
都可以整體回滾,達到原子操作的效果,其主要原因是
userService.insert
和companyService.insert
共享了同一個Connection,這是spring底層通過ThreadLocal
緩存了Connection
實現的。
@Transactional(rollbackFor = Exception.class)
@Override
public void dbTransactional(UserDTO userDTO, CompanyDTO companyDTO) {
userService.insert(userDTO);
companyService.insert(companyDTO);
}
public interface UserService {
UserDTO insert(UserDTO userDTO);
}
public interface CompanyService {
CompanyDTO insert(CompanyDTO companyDTO);
}
3.2、sharding-jdbc
事務
public enum TransactionType {
LOCAL,//本地事務
XA, //二階段事務
BASE;//
private TransactionType() {
}
}
本地事務 | 兩階段提交 | 柔性事務 | |
---|---|---|---|
業務改造 | 無 | 無 | 實現相關接口 |
一致性 | 不支持 | 支持 | 最終一致 |
隔離性 | 不支持 | 支持 | 業務方保證(規劃中) |
併發性能 | 無影響 | 嚴重衰退 | 略微衰退 |
適合場景 | 業務方處理不一致 | 短事務 & 低併發 | 長事務 & 高併發 |
3.2.1、LOCAL
之本地事務(默認)
如果不使用柔性事務,默認提供的是本地事務(弱
XA
事務支持) ,基於弱XA
的事務無需額外的實現成本,因此Sharding-Sphere
默認支持。
3.2.1.1、特性
1、完全支持非跨庫事務,例如:僅分表,或分庫但是路由的結果在單庫中。
2、完全支持因邏輯異常導致的跨庫事務。例如:同一事務中,跨兩個庫更新。更新完畢後,拋出空指針,則兩個庫的內容都能回滾。
3、不支持因網絡、硬件異常導致的跨庫事務。例如:同一事務中,跨兩個庫更新,更新完畢後、未提交之前,第一個庫死機(可以理解爲網絡導致的,但是程序認爲提交無誤),則只有第二個庫數據提交。
3.2.1.2、理解
3.2.1.2.1、正常流程
這是一個非常常見流程,一個總連接處理了多條sql
語句,最後一次性提交整個事務,每一條sql
語句可能會分爲多條子sql
分庫分表去執行,這意味着底層可能會關聯多個真正的數據庫連接,我們先來看看如果一切正常,commit會如何去處理。
@Transactional(rollbackFor = Exception.class)
@Override
public void dbTransactional(UserDTO userDTO, CompanyDTO companyDTO) {
userService.insert(userDTO);
companyService.insert(companyDTO);
}
public interface UserService {
UserDTO insert(UserDTO userDTO);
}
public interface CompanyService {
CompanyDTO insert(CompanyDTO companyDTO);
}
在進入
dbTransactional
初始化才初始化事務管理器DataSourceTransactionManager等
(因爲是多個數據源的情況) ,在方法結束的時候多個數據源連接統一commit
public final class ShardingConnection extends AbstractConnectionAdapter {
@Override
public void commit() throws SQLException {
if (TransactionType.LOCAL == transactionType) {//local 本地事務
super.commit();
} else {
shardingTransactionManager.commit();
}
}
}
public abstract class AbstractConnectionAdapter
extends AbstractUnsupportedOperationConnection {
public void commit() throws SQLException {
this.forceExecuteTemplate.execute(this.cachedConnections.values(), //所有數據庫連接
new ForceExecuteCallback<Connection>() {
public void execute(Connection connection) throws SQLException {
connection.commit();//一個一個commit提交
}
});
}
}
cachedConnections
public final class ForceExecuteTemplate<T> {
public void execute(Collection<T> targets, ForceExecuteCallback<T> callback)
throws SQLException {
Collection<SQLException> exceptions = new LinkedList();
Iterator var4 = targets.iterator();
while(var4.hasNext()) {
Object each = var4.next();
try {
callback.execute(each);
} catch (SQLException var7) {
exceptions.add(var7);
}
}
this.throwSQLExceptionIfNecessary(exceptions);
}
}
到了這裏會發現一個個進行commit操作,如果任何一個出現了異常,直接捕獲異常,但是也只是捕獲而已,然後接着下一個連接的commit,這也就很好的說明了下面兩點。異常情況看後面
3.2.1.2.2、異常流程
如果已經到了commit這一步的話,如果因爲網絡原因導致的commit
失敗了,是不會影響到其他連接的。
如果在整個方法結束的時候之前出現了邏輯異常(i = 1/0
),則不會執行commit
,而是直接執行回滾rollback
方法,如下 (有個問題:callback出現網絡異常怎麼辦呢。反正肯定不會入庫的)
public final class ShardingConnection extends AbstractConnectionAdapter {
@Override
public void rollback() throws SQLException {
if (TransactionType.LOCAL == transactionType) {//local 本地事務
super.rollback();
} else {
shardingTransactionManager.rollback();
}
}
}
public abstract class AbstractConnectionAdapter
extends
AbstractUnsupportedOperationConnection {
public void rollback() throws SQLException {
this.forceExecuteTemplate.execute(this.cachedConnections.values(), //所有的數據庫連接
new ForceExecuteCallback<Connection>() {
public void execute(Connection connection) throws SQLException {
connection.rollback();//數據庫回滾(一個一個回滾)
}
});
}
}
public final class ForceExecuteTemplate<T> {
public void execute(Collection<T> targets, ForceExecuteCallback<T> callback)
throws SQLException {
Collection<SQLException> exceptions = new LinkedList();
Iterator var4 = targets.iterator();
while(var4.hasNext()) {
Object each = var4.next();
try {
callback.execute(each);
} catch (SQLException var7) {
exceptions.add(var7);
}
}
this.throwSQLExceptionIfNecessary(exceptions);
}
}
3.2.2、XA
:2階段事務
對數據庫分佈式事務有了解的同學一定知道數據庫支持的
2PC
,又叫做XA Transactions
。
XA
是一個兩階段提交協議,該協議分爲以下兩個階段:第一階段:事務協調器要求每個涉及到事務的數據庫預提交(
precommit
)此操作,並反映是否可以提交.第二階段:事務協調器要求每個數據庫提交數據。
其中,如果有任何一個數據庫否決此次提交,那麼所有數據庫都會被要求回滾它們在此事務中的那部分信息。這樣做的缺陷是什麼呢?
首先需要了解一個定理 :CAP定理
分佈式有一個定理:CAP原則又稱CAP定理,指的是在一個分佈式系統中,一致性(Consistency)、可用性(Availability)、分區容錯性(Partition tolerance)。CAP 原則指的是,這三個要素最多隻能同時實現兩點,不可能三者兼顧,到底要什麼根據情況看。
CAP理論就是說在分佈式存儲系統中,最多隻能實現上面的兩點。而由於網絡硬件肯定會出現延遲丟包等問題,所以分區容錯性是我們必須需要實現的。所以我們只能在一致性和可用性之間進行權衡
一致性和可用性,爲什麼不可能同時成立?答案很簡單,因爲可能通信失敗(即出現分區容錯)。
咋看之下我們可以在數據庫分區之間獲得一致性。但是仔細想想,如果數據庫都特別多,這種方案就是犧牲了一定的可用性換取一致性
如果說系統的可用性代表的是執行某項操作相關所有組件的可用性的和。那麼在兩階段提交的過程中,可用性就代表了涉及到的每一個數據庫中可用性的和。
我們假設兩階段提交的過程中每一個數據庫都具有99.9%的可用性,那麼如果兩階段提交涉及到兩個數據庫,這個結果就是99.8%。根據系統可用性計算公式,假設每個月43200分鐘,99.9%的可用性就是43157分鐘, 99.8%的可用性就是43114分鐘,相當於每個月的宕機時間增加了43分鐘。
在分佈式系統中,我們往往追求的是可用性,它的重要性比一致性要高(不一定哦,我的是金融,必須一致性高),那麼如何實現高可用性呢?前人已經給我們提出來了另外一個理論,就是BASE理論,具體看下面的Base
3.2.2.2、特性
1、支持數據分片後的跨庫XA事務
2、兩階段提交保證操作的原子性和數據的強一致性
3、服務宕機重啓後,提交/回滾中的事務可自動恢復
4、SPI機制整合主流的XA事務管理器,默認Atomikos,可以選擇使用Narayana和Bitronix
5、同時支持XA和非XA的連接池
6、提供spring-boot和namespace的接入端
優點:實現比較簡單,儘量保證了數據的強一致,適合對數據強一致要求很高的關鍵領域,比如我們金融業務。(其實也不能100%保證強一致)
缺點: 犧牲了可用性,對性能影響較大,不適合高併發高性能場景,如果分佈式系統跨接口調用,在事務執行過程中,所有的資源都是被鎖定的,這種情況只適合執行時間確定的短事務。 而且因爲2PC的協議成本比較高,又有全局鎖的問題,性能會比較差。 現在大家基本上不會採用這種強一致解決方案。
3.2.2.2、配置XA
:2階段事務
依賴
<!-- 分表分庫 ShardingShpere -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC2</version>
</dependency>
<!--XA事務必須配置如下,否則如下報錯-->
<!--Caused by: java.lang.NullPointerException: Cannot find transaction manager of [XA]-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
<version>4.0.0-RC2</version>
</dependency>
配置方法(配合@Transactional
註解使用)
方式一:註解
@ShardingTransactionType(value = TransactionType.XA)
非常抱歉的是,我使用註解沒有成功,所以我選擇了第二種方式,在進入這個事務方法的時候,用代碼控制
@Transactional(rollbackFor = Exception.class)
@ShardingTransactionType(value = TransactionType.XA)
@Override
public void dbTransactional(UserDTO userDTO, CompanyDTO companyDTO) {
System.out.println("----------------開始進入事務");
userService.insert(userDTO);
companyService.insert(companyDTO);
}
方式二:Java代碼
當然可以自己自定義一個註解,用來實現下面的
TransactionTypeHolder.set(TransactionType.XA);
3.2.2.2.1、正常流程
@Transactional(rollbackFor = Exception.class)
@ShardingTransactionType(value = TransactionType.XA)
@Override
public void dbTransactional(UserDTO userDTO, CompanyDTO companyDTO) {
System.out.println("----------------開始進入事務");
userService.insert(userDTO);
companyService.insert(companyDTO);
}
事務方法,剛進入開啓事務
public abstract class AbstractConnectionAdapter
extends AbstractUnsupportedOperationConnection {
public final void setAutoCommit(final boolean autoCommit) throws SQLException {
this.autoCommit = autoCommit;
if (TransactionType.LOCAL == transactionType || isOnlyLocalTransactionValid()) {
setAutoCommitForLocalTransaction(autoCommit);
} else if (!autoCommit) {
shardingTransactionManager.begin();//事務管理器開始
}
}
}
public final class XAShardingTransactionManager implements ShardingTransactionManager {
@SneakyThrows
@Override
public void begin() {
xaTransactionManager.getTransactionManager().begin();
}
}
事務方法結束的時候,事務管理器提交事務,清除XAResource
public final class XAShardingTransactionManager implements ShardingTransactionManager {
@SneakyThrows
@Override
public void commit() {
try {
xaTransactionManager.getTransactionManager().commit();//事務管理器提交,實現類爲AtomikosTransactionManager
} finally {
enlistedXAResource.remove();
}
}
}
public final class AtomikosTransactionManager implements XATransactionManager {
}
3.2.2.2.2、異常流程
@Transactional(rollbackFor = Exception.class)
@Override
public void dbTransactional(UserDTO userDTO, CompanyDTO companyDTO) {
System.out.println("----------------開始進入事務");
userService.insert(userDTO);
companyService.insert(companyDTO);
int i = 1 / 0;
}
事務開啓和上面正常流程一樣,如果發了異常情況,就會會館,具體執行操作,看下文
public final class ShardingConnection extends AbstractConnectionAdapter {
@Override
public void rollback() throws SQLException {
if (TransactionType.LOCAL == transactionType) {
super.rollback();
} else {
shardingTransactionManager.rollback();
}
}
}
public void rollback() {
try {
try {
this.xaTransactionManager.getTransactionManager().rollback();
} finally {
this.enlistedXAResource.remove();
}
} catch (Throwable var5) {
throw var5;
}
}
3.2.2.2.3、原理分析
1、Begin(開啓XA
全局事務)
通常收到接入端的
set autoCommit=0時,
XAShardingTransactionManager會調用具體的
XA事務管理器開啓XA
的全局事務,通常以XID
的形式進行標記。
2、執行物理SQL
ShardingSphere
進行解析/優化/路由後,會生成邏輯SQL
的分片SQLUnit
,執行引擎爲每個物理SQL
創建連接的同時,物理連接所對應的XAResource也
會被註冊到當前XA事務
中,事務管理器會在此階段發送XAResource.start
命令給數據庫,數據庫在收到XAResource.end
命令(個人可以理解爲連接試探)之前的所有SQL
操作,會被標記爲XA事務
。
XAResource1.start ## Enlist階段執行
statement.execute("sql1"); ## 模擬執行一個分片SQL1
statement.execute("sql2"); ## 模擬執行一個分片SQL2
XAResource1.end
這裏sql1和sql2將會被標記爲XA事務。
3、Commit/rollback(提交XA事務
)
XAShardingTransactionManager
收到接入端的提交命令後,會委託實際的XA事務管理
進行提交動作,這時事務管理器會收集當前線程裏所有註冊的XAResource
,首先發送XAResource.end
指令,
用以標記此XA
事務的邊界。 接着會依次發送prepare
指令,收集所有參與XAResource
投票,如果所有XAResource
的反饋結果都是OK,則會再次調用commit指令進行最終提交,
如果有一個XAResource
的反饋結果爲No,則會調用rollback
指令進行回滾。 在事務管理器發出提交指令後,任何XAResource
產生的異常都會通過recovery日誌進行重試,來保證提交階段的操作原子性,和數據強一致性。
XAResource1.prepare ## ack: yes
XAResource2.prepare ## ack: yes
XAResource1.commit
XAResource2.commit
XAResource1.prepare ## ack: yes
XAResource2.prepare ## ack: no
XAResource1.rollback
XAResource2.rollback
3.2.3、Saga:BASE(柔性)事務
這個以後再看吧,朋友。
Basically Available(基本可用)
Soft state(軟狀態)
Eventually consistent(最終一致性)
3.2.3.2、配置
<!-- saga柔性事務 -->
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-transaction-base-saga</artifactId>
<version>${shardingsphere-spi-impl.version}</version>
</dependency>
${shardingsphere-spi-impl.version} 的jar暫未發佈到maven中央倉,因此需要您根據源碼自行部署。項目地址: shardingsphere-spi-impl
可以通過在項目的classpath
中添加saga.properties
來定製化Saga事務的配置項。當saga.persistence.enabled=true
時,事務日誌默認按JDBC的方式持久化到數據庫中
也可以通過實現io.shardingsphere.transaction.saga.persistence.SagaPersistence
SPI
,支持定製化存儲,具體可參考項目sharding-transaction-base-saga-persistence-jpa
。
配置項的屬性及說明如下:
屬性名稱 | 默認值 | 說明 |
---|---|---|
saga.actuator.executor.size | 5 | 使用的線程池大小 |
saga.actuator.transaction.max.retries | 5 | 失敗SQL的最大重試次數 |
saga.actuator.compensation.max.retries | 5 | 失敗SQL的最大嘗試補償次數 |
saga.actuator.transaction.retry.delay.milliseconds | 5000 | 失敗SQL的重試間隔,單位毫秒 |
saga.actuator.compensation.retry.delay.milliseconds | 3000 | 失敗SQL的補償間隔,單位毫秒 |
saga.persistence.enabled | false | 是否對日誌進行持久化 |
saga.persistence.ds.url | 無 | 事務日誌數據庫JDBC連接 |
saga.persistence.ds.username | 無 | 事務日誌數據庫用戶名 |
saga.persistence.ds.password | 無 | 事務日誌數據庫密碼 |
saga.persistence.ds.max.pool.size | 50 | 事務日誌連接池最大連接數 |
saga.persistence.ds.min.pool.size | 1 | 事務日誌連接池最小連接數 |
saga.persistence.ds.max.life.time.milliseconds | 0(無限制) | 事務日誌連接池最大存活時間,單位毫秒 |
saga.persistence.ds.idle.timeout.milliseconds | 60 * 1000 | 事務日誌連接池空閒回收時間,單位毫秒 |
saga.persistence.ds.connection.timeout.milliseconds | 30 * 1000 | 事務日誌連接池超時時間,單位毫秒 |
Saga事務日誌表:
-- MySQL init table SQL
CREATE TABLE IF NOT EXISTS saga_event(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
saga_id VARCHAR(255) null,
type VARCHAR(255) null,
content_json TEXT null,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX saga_id_index(saga_id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8
在classpath中添加schema-init.sql
可以定日誌表,Saga引擎會完成初始化建表操作。
3.2.3.1、原理分析
1、Init(Saga引擎初始化)
包含Saga柔性事務的應用啓動時,saga-actuator引擎會根據saga.properties
的配置進行初始化的流程。
2、Begin(開啓Saga全局事務)
每次開啓Saga全局事務時,將會生成本次全局事務的上下文(SagaTransactionContext
),事務上下文記錄了所有子事務的正向SQL和逆向SQL,作爲生成事務調用鏈的元數據使用。
3、執行物理SQL
在物理SQL執行前,ShardingSphere根據SQL的類型生成逆向SQL,這裏是通過Hook的方式攔截Parser的解析結果進行實現。
4、Commit/rollback(提交Saga事務)
提交階段會生成Saga執行引擎所需的調用鏈路圖,commit操作產生ForwardRecovery(正向SQL補償)任務,rollback操作產生BackwardRecovery任務(逆向SQL補償)。
3.2.4、Seata:BASE(柔性)事務
3.2.4.1、配置
1、按照seata-work-shop中的步驟,下載並啓動seata server
,參考 Step6 和 Step7即可。
2、在每一個分片數據庫實例中執創建undo_log表(目前只支持Mysql
)
CREATE TABLE IF NOT EXISTS `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
3.在classpath
中修改seata.conf
client {
application.id = raw-jdbc ## 應用唯一id
transaction.service.group = raw-jdbc-group ## 所屬事務組
}