LCN解決分布式事務原理解析+項目實戰(原創精華版)


前言

  SpringCloud分布式架構給我們帶來開發上的便利,同時增加了我們對事務管理的難度,微服務的遍地開花,本地事務已經無法滿足分布式的要求,由此分布式事務問題誕生。 分布式事務被稱為世界性的難題。
  更多分布式事務介紹請看這篇文章:再有人問你分布式事務,把這篇扔給他
  本文記錄整合TX-LCN分布式事務框架管理分布式事務,用的版本是5.0.2.RELEASE

了解分布式事務

分布式事務產生的背景

  在分布式產生之前,互聯網公司的項目都是傳統的單體項目,整個項目都共用了一個數據源,那么進行業務執行對數據庫進行操作的時候,不會產生這種事務不一致問題,因為是同一個數據源用的一個本地事務。

  但隨着我們架構的演變,從單體架構到分布式架構到SOA架構項目再到如今公司采用的微服務架構,我們對服務進行了業務拆分,那么數據源也被拆分,一般微服務項目是一個服務對應一個數據源。

  那么在這種架構下,每個服務都有自己獨立的數據源有自己的本地事務,從而便會產生分布式事務,可能造成服務間數據不一致問題。

不同架構體系下解決事務的問題

單體架構(單數據源)

在單體的項目中,多個不同業務邏輯都是在同一個數據源中實現事務管理,是不存在分布式事務的問題,因為同一數據源的情況下都是采用事務管理器,相當於每個事務管理器對應一個數據源

單體架構(多數據源)

在單體的項目中,有多個不同的數據源,每個數據源中都有自己獨立的事務管理器,互不影響,那么這時候也會存在多數據源事務管理:解決方案jta+ Atomikos。

分布式/微服務架構

在分布式/微服務架構,每個服務都有自己獨立的數據源和事務管理器,那么在這種情況下如果有業務要進行RPC遠程調用的時候,那就必然可能產生分布式事務。目前主要解決方案有:MQ、LCN、Seata等方案。

LCN簡介和背景

LCN分布式事務框架其本身並不創建事務,而是基於對本地事務的協調從而達到事務一致性的效果。
LCN框架在2017年6月份發布第一個版本,從開始的1.0,已經發展到了5.0版本。

LCN名稱是由早期版本的LCN框架命名,在設計框架之初的1.0 ~ 2.0的版本時框架設計的步驟是如下,各取其首字母得來的LCN命名。

5.0以后由於框架兼容了LCN、TCC、TXC三種事務模式,為了避免區分LCN模式,特此將LCN分布式事務改名為TX-LCN分布式事務框架。
TX-LCN分布式事務框架,LCN並不生產事務,LCN只是本地事務的協調工,LCN是一個高性能的分布式事務框架,兼容dubbo、springcloud框架,支持RPC框架拓展,支持各種ORM框架、NoSQL、負載均衡、事務補償

定位

TX-LCN定位於一款事務協調性框架,框架其本身並不操作事務,而是基於對事務的協調從而達到事務一致性的效果。(LCN不生產事務,它只是事務的搬運工...woc這有點像農夫山泉的文案)

LCN模式

LCN模式是通過代理Connection的方式實現對本地事務的操作,然后在由TxManager統一協調控制事務。當本地事務提交回滾或者關閉連接時將會執行假操作,該代理的連接將由LCN連接池管理。
該模式的特點:

  • 該模式對代碼的嵌入性為低。
  • 該模式僅限於本地存在連接對象且可通過連接對象控制事務的模塊。
  • 該模式下的事務提交與回滾是由本地事務方控制,對於數據一致性上有較高的保障。
  • 該模式缺陷在於代理的連接需要隨事務發起方一共釋放連接,增加了連接占用的時間。

TCC模式

