分布式事務存在的原因:數據分布在不同的數據庫實例。一個分布式系統下存在多個模塊協調來完成一次業務,每一個模塊對應一個數據源,同一個業務需要操作不同的模塊,改動不同的數據庫,
舉例:
去A庫中存儲老師實體類的數據
@Data public class Teacher{ @Id private Integer tid; @Field private String tname; }
去B庫中存儲學生實體類的數據
@Data @Document public class Teacher{ @Id private String id; @Field privtae String name; @Field private Integer tid; }
這兩個pojo類通過tid關聯,學生對老師是一對多的關系,兩個POJO的數據通過mysql存儲在兩個不同的類。
常見的分布式框架解決方案
-
全局事務2pc --關系型數據庫
缺點:效率低,算法執行過程中,所有的節點處於阻塞狀態,所有節點所持有的資源處於封鎖狀態。
-
3pc(三段提交協議)
-
消息中間件
-
提供回滾接口
為了方便我們了解分布式事務框架,我們需要先了解的相關說明。
Spring本地事務是在同一個系統里做的實務操作,需要符合數據庫事務ACID特性,為剛性事務;而分布式事務往往不能滿足ACID,不同的數據庫實例對同一個方法在一個時間的響應並不完全一樣,但可能最終一致,這種事務是柔性事務。
柔性事務定理了解
1. CAP定理
一個分布式系統於事務由三個特性:Consistency(一致性) 、Availability(可用性)、 Partition tolerance(分區容錯性)。現實情況下,分布式系統最多同時滿足其中兩個,不能同時滿足三個。
- 可用性和一致性是矛盾的,原因是在分布式事務中操作多個節點的時候,一個節點壞了還得有別的節點來承擔,這是為了滿足可用性,但不符合一致性。
- 分區容錯性意思是,系統不能在一定時限內達到數據的一致性,就意味着發生了分區的情況。假設在一定時間內,A節點和C節點沒能同步數據,那么框架需要決定到底是A版本還是C版本作為后續操作數據的基版。
2. BASE理論
CAP理論的優化。Basically Available(基本可用 )、soft state(軟狀態),Eventually consistent(最終一致性)三個短語的縮寫。
BASE理論要求,即使無法做到節點強一致,但每個分布式應用都可以根據自身業務特點,采用適當的方式達到最終一致性。
- BA基本可用:指分布式系統中出現不可知故障的時候,允許損失部分可用性,但不代表整個系統不可用。(比如秒殺活動的並發降級頁面)
- S軟狀態:系統中數據允許存在中間狀態(軟狀態),並認為這個狀態不影響可用性,允許分布式節點之間存在同步延遲。(如Eureka集群同步數據)
- 最終一致性:允許整個系統數據在經過一定時間后,最終能達到整個系統的一致性。為弱一致性,響應給用戶結果時整個系統沒有達到一致性,但最終一定會達到一致性的。
*強一致性:系統接受請求后,整個系統必須達到一致的結果才能響應。
根據不同的解決方案衍生出了不同的分布式事務技術,下面介紹不同的分布式技術在這個示例的表現。
介紹
XA兩階段提交協議
- 第一階段所有的參與者TC(Transaction Client)准備執行事務並鎖住需要的資源,並向事務管理器報備自己已經准備好。
- 第二階段事務管理器負責協調,向不同的RM(資源管理器)索要數據,盡可能晚提交事務(在提交之前完成所有的工作),但也要等待TM(Transaction Manager)響應容易阻塞。吞吐量優先,不適合互聯網。
三種模式
基於XA兩階段提交協議,現存LCN分布式事務框架有三種模式,分別是LCN模式,TCC模式,TXC模式。
1. LCN模式:基於jdbc關系型數據庫實現對本地事務的操作,然后在由TM(Transaction Manager)統一協調控制事務。當本地事務提交回滾或者關閉連接時將會執行假操作,該代理的連接將由LCN連接池管理。基於注解 @LcnTransaction。
2. TCC模式:基於非關系型數據庫(非關系型就是非事務性,除此之外也支持關系型數據庫),相對於傳統事務機制(X/Open XA Two-Phase-Commit),其特征在於它不依賴資源管理器(RM)對XA的支持,而是通過對(由業務系統提供的)業務邏輯的調度來實現分布式事務。主要由三階段完成,Try: 嘗試執行業務、 Confirm:確認執行業務、 Cancel: 取消執行業務。特點:完全依賴開發者,通過重寫三個方法來實現對事務的控制,基於注解@TccTransaction。
3. TXC模式:命名來源於淘寶,實現原理是在執行SQL之前,先查詢SQL的影響數據,然后保存執行的SQL快走信息和創建鎖。當需要回滾的時候就采用這些記錄數據回滾數據庫,目前鎖實現依賴redis分布式鎖控制。特點:嵌入低,不會占用數據庫的連接資源,資源消耗較多,基於注解@TxcTransaction。
LCN原理解析
事務發起方和參與方都屬於TxClient,TxManager負責控制整個事務。
123代表三個txClient將自己的事務發布到TxManager,4為發起方告訴txManager獲取數據成功或者失敗,如果成功(其中一個失敗也算失敗),TxManager返回通知AB讓他們各自提交自己的事務。5為txManager通知發起方提交自己的事務。
LCN事務核心步驟
1. 創建事務組:事務發起方在開始執行業務代碼之前先調用txManager創建事務組對象,拿到事務表示GroupId。
2. 加入事務組:2和3,參與方將自己的事務執行情況報備給txManager
3. 通知事務組:發起方執行完業務代碼之后,將執行結果狀態通知給txManager
4. 通知事務單元:txManager詢問參與方的事務執行結果
5. 響應通知事務組:txManager通知發起方事務的最終結果
配置Tx-LCN案例
一、新建
1. 執行依賴中帶有的tx-manager.sql文件到數據庫中
2. 配置application.properties
TC配置
# 是否啟動LCN負載均衡策略(優化選項,開啟與否,功能不受影響) tx-lcn.ribbon.loadbalancer.dtx.enabled=true # tx-manager 的配置地址,可以指定TM集群中的任何一個或多個地址 # tx-manager 下集群策略,每個TC都會從始至終<斷線重連>與TM集群保持集群大小個連接。 # TM方,每有TM進入集群,會找到所有TC並通知其與新TM建立連接。 # TC方,啟動時按配置與集群建立連接,成功后,會再與集群協商,查詢集群大小並保持與所有TM的連接 tx-lcn.client.manager-address=127.0.0.1:8070 # 該參數是分布式事務框架存儲的業務切面信息。采用的是h2數據庫。絕對路徑。該參數默認的值為{user.dir}/.txlcn/{application.name}-{application.port} tx-lcn.aspect.log.file-path=logs/.txlcn/demo-8080 # 調用鏈長度等級,默認值為3(優化選項。系統中每個請求大致調用鏈平均長度,估算值。) tx-lcn.client.chain-level=3 # 該參數為tc與tm通訊時的最大超時時間,單位ms。該參數不需要配置會在連接初始化時由tm返回。 tx-lcn.client.tm-rpc-timeout=2000 # 該參數為分布式事務的最大時間,單位ms。該參數不允許TC方配置,會在連接初始化時由tm返回。 tx-lcn.client.dtx-time=8000 # 該參數為雪花算法的機器編號,所有TC不能相同。該參數不允許配置,會在連接初始化時由tm返回。 tx-lcn.client.machine-id=1 # 該參數為事務方法注解切面的orderNumber,默認值為0. tx-lcn.client.dtx-aspect-order=0 # 該參數為事務連接資源方法切面的orderNumber,默認值為0. tx-lcn.client.resource-order=0 # 是否開啟日志記錄。當開啟以后需要配置對應logger的數據庫連接配置信息。 tx-lcn.logger.enabled=false tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name} tx-lcn.logger.jdbc-url=${spring.datasource.url} #jdbc:mysql://localhost:3306/tx-manager?characterEncoding=UTF-8 遇上servertimezone問題添加serverTimezone=GMT
tx-lcn.logger.username=${spring.datasource.username}
tx-lcn.logger.password=${spring.datasource.password}
tx-lcn.logger.enabled=false #如果打開事務:更改 tx-lcn.logger.enabled=true
TM配置
spring.application.name=TransactionManager server.port=7970 # JDBC 數據庫配置 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tx-manager?characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=123456 # 數據庫方言 spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect # 第一次運行可以設置為: create, 為TM創建持久化數據庫表 spring.jpa.hibernate.ddl-auto=validate # 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=codingapi # 分布式事務鎖超時時間 默認為-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/***@**.com # 開啟日志,默認為false tx-lcn.logger.enabled=true tx-lcn.logger.enabled=false 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} # redis 的設置信息. 線上請用Redis Cluster spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password=
配置本地事務和分布式事務
@Configuration @EnableTransactionManagement public class TransactionConfiguration { /** * 本地事務配置 * @param transactionManager * @return */ @Bean @ConditionalOnMissingBean public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) { Properties properties = new Properties(); properties.setProperty("*", "PROPAGATION_REQUIRED,-Throwable"); TransactionInterceptor transactionInterceptor = new TransactionInterceptor(); transactionInterceptor.setTransactionManager(transactionManager); transactionInterceptor.setTransactionAttributes(properties); return transactionInterceptor; } /** * 分布式事務配置 設置為LCN模式 * @param dtxLogicWeaver * @return */ @ConditionalOnBean(DTXLogicWeaver.class) @Bean public TxLcnInterceptor txLcnInterceptor(DTXLogicWeaver dtxLogicWeaver) { TxLcnInterceptor txLcnInterceptor = new TxLcnInterceptor(dtxLogicWeaver); Properties properties = new Properties(); properties.setProperty(Transactions.DTX_TYPE,Transactions.LCN); properties.setProperty(Transactions.DTX_PROPAGATION, "REQUIRED"); txLcnInterceptor.setTransactionAttributes(properties); return txLcnInterceptor; } @Bean public BeanNameAutoProxyCreator beanNameAutoProxyCreator() { BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator(); //需要調整優先級,分布式事務在前,本地事務在后。 beanNameAutoProxyCreator.setInterceptorNames("txLcnInterceptor","transactionInterceptor"); beanNameAutoProxyCreator.setBeanNames("*Impl"); return beanNameAutoProxyCreator; } }
二、依賴
TX-Manager配置,獨立於微服務,是獨立的依賴
<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.codingapi.txlcn</groupId> <artifactId>txlcn-tm</artifactId> <version>5.0.2.RELEASE</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Tx-Client依賴
<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>
三、操作案例
Tx-Student微服務有addStudent()方法,Tc-Teacher微服務有addTeacher()方法,因為student的tid依賴於teacher,所以只有teacher插入成功了s才能插入成功,先不要添加外鍵。
這時候只在addStudent()方法上@transactional是不夠的,@transactional只限於本地事務。而這是分布式事務。
兩個方法上都添加
@Transavtional @TxTransaction
兩個工程的啟動類上都添加
@EnableDistributedTransaction
在teacher和student的application.properties文件中設置地址
#配置事務管理器TxManager的事務端口
tx-lcn.client.manager-address: 127.0.0.1:8070
代碼
@Autowired private TeacherFeign teacherFeign; @Autowired private StudentMapper studentMapper; @Transactional @TcTransaction public String addStudent(){ Student s = new Student(); teacherFeign.addTeacher(1,"lc老師“); s.setId(1); s.setName("xx學生"); s.setTid(1); studentMapper.insert(s); }
訪問 http://localhost:7090
可以進入
TxManager系統后台,可以看到配置信息及系統日志
配置TCC(非事務型數據庫)案例
假設要將Teacher數據存儲到mangodb
添加以下依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <!-- 數據源--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--數據源--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.1</version> </dependency> <!--連接驅動 --> <dependency> <groupId>mysql</groupId></groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>
TCC模式需要開發者手動處理,原因是mongodb沒有事務,需要手動處理刪除,所以TCC也叫代碼補償業務,其實現需要三個方法基本方法:
-
xxx() 需要加上@TccTransaction注解
-
confirmXxx()
-
cancelXxx()
@Autowired
private MongoTemplate mongoTemplate;
@TccTransaction public int addTercher(Teacher teacher){ System.out.println("嘗試新增數據") Teacher result = mongoTemplate.insert(teacher); if(result!=null){return 1;} return 0; } public void confirmAddTeacher(Teacher teacher){ System.out.println("teacher="+teacher) //只是有確認的作用 } public void cancelAddTeacher(Teacher teacher){ Criteria criteria = Criteria.where("id").is(teacher.getId()); Query query=new Query(criteria); DeleteResult deleteResut=mongoTemplate.remove(query,Teacher.class); System.out.println("deleteResut.getDeletedCount="+deleteResut.getDeletedCount()) }
student方不需要加@TccTransaction,仍然還是lcn(沒有更改數據庫)
Seata
組成部分
- Transaction Coordinator(TC):事務協調器,維護全局事務的運行狀態,相當於Seata的server
- Transaction Manager(TM):事務管理器。負責開啟一個全局事務,並最終發起全局提交或全局回滾
- Resource Manger(RM) 資源管理器,向事務管理器注冊分支事務和並報告分支事務的狀態,負責分支事務提交或回滾
事務控制流程
-
TM向TC申請開啟全局事務(@GlobalTransaction)
-
TC向TM返回全局事務的唯一XID
-
分支服務①向TC申請注冊分支服務,TC向分支服務①返回BranchID
-
分支服務①進行操作(RM),記錄操作日志undo_log,提交本地分支事務,向TC上報事務處理結果
-
分支服務①遠程調用分支服務②
-
分支服務②進行與①一樣的操作(RM),得到唯一的BranchID,undo_log以及上報
-
分支服務處理完畢后,TM向TC提交全局事務決議,詢問是否全部執行成功。
-
如果事務出現問題,反向操作undo_log回滾本地事務。
配置案例
一、依賴
<seata.version>1.4.2</seata.version> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>${seata.version}</version> </dependency>
https://seata.io/en-us/blog/download.html 下載好對應版本的seata server
1. 修改\seata\conf目錄下的file.conf的mode以及db的配置方式,目前mode有三個選項 file、db、redis
2. 然后在對應數據庫創建 globalTable
(持久化全局事務)、branchTable
(持久化各提交分支的事務)、 lockTable
(持久化各分支鎖定資源事務)三張表
-- the table to store GlobalSession data
-- 持久化全局事務
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
-- 持久化各提交分支的事務
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
-- 持久化每個分支鎖表事務
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;。
3. \seata\conf目錄下的registry.conf
文件,設置 注冊中心 和 配置中心,我選擇了都放在nacos(參見《Seata 1.4.0 + nacos配置和使用,超詳細》 https://blog.csdn.net/qq853632587/article/details/111644295)
4. 第三步下載好的nacos-config.sh放在seata的conf目錄下
下載config.txt(https://link.csdn.net/?target=https%3A%2F%2Fgithub.com%2Fseata%2Fseata%2Fblob%2Fdevelop%2Fscript%2Fconfig-center%2Fnacos%2Fnacos-config.sh)seata各種詳細的配置,執行 nacos-config.sh 即可將這些配置導入到nacos,這樣就不需要將file.conf和registry.conf放到我們的項目中了,需要什么配置就直接從nacos中讀取。
在conf文件git bash
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 0af6e97b-a684-4647-b696-7c6d42aecce7 -u nacos -w nacos
5. 每個服務都搭建配置bootstrap.yml
spring:
application:
name: application_name
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata
username: root
password: 123456
seata:
enabled: true
enable-auto-data-source-proxy: true
tx-service-group: my_test_tx_group
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
username: nacos
password: nacos
namespace: 自己的namespace
service:
vgroup-mapping:
my_test_tx_group: default
disable-global-transaction: false
client:
rm:
report-success-enable: false
在業務邏輯層的方法(不同方法)上方增加@ GlobalTransactional
MQ事務消息
有一些第三方的MQ是支持事務消息的,如RocketMQ。但RabbitMQ和Kafka都不支持。支持事務消息的方式也是類似於二階段提交。
舉例:讓支付寶賬戶扣除一萬,我們生成一個憑證(消息),這個消息寫着讓余額寶賬戶增加一萬。我們可以拿這個消息完成最終一致性。
消息解耦
-
支付寶在扣款事務提交之前,向實時消息服務請求發送消息,實時消息服務只記錄消息數據,而不真正發送(只是知道有這一條消息),只有消息發送成功后才會提交事務
-
先把(支付寶-10000)封裝成一個消息(new Message())),后把這個消息提交到MQ服務器上,當本地事務操作完成了以后,要么成功:(給MQ一個標識:COMMIT),要么失敗:(給MQ一個標識:ROLLBACK)
send(producer.send(new Message(),callback(里面處理本地事務))) //在callback處理本地事務:在callback方法里: update A set amount = amount - 10000 where userId = 1;
-
當支付寶扣款事務被成功提交后,向實時消息服務確認發送,只有在得到確認發送指令后,實時消息服務才真正發送該消息。
-
當支付寶扣款事務提交失敗回滾后,向實時消息服務取消發送。消息就不會被發送。
-
對於那些未確認的消息或者取消的消息,需要有一個消息狀態確認系統定時去支付寶系統查詢這個消息的狀態進行更新。
優點:消息數據獨立存儲,降低業務系統與消息系統之間的耦合
缺點:一次消息發送需要兩次請求;業務處理服務需要實現消息狀態回查接口。
參考:
【分布式事務----LCN】LCN原理及使用方式 https://blog.csdn.net/lgxzzz/article/details/121258819
微服務分布式事務之LCN、TCC特點、事務補償機制緣由以及設計重點 https://www.cnblogs.com/Courage129/p/14528981.html
分布式事務之TX-LCN https://cloud.tencent.com/developer/article/1752752