我相信很多朋友都嘗試寫過讀寫分離插件,或者項(xiàng)目中用到過。首先讀寫分離的職責(zé)應(yīng)該屬于數(shù)據(jù)訪問層而不是業(yè)務(wù)層,其次讀寫分離不應(yīng)該侵入我們代碼層中。因此在 service—dao—orm— 數(shù)據(jù)庫驅(qū)動調(diào)用鏈中,要想插件不侵入我們的代碼中,只能寫在 orm 層和數(shù)據(jù)庫驅(qū)動層,寫在 orm 層就和具體 orm 框架耦合,寫在數(shù)據(jù)庫驅(qū)動層,就和具體數(shù)據(jù)庫耦合。
在 orm 層實(shí)現(xiàn)讀寫分離還是在數(shù)據(jù)庫驅(qū)動實(shí)現(xiàn)讀寫分離,主要看更換 orm 框架和數(shù)據(jù)庫那個成本更高和實(shí)現(xiàn)難易程度。在此處不討論那個更優(yōu),今天介紹的讀寫分離插件是基于 mybatis 框架實(shí)現(xiàn)的一寫多讀?;?springboot 配置,因此在現(xiàn)有項(xiàng)目中集成非常方便,下載源碼打成 jar 包引入到項(xiàng)目中,在 springboot 的配置文件中添加如下配置即可開啟讀寫分離。
此插件一共做了三件事:數(shù)據(jù)源代理,數(shù)據(jù)源路由,分布式事務(wù)。
此插件對現(xiàn)有代碼零侵入,要達(dá)到零侵入得益于代理模式。
首先數(shù)據(jù)源代理,讀寫分離在一個業(yè)務(wù)里面至少有兩個數(shù)據(jù)源,讀數(shù)據(jù)源,寫數(shù)據(jù)源,但是在一個事務(wù)里面所有 sql 執(zhí)行都是在同一個數(shù)據(jù)庫連接下操作,因此需要實(shí)現(xiàn) DataSorce 接口代理讀寫數(shù)據(jù)源: DataSourceProxy。DataSourceProxy 類寫操作時,返回寫數(shù)據(jù)源的 Connection,讀操作時,返回讀數(shù)據(jù)源的 Connection。然而讀寫操作,要在真正執(zhí)行數(shù)據(jù)庫操作時才能確定,然而在真正在執(zhí)行 sql 語句之前,就已經(jīng)獲取 Connection 操作,因此獲取 Connection 操作時,應(yīng)該返回一個代理的Connection,再實(shí)際執(zhí)行 sql 語句時根據(jù)當(dāng)前環(huán)境獲取真實(shí)的 Connetion。
因此 DataSourceProxy 返回的 Connection 是一個代理類, 依賴一個 DataSourceRout 接口,在未執(zhí)行sql語句之前都是由 Connection 代理類完成操作。再執(zhí)行 sql 語句時,由 DataSourceRout 接口返回具體 Connection 執(zhí)行 sql 語句,DataSourceRout 接口只有一個 getTargetDataSource 方法,由具體實(shí)現(xiàn)類根據(jù)當(dāng)前環(huán)境確定目標(biāo)數(shù)據(jù)源,可能是讀寫數(shù)據(jù)源,也可能是分表后的具體目標(biāo)數(shù)據(jù)源。
DataSourceRout 接口目前有兩個實(shí)現(xiàn)類,AbstractRWDataSourceRout 實(shí)現(xiàn)讀寫分離,UserDataSourceRout 實(shí)現(xiàn)根據(jù)不同的用戶路由到不同的數(shù)據(jù)庫組上。UserDataSourceRout 這個類依賴一組 AbstractRWDataSourceRout,實(shí)現(xiàn)讀寫分離。
具體類結(jié)構(gòu)如下:
將 DataSourceProxy 注入到 org.mybatis.spring.SqlSessionTemplate 里面。Mybatis 便實(shí)現(xiàn)讀寫分離。此時對現(xiàn)有代碼完全透明。當(dāng)然也可以注入到 hibernate 框架中,只不過需要自己實(shí)現(xiàn) DataSourceRout 接口,DataSourceRout 接口的實(shí)現(xiàn)類AbstractRWDataSourceRout 是基于 mybatis 的。
通過 org.mybatis.spring.SqlSessionTemplate 這個類的源碼查詢,org.apache.ibatis.mapping.MappedStatement 這個類里面的 org.apache.ibatis.mapping.SqlCommandType 這個域定義了 mybatis 執(zhí)行 sql 語句類型,可以通過這個類確定當(dāng)前操作是讀操作還是寫操作。
寫一個 mybatis 的插件,在 sql 執(zhí)行過程中通 SqlCommandType 這個類確定當(dāng)前上下文是讀操作還是寫操作。把讀寫標(biāo)記存入上下文中,在 AbstractRWDataSourceRout 這個類中拿取上下文中的讀寫標(biāo)記返回對應(yīng)的數(shù)據(jù)源,為了事務(wù)簡單,保證當(dāng)前上下文最多只有一個寫連接和一個讀連接,檢查當(dāng)前上下文是否有對應(yīng)的數(shù)據(jù)庫連接,如果沒有相應(yīng)的連接,獲取連接,保存在當(dāng)前上下文中,方便下次 sql 語句執(zhí)行和事務(wù)執(zhí)行。
數(shù)據(jù)源由原來單一數(shù)據(jù)源變成了一個讀數(shù)據(jù)源和一個寫數(shù)據(jù)源,事務(wù)也就變成了兩個事務(wù)。Mybatis 集成 spring 后,mybatis 的事務(wù)交由 spring 管理,具體實(shí)現(xiàn)類是 org.mybatis.spring.transaction.SpringManagedTransaction,為了和 myabtis-spring 無縫集成,采用代理模式,RWManagedTransaction 繼承 SpringManagedTransaction,把事務(wù)分別委托給讀寫事務(wù),整個線程只有一個讀事務(wù)和一個寫事務(wù),讀事務(wù)比較弱。因此分布式事務(wù)采用 Best Efforts 1PC 模式。
public void commit() throws SQLException {
Map
Connection writeCon = connectionMap.remove(ConnectionHold.WRITE);
if(writeCon != null){
writeCon.commit();
}
Connection readCon = connectionMap.remove(ConnectionHold.READ);
if(readCon != null){
try {
readCon.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
第一個事務(wù)成功后,第二個事務(wù)有可能因?yàn)榫W(wǎng)絡(luò)原因或者服務(wù)器宕機(jī),不能執(zhí)行成功,這個網(wǎng)絡(luò)通訊的危險期雖然概率很小,但是也是個不可靠因素之一。由于整個會話中,只有一個寫數(shù)據(jù)連接和一個讀數(shù)據(jù)連接,讀的事務(wù)性比較弱,只要寫事務(wù)成功了,讀事務(wù)失敗影響不大,當(dāng)然也可以不考慮讀事務(wù)。因此先處理寫事務(wù),再處理讀事務(wù)。
作為一個初級程序員,想要擴(kuò)展現(xiàn)有的開源框架,其實(shí)不是那么困難,只要實(shí)現(xiàn)相應(yīng)的接口,參考現(xiàn)有實(shí)現(xiàn)類實(shí)現(xiàn)接口,如果現(xiàn)有的實(shí)現(xiàn)類太復(fù)雜看不懂邏輯,其實(shí)也很好實(shí)現(xiàn)接口,就是把自己的實(shí)現(xiàn)委托給現(xiàn)有實(shí)現(xiàn)類,自己在委托前后做一些自己的業(yè)務(wù)邏輯。
此插件代碼托管在
https://github.com/chenlei2/spring-boot-mybatis-rw
歡迎大家 fork。
本文作者:陳雷(點(diǎn)融黑幫),promotion 后端開發(fā)人員,熱愛計(jì)算機(jī),崇尚開源。