TCC事務機制相對於傳統事務機制(X/Open XA Two-Phase-Commit),其特征在於它不依賴資源管理器(RM)對XA的支持,而是通過對(由業務系統提供的)業務邏輯的調度來實現分布式事務。主要由三步操作,Try: 嘗試執行業務、 Confirm:確認執行業務、 Cancel: 取消執行業務。

該模式的特點:

  • 該模式對代碼的嵌入性高,要求每個業務需要寫三種步驟的操作。
  • 該模式對有無本地事務控制都可以支持使用面廣。
  • 數據一致性控制幾乎完全由開發者控制,對業務開發難度要求高。

TXC模式

TXC模式命名來源於淘寶,實現原理是在執行SQL之前,先查詢SQL的影響數據,然后保存執行的SQL快走信息和創建鎖。當需要回滾的時候就采用這些記錄數據回滾數據庫,目前鎖實現依賴redis分布式鎖控制。
該模式的特點:

  • 該模式同樣對代碼的嵌入性低。
  • 該模式僅限於對支持SQL方式的模塊支持。
  • 該模式由於每次執行SQL之前需要先查詢影響數據,因此相比LCN模式消耗資源與時間要多。
  • 該模式不會占用數據庫的連接資源。

LCN分布式事務原理(自己理解的,白話文通俗易懂)


  1) 首先我們的lcn協調者(TM)會和lcn客戶端(TC)通過引入的netty一直保持着長連接(持續監聽)。

  2) 當請求的發起方(調用方)進入接口業務之前,會通過AOP技術進到@LcnTransaction注解中去LCN協調者那邊生成注冊一個全局的事務組Id(groupId)。

  3) 當發起方(調用方)通過rpc調用參與方(被調用方)的時候,lcn重寫了Feign客戶端,會從ThreadLocal中拿到該事務組Id(groupId),並將該事務組Id設置到請求頭中。

  4) 參與方(被調用方)在請求頭中獲取到了這個groupId的時候,lcn會標識該服務為參與方並加入到該事務組,並會被lcn代理數據源,當該服務業務邏輯執行完成后,進行數據源的假關閉,並不會真正的提交或回滾當前服務的事務。

  5) 當發起方執行完全部業務邏輯的時候,如果無異常會告知lcn協調者,lcn協調者再分別告訴該請求鏈上的所有參與方可以提交了,再進行真正的提交。若發起方調用完參與方后報錯了,也會告知lcn協調者,lcn協調者再告知所有的參與方進行真正的回滾操作,這樣就解決了分布式事務的問題

快速擼代碼

搭建tx-manager

創建數據庫、表

  • 創建MySQL數據庫, 名稱為:tx-manager(我們直接選擇在我們自己的數據庫下面創建表就行了,這里就不創建這個數據庫)
  • 創建數據表:t_tx_exception
CREATE TABLE `t_tx_exception`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `transaction_state` tinyint(4) NULL DEFAULT NULL,
  `registrar` tinyint(4) NULL DEFAULT NULL,
  `remark` varchar(4096) NULL DEFAULT  NULL,
  `ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 未解決 1已解決',
  `create_time` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

https://img2018.cnblogs.com/blog/1353055/201906/1353055-20190620151602501-133032900.png

下載源碼並編譯

源碼下載地址:https://github.com/codingapi/tx-lcn
工程目錄

修改txlcn-tm的配置文件application.properties

####################### 服務 ############################################
 
spring.application.name=TransactionManager
server.port=7970
 
####################### 數據庫 ############################################
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://47.92.145.192:3306/scm_transaction?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=db_user2
spring.datasource.password=db_pass
# 驗證連接是否有效。此參數必須設置為非空字符串,下面三項設置成true才能生
spring.datasource.validationQuery=SELECT 1
# 指明連接是否被空閑連接回收器(如果有)進行檢驗.如果檢測失敗,則連接將被從池中去除.
spring.datasource.testWhileIdle=true
# 指明是否在從池中取出連接前進行檢驗,如果檢驗失敗,則從池中去除連接並嘗試取出另一個
spring.datasource.testOnBorrow=true
# 指明是否在歸還到池中前進行檢驗
spring.datasource.testOnReturn=false
 
# 以下可省略
# 初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=10
spring.datasource.maxActive=1000
# 配置獲取連接等待超時的時間
spring.datasource.maxWait=60000
#配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
#配置一個連接在池中最小生存的時間,單位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
#打開PSCache,並且指定每個連接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
#配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
spring.datasource.filters=stat,wall,log4j
#通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000;druid.stat.logSlowSql=true
#合並多個DruidDataSource的監控數據
spring.datasource.useGlobalDataSourceStat=true
#spring.datasource.WebStatFilter.exclusions="*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
#spring.datasource.stat-view-servlet.login-username=admin
#spring.datasource.stat-view-servlet.login-password=admin
 
####################### 數據庫方言 ############################################
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
# 第一次運行可以設置為: create, 為TM創建持久化數據庫表
spring.jpa.hibernate.ddl-auto=update
 
####################### Redis ############################################
spring.redis.host=47.92.145.192
spring.redis.port=6379
spring.redis.password=WZTH@dev123
 
####################### 事務 ############################################
# TM監聽IP. 默認為 127.0.0.1
tx-lcn.manager.host=127.0.0.1
# TM監聽Socket端口. 默認為 ${server.port} - 100
tx-lcn.manager.port=8070
# 心跳檢測時間(ms). 默認為 300000
tx-lcn.manager.heart-time=300000
# 分布式事務執行總時間(ms). 默認為36000
tx-lcn.manager.dtx-time=8000
# 參數延遲刪除時間單位ms  默認為dtx-time值
tx-lcn.message.netty.attr-delay-time=${tx-lcn.manager.dtx-time}
# 事務處理並發等級. 默認為機器邏輯核心數5倍
tx-lcn.manager.concurrent-level=160
# TM后台登陸密碼,默認值為codingapi
tx-lcn.manager.admin-key=123456
# 分布式事務鎖超時時間 默認為-1,當-1時會用tx-lcn.manager.dtx-time的時間
tx-lcn.manager.dtx-lock-time=${tx-lcn.manager.dtx-time}
# 雪花算法的sequence位長度,默認為12位.
tx-lcn.manager.seq-len=12
# 異常回調開關。開啟時請制定ex-url
tx-lcn.manager.ex-url-enabled=false
# 事務異常通知(任何http協議地址。未指定協議時,為TM提供內置功能接口)。默認是郵件通知
tx-lcn.manager.ex-url=/provider/email-to/306509906@qq.com

注意:個人修改了數據庫的名稱,和用戶名密碼,根據自己的實際情況修改

啟動txlcn-tm模塊

不知道怎么啟動的可以自行查閱Springboot運行方式
啟動后打開后台地址http://localhost:7970,初始密碼是codingapi,我這里改成了123456

登陸后

搭建Tx-Client

TC端參照官網一步步操作:https://www.txlcn.org/zh-cn/docs/start.html

TC引入依賴

<dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-tc</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-txmsg-netty</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

PS:如果你沒有添加jdbc驅動,啟動的時候會報錯

Parameter 0 of constructor in com.codingapi.txlcn.tc.core.transaction.txc.analy.TableStructAnalyser required a bean of type 'javax.sql.DataSource' that could not be found.

因此要添加jdbc依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

配置文件添加TM地址跟監聽端口,如果TM是默認8070端口,且跟TC部署在同一台機器,可以忽略這個配置,並且開啟日志,開發階段最好開啟日志,並設置為debug等級,這樣方便追蹤排查問題

# 是否啟動LCN負載均衡策略(優化選項,開啟與否,功能不受影響)
tx-lcn.ribbon.loadbalancer.dtx.enabled=true
# 默認之配置為TM的本機默認端口
tx-lcn.client.manager-address=127.0.0.1:8070
# 開啟日志,默認為false
tx-lcn.logger.enabled=true
tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
tx-lcn.logger.jdbc-url=${spring.datasource.url}
tx-lcn.logger.username=${spring.datasource.username}
tx-lcn.logger.password=${spring.datasource.password}
logging.level.com.codingapi.txlcn=DEBUG

在啟動類上使用 @EnableDistributedTransaction

//省略其他代碼...
@EnableDistributedTransaction
public class MyspringbootApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyspringbootApplication.class, args);
    }
}

在提交本地事務的地方添加@LcnTransaction,分布式事務注解,PS:@LcnTransaction的target是在方法上的,@Target({ElementType.METHOD})

演示demo

我們挑選之前的兩個項目myspringboot、springdatejpa,按照步驟設置成TC,
並且在兩個TC添加測試接口

myspringboot-controller

/**
     * 測試分布式事務
     */
    @GetMapping("feign/save")
    Result<UserVo> save(UserVo userVo){
        //模擬數據
        Description description = new Description();
        description.setUserId("111");
        description.setDescription("測試用戶描述");

        Result<Description> save = descriptionService.save(description);
        System.out.println(save);
        return null;
    }

myspringboot-service

@Override
    @LcnTransaction//分布式事務
    @Transactional //本地事務
    public Result<Description> save(Description description) {
        UserVo userVo = new UserVo();
        userVo.setUsername("huanzi");
        userVo.setPassword("123");
        //調用springdatejpa服務保存userVo
        Result<UserVo>  result = myspringbootFeign.save(userVo);
        System.out.println(result);

        //myspringboot本地服務保存description
        Description save = descriptionRepository.save(description);
        System.out.println(save);
        
        //模擬發生異常
        throw new RuntimeException("business code error");
    }

myspringboot-feign

@FeignClient(name = "springdatejpa", path = "/user/",fallback = MyspringbootFeignFallback.class,fallbackFactory = MyspringbootFeignFallbackFactory.class)
public interface MyspringbootFeign {

    @RequestMapping(value = "save")
    Result<UserVo> save(@RequestBody UserVo userVo);
}

springdatejpa

這個原先就已經有對應的save接口,其他的代碼我們就不貼了,在UserServiceImpl類重寫save方法,在save方法上添加@LcnTransaction注解

@LcnTransaction//分布式事務
    @Transactional //本地事務
    @Override
    public Result<UserVo> save(UserVo entity) {
        User user = userRepository.save(FastCopy.copy(entity, User.class));
        return Result.of(FastCopy.copy(user, UserVo.class));
    }

演示效果

啟動所有項目,TM跟Redis服務也要記得啟動

查看TM后台,可以看到成功注冊了兩個TC

訪問http://localhost:10010/myspringboot/feign/save,被單點登錄攔截,登錄后跳轉正常跳轉該接口,這些就不再演示了,下面直接看后台debug日志
調用流程
myspringboot(A) ---> springdatejpa(B)

事務回滾
myspringboot(A)


springdatejpa(B)

到這里springdatejpa(B)已經響應了user數據給myspringboot(A),而后收到回滾通知

事務提交
我們看一下事務正常提交是怎么樣的,我們把模擬異常注釋起來,並返回保存后的數據

        //模擬發生異常
        //throw new RuntimeException("business code error");
        return Result.of(save);

我們直接從springdatejpa(B)響應數據之后看myspringboot(A)

springdatejpa(B)

后記

要注意我們的springboot版本跟txlcn的版本是不是兼容,按照官網的快速開始(https://www.txlcn.org/zh-cn/docs/start.html),以及參考官方例子(https://github.com/codingapi/txlcn-demo),一路下來碰到了一下小問題在這里總結一下:

1、A調B,A拋出異常,A事務回滾,B事務沒有回滾

原因:這個是因為剛開始我是在A的controller層調用B,相當於B是一個單獨是事務組,A又是一個單獨的事務組

解決:在A開啟事務后再調用B


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM