Spring Cloud Alibaba 使用Seata解決分布式事務(wù)
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)

-? ? ?為什么會(huì)產(chǎn)生分布式事務(wù)?? ? -
隨著業(yè)務(wù)的快速發(fā)展,網(wǎng)站系統(tǒng)往往由單體架構(gòu)逐漸演變?yōu)榉植际健⑽⒎?wù)架構(gòu),而對(duì)于數(shù)據(jù)庫(kù)則由單機(jī)數(shù)據(jù)庫(kù)架構(gòu)向分布式數(shù)據(jù)庫(kù)架構(gòu)轉(zhuǎn)變。此時(shí),我們會(huì)將一個(gè)大的應(yīng)用系統(tǒng)拆分為多個(gè)可以獨(dú)立部署的應(yīng)用服務(wù),需要各個(gè)服務(wù)之間進(jìn)行遠(yuǎn)程協(xié)作才能完成事務(wù)操作。在微服務(wù)項(xiàng)目中通常一個(gè)大項(xiàng)目會(huì)被拆分為N個(gè)子項(xiàng)目,例如用戶中心服務(wù),會(huì)員中心服務(wù),支付中心服務(wù)等一系列微服務(wù),在面臨各種業(yè)務(wù)需求時(shí)難免會(huì)產(chǎn)生用戶中心服務(wù)中需要調(diào)用會(huì)員中心服務(wù),支付中心服務(wù)而產(chǎn)生調(diào)用鏈路;服務(wù)與服務(wù)之間通訊采用RPC遠(yuǎn)程調(diào)用技術(shù),但是每個(gè)服務(wù)中都有自己獨(dú)立的數(shù)據(jù)源,即自己獨(dú)立的本地事務(wù);兩個(gè)服務(wù)相互進(jìn)行通訊的時(shí)候,兩個(gè)本地事務(wù)互不影響,從而出現(xiàn)分布式事務(wù)產(chǎn)生的原因。

-? ? ?Seata簡(jiǎn)介? ? -
Seata 是一款開源的分布式事務(wù)解決方案,致力于提供高性能和簡(jiǎn)單易用的分布式事務(wù)服務(wù)。Seata 將為用戶提供了 AT、TCC、SAGA 和 XA 事務(wù)模式,為用戶打造一站式的分布式解決方案。
Seata核心術(shù)語(yǔ)
TC (Transaction Coordinator) - 事務(wù)協(xié)調(diào)者:維護(hù)全局和分支事務(wù)的狀態(tài),驅(qū)動(dòng)全局事務(wù)提交或回滾。
TM (Transaction Manager) - 事務(wù)管理器:定義全局事務(wù)的范圍:開始全局事務(wù)、提交或回滾全局事務(wù)。
RM (Resource Manager) - 資源管理器:管理分支事務(wù)處理的資源,與TC交談以注冊(cè)分支事務(wù)和報(bào)告分支事務(wù)的狀態(tài),并驅(qū)動(dòng)分支事務(wù)提交或回滾。
AT模式工作機(jī)制
根據(jù)官方說(shuō)明當(dāng)前:通過JDBC訪問支持本地 ACID 事務(wù)的關(guān)系型數(shù)據(jù)庫(kù)的Java應(yīng)用程序才支持AT模式。
兩階段提交協(xié)議的演變:
一階段:業(yè)務(wù)數(shù)據(jù)和回滾日志記錄在同一個(gè)本地事務(wù)中提交,釋放本地鎖和連接資源。
二階段:
提交異步化,非常快速地完成。
回滾通過一階段的回滾日志進(jìn)行反向補(bǔ)償。
更詳細(xì)可參考官方文檔:?http://seata.io/zh-cn/docs/dev/mode/at-mode.html

-? ? ?Seata Server 部署? ? -
官方Seata配置中心信息:https://github.com/seata/seata/blob/develop/script/config-center/config.txt
官方Seata Nacos配置部署腳本:https://github.com/seata/seata/blob/develop/script/config-center/config.txt
版本信息與Seata Server下載地址可參考首頁(yè)介紹文檔:https://gitee.com/SimpleWu/spring-cloud-alibaba-example
Seata目錄結(jié)構(gòu)說(shuō)明:
bin:運(yùn)行腳本
conf:配置文件
lib:依賴包
當(dāng)前部署方式采用Nacos作為注冊(cè)中心與配置中心。
registry.conf
該配置位于conf目錄,按下以下注釋區(qū)域進(jìn)行修改
registry?{
??#?file?、nacos?、eureka、redis、zk、consul、etcd3、sofa
??#?使用nacos作為注冊(cè)中心
??type?=?"nacos"
??nacos?{
????#?注冊(cè)到nacos應(yīng)用名稱
????application?=?"seata-server"
????#?nacos?ip
????serverAddr?=?"127.0.0.1:8848"
????#?所在分組
????group?=?"EXAMPLE-GROUP"
????#?所在命名空間
????namespace?=?"7e3699fa-09eb-4d47-8967-60f6c98da94a"
????#?所在集群
????#cluster?=?"default"
????username?=?"nacos"
????password?=?"nacos"
??}
??eureka?{
????serviceUrl?=?"http://localhost:8761/eureka"
????application?=?"default"
????weight?=?"1"
??}
??redis?{
????serverAddr?=?"localhost:6379"
????db?=?0
????password?=?""
????cluster?=?"default"
????timeout?=?0
??}
??zk?{
????cluster?=?"default"
????serverAddr?=?"127.0.0.1:2181"
????sessionTimeout?=?6000
????connectTimeout?=?2000
????username?=?""
????password?=?""
??}
??consul?{
????cluster?=?"default"
????serverAddr?=?"127.0.0.1:8500"
??}
??etcd3?{
????cluster?=?"default"
????serverAddr?=?"http://localhost:2379"
??}
??sofa?{
????serverAddr?=?"127.0.0.1:9603"
????application?=?"default"
????region?=?"DEFAULT_ZONE"
????datacenter?=?"DefaultDataCenter"
????cluster?=?"default"
????group?=?"SEATA_GROUP"
????addressWaitTime?=?"3000"
??}
??file?{
????name?=?"file.conf"
??}
}
config?{
??#?file、nacos?、apollo、zk、consul、etcd3
??#?使用nacos管理配置
??type?=?"nacos"
??nacos?{
????#?nacos?ip
????serverAddr?=?"127.0.0.1:8848"
????#?所在命名空間
????namespace?=?"7e3699fa-09eb-4d47-8967-60f6c98da94a"
????#?所在分組
????group?=?"EXAMPLE-GROUP"
????username?=?"nacos"
????password?=?"nacos"
??}
??consul?{
????serverAddr?=?"127.0.0.1:8500"
??}
??apollo?{
????appId?=?"seata-server"
????apolloMeta?=?"http://192.168.1.204:8801"
????namespace?=?"application"
??}
??zk?{
????serverAddr?=?"127.0.0.1:2181"
????sessionTimeout?=?6000
????connectTimeout?=?2000
????username?=?""
????password?=?""
??}
??etcd3?{
????serverAddr?=?"http://localhost:2379"
??}
??file?{
????name?=?"file.conf"
??}
}
以上內(nèi)容主要修改了注冊(cè)中心與配置中心為Nacos并且修改了Nacos地址與登錄賬號(hào)/登錄密碼,命名空間,分組;
配置部署到Nacos
這里簡(jiǎn)化了下Nacos官網(wǎng)下載的config.txt內(nèi)容,從官網(wǎng)下載的配置文本以下內(nèi)容標(biāo)記需要修改的需要關(guān)注
#事務(wù)組?重點(diǎn)關(guān)注
service.vgroupMapping.my_test_tx_group=default
#服務(wù)段分組地址
service.default.grouplist=127.0.0.1:8091
#保持默認(rèn)
service.enableDegrade=false
#保持默認(rèn)
service.disableGlobalTransaction=false
#存儲(chǔ)方式選擇?db模式則數(shù)據(jù)庫(kù)
store.mode=db
#需修改
store.lock.mode=db
#需修改
store.session.mode=db
store.publicKey=
#需修改
store.db.datasource=druid
#需修改
store.db.dbType=mysql
#需修改
store.db.driverClassName=com.mysql.jdbc.Driver
#需修改
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
#需修改
store.db.user=root
#需修改
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
client.undo.dataValidation=true
#需修改
#jackson改為kryo?解決數(shù)據(jù)庫(kù)Datetime類型問題
client.undo.logSerialization=kryo
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
其中該配置需要重點(diǎn)關(guān)注service.vgroupMapping.my_test_tx_group=default這里的配置與微服務(wù)應(yīng)用中的配置必須要一致后面會(huì)描述到。
由于有時(shí)間類型是Seata回滾反序列化Date類型無(wú)法成功反序列化,需要修改序列化方式解決該問題:?client.undo.logSerialization=kryo
修改完所有配置運(yùn)行從官網(wǎng)下載的nacos-config.sh文件將文本內(nèi)容上次到nacos配置中心中:
#?-h?ip?-p?端口?-t?命名空間?-g?分組
sh?nacos-config.sh?-h?localhost?-p?8848?-t?7e3699fa-09eb-4d47-8967-60f6c98da94a?-g?EXAMPLE-GROUP
部署好配置文件之后在Nacos命名空間為7e3699fa-09eb-4d47-8967-60f6c98da94a(dev)的配置管理界面可以看到文本中的內(nèi)容。
Seata數(shù)據(jù)庫(kù)
按照config.txt中對(duì)應(yīng)的數(shù)據(jù)庫(kù)連接信息創(chuàng)建Seata數(shù)據(jù)庫(kù)并且創(chuàng)建以下幾張表
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;
部署Seata Server
以上工作準(zhǔn)備就緒,進(jìn)入bin目錄運(yùn)行seata-server.bat(windows用戶)/seata-server.sh(linux用戶)即可。

-? ? ?Seata應(yīng)用場(chǎng)景模擬? ? -
這里做一個(gè)用戶服務(wù)用戶登錄成功后調(diào)用會(huì)員服務(wù)增加會(huì)員積分場(chǎng)景案例。
父工程改造
工程名稱:spring-cloud-alibaba-version-parent,增加mybatis,seata序列化等依賴版本管理。
3.4.2
2.5.4
1.3.0
????com.baomidou
????mybatis-plus-boot-starter
????${mybatis.plus.version}
????io.seata
????seata-serializer-kryo
????${seata.serializer.kryo.version}
會(huì)員服務(wù)工程改造
工程名稱:spring-cloud-alibaba-service-member,增加數(shù)據(jù)庫(kù)與Seata依賴,增加用戶會(huì)員積分接口。
pom.xml
?
?com.alibaba.cloud
?spring-cloud-starter-alibaba-seata
?io.seata
?seata-serializer-kryo
?com.baomidou
?mybatis-plus-boot-starter
?mysql
?mysql-connector-java
bootstrap.yaml
#注意,此處省略之前配置的信息....
#注意,此處省略之前配置的信息....
#注意,此處省略之前配置的信息....
#注意,此處省略之前配置的信息....
#數(shù)據(jù)庫(kù)信息配置
spring:
??datasource:
????driver-class-name:?com.mysql.cj.jdbc.Driver
????url:?jdbc:mysql://localhost:3306/member_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
????username:?root
????password:?123456
#Seata配置
seata:
??enabled:?true
??application-id:?${spring.application.name}
??#對(duì)應(yīng)nacos配置?service.vgroupMapping.my_test_tx_group
??tx-service-group:?'my_test_tx_group'
??service:
????vgroup-mapping:
??????#對(duì)應(yīng)nacos配置?service.vgroupMapping.my_test_tx_group?的值?default
??????my_test_tx_group:?'default'
??registry:
????type:?nacos
????nacos:
??????server-addr:?${spring.cloud.nacos.discovery.server-addr}
??????namespace:?${spring.cloud.nacos.discovery.namespace}
??????group:?${spring.cloud.nacos.discovery.group}
??????#cluster:?${spring.cloud.nacos.discovery.cluster}
??config:
????type:?nacos
????nacos:
??????server-addr:?${spring.cloud.nacos.discovery.server-addr}
??????namespace:?${spring.cloud.nacos.discovery.namespace}
??????group:?${spring.cloud.nacos.discovery.group}
注意事項(xiàng):
bootstrap.yaml中seata.tx-service-group 配置項(xiàng)一定要配置nacos配置中心中service.vgroupMapping對(duì)應(yīng)的my_test_tx_group。也就是說(shuō)一定要保持一致。
bootstrap.yaml中seata.service.vgroup-mapping.my_test_tx_group配置項(xiàng)一定要配置nacos配置中心對(duì)應(yīng)service.vgroupMapping.my_test_tx_group配置祥的值。
如果沒有注意上方兩點(diǎn)將會(huì)導(dǎo)致啟動(dòng)時(shí)報(bào):no available service 'default' found, please make sure registry config correct。
創(chuàng)建member_db數(shù)據(jù)庫(kù)
其中undo_log表為Seata回滾日志表,需要在每個(gè)使用到Seata的業(yè)務(wù)服務(wù)數(shù)據(jù)庫(kù)中都需要?jiǎng)?chuàng)建。
SET?NAMES?utf8mb4;
SET?FOREIGN_KEY_CHECKS?=?0;
--?----------------------------
--?Table?structure?for?t_member_integral
--?----------------------------
DROP?TABLE?IF?EXISTS?`t_member_integral`;
CREATE?TABLE?`t_member_integral`??(
??`ID`?bigint(20)?NOT?NULL?COMMENT?'主鍵',
??`USERNAME`?varchar(55)?CHARACTER?SET?utf8mb4?COLLATE?utf8mb4_general_ci?DEFAULT?NULL?COMMENT?'用戶名稱',
??`INTEGRAL`?int(11)?DEFAULT?NULL?COMMENT?'積分',
??`CREDATE`?datetime(0)?DEFAULT?NULL?COMMENT?'時(shí)間',
??PRIMARY?KEY?(`ID`)?USING?BTREE
)?ENGINE?=?InnoDB?CHARACTER?SET?=?utf8mb4?COLLATE?=?utf8mb4_general_ci?ROW_FORMAT?=?Dynamic;
--?----------------------------
--?Table?structure?for?undo_log
--?----------------------------
DROP?TABLE?IF?EXISTS?`undo_log`;
CREATE?TABLE?`undo_log`??(
??`id`?bigint(20)?NOT?NULL?AUTO_INCREMENT,
??`branch_id`?bigint(20)?NOT?NULL,
??`xid`?varchar(100)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL,
??`context`?varchar(128)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL,
??`rollback_info`?longblob?NOT?NULL,
??`log_status`?int(11)?NOT?NULL,
??`log_created`?datetime(0)?NOT?NULL,
??`log_modified`?datetime(0)?NOT?NULL,
??`ext`?varchar(100)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?DEFAULT?NULL,
??PRIMARY?KEY?(`id`)?USING?BTREE,
??UNIQUE?INDEX?`ux_undo_log`(`xid`,?`branch_id`)?USING?BTREE
)?ENGINE?=?InnoDB?AUTO_INCREMENT?=?1?CHARACTER?SET?=?utf8?COLLATE?=?utf8_general_ci?ROW_FORMAT?=?Dynamic;
SET?FOREIGN_KEY_CHECKS?=?1;
新增會(huì)員積分CRUD
我這里新增以下類,具體內(nèi)容大家都比較熟悉。
MemberIntegralController.java
IMemberIntegralBiz.java
IMemberIntegralBizImpl.java
MemberIntegralMapper.java
MemberIntegral.xml
在這里所有增加會(huì)員積分的邏輯都寫在同一個(gè)類中 MemberIntegralController.java
import?com.baomidou.mybatisplus.core.toolkit.IdWorker;
import?com.gitee.eample.member.service.biz.IMemberIntegralBiz;
import?com.gitee.eample.member.service.domain.MemberIntegral;
import?com.gtiee.example.common.exception.Response;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.web.bind.annotation.PathVariable;
import?org.springframework.web.bind.annotation.PostMapping;
import?org.springframework.web.bind.annotation.RequestMapping;
import?org.springframework.web.bind.annotation.RestController;
import?java.util.Date;
/**
?*?用戶積分
?*
?*?@author?wentao.wu
?*/
@RestController
@RequestMapping("/member/integral")
public?class?MemberIntegralController?{
????@Autowired
????private?IMemberIntegralBiz?memberIntegralBiz;
????@PostMapping("/login/{username}")
????public?Response?login(@PathVariable("username")?String?username)?{
????????//?每天第一次登錄則增加積分,我這里就不判斷了,每次調(diào)用都新增一條積分記錄了
????????MemberIntegral?memberIntegral?=?new?MemberIntegral();
????????memberIntegral.setId(IdWorker.getId());
????????memberIntegral.setIntegral(10);//固定10積分
????????memberIntegral.setUsername(username);
????????memberIntegral.setCredate(new?Date());
????????memberIntegralBiz.save(memberIntegral);
????????return?Response.createOk("登錄新增會(huì)員積分成功!",?true);
????}
}
運(yùn)行MemberServiceApplication.java啟動(dòng)服務(wù),如果想知道有沒有注冊(cè)成功:
第一可以看Seata Server端有沒有日志輸出,該日志內(nèi)容主要為注冊(cè)的業(yè)務(wù)服務(wù)的數(shù)據(jù)庫(kù)信息。
第二可以看業(yè)務(wù)服務(wù)有沒有輸出以下日志,有輸出以下日志則Seata Server端注冊(cè)成功
2021-11-05?09:56:30.962??INFO?16420?---?[???????????main]?i.s.c.r.netty.NettyClientChannelManager??:?will?connect?to?2.0.4.58:8091
2021-11-05?09:56:30.962??INFO?16420?---?[???????????main]?i.s.c.rpc.netty.RmNettyRemotingClient????:?RM?will?register?:jdbc:mysql://localhost:3306/member_db
2021-11-05?09:56:30.967??INFO?16420?---?[???????????main]?i.s.core.rpc.netty.NettyPoolableFactory??:?NettyPool?create?channel?to?transactionRole:RMROLE,address:2.0.4.58:8091,msg:'jdbc:mysql://localhost:3306/member_db',?applicationId='service-member',?transactionServiceGroup='my_test_tx_group'}?>
用戶服務(wù)工程改造
工程名稱:spring-cloud-alibaba-service-member,增加數(shù)據(jù)庫(kù)與Seata依賴,增加用戶登錄接口,增加調(diào)用會(huì)員服務(wù)積分接口feign。
由于內(nèi)容一致此處省略pom.xml,bootstrap.xml(里面注意數(shù)據(jù)庫(kù)要修改為用戶服務(wù)的數(shù)據(jù)庫(kù))。
創(chuàng)建user_db數(shù)據(jù)庫(kù)
其中undo_log表為Seata回滾日志表,需要在每個(gè)使用到Seata的業(yè)務(wù)服務(wù)數(shù)據(jù)庫(kù)中都需要?jiǎng)?chuàng)建。
SET?NAMES?utf8mb4;
SET?FOREIGN_KEY_CHECKS?=?0;
--?----------------------------
--?Table?structure?for?t_user
--?----------------------------
DROP?TABLE?IF?EXISTS?`t_user`;
CREATE?TABLE?`t_user`??(
??`ID`?bigint(20)?NOT?NULL?COMMENT?'主鍵',
??`USERNAME`?varchar(55)?CHARACTER?SET?utf8mb4?COLLATE?utf8mb4_general_ci?DEFAULT?NULL?COMMENT?'用戶名',
??`PWD`?varchar(255)?CHARACTER?SET?utf8mb4?COLLATE?utf8mb4_general_ci?DEFAULT?NULL?COMMENT?'密碼',
??`ADDR`?varchar(255)?CHARACTER?SET?utf8mb4?COLLATE?utf8mb4_general_ci?DEFAULT?NULL?COMMENT?'地址',
??`LAST_LOGIN_DATE`?datetime(0)?DEFAULT?NULL?COMMENT?'最后登錄時(shí)間',
??PRIMARY?KEY?(`ID`)?USING?BTREE
)?ENGINE?=?InnoDB?CHARACTER?SET?=?utf8mb4?COLLATE?=?utf8mb4_general_ci?ROW_FORMAT?=?Dynamic;
--?----------------------------
--?Records?of?t_user
--?----------------------------
INSERT?INTO?`t_user`?VALUES?(1,?'test1',?'123456',?'123',?NULL);
--?----------------------------
--?Table?structure?for?undo_log
--?----------------------------
DROP?TABLE?IF?EXISTS?`undo_log`;
CREATE?TABLE?`undo_log`??(
??`id`?bigint(20)?NOT?NULL?AUTO_INCREMENT,
??`branch_id`?bigint(20)?NOT?NULL,
??`xid`?varchar(100)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL,
??`context`?varchar(128)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL,
??`rollback_info`?longblob?NOT?NULL,
??`log_status`?int(11)?NOT?NULL,
??`log_created`?datetime(0)?NOT?NULL,
??`log_modified`?datetime(0)?NOT?NULL,
??`ext`?varchar(100)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?DEFAULT?NULL,
??PRIMARY?KEY?(`id`)?USING?BTREE,
??UNIQUE?INDEX?`ux_undo_log`(`xid`,?`branch_id`)?USING?BTREE
)?ENGINE?=?InnoDB?AUTO_INCREMENT?=?1?CHARACTER?SET?=?utf8?COLLATE?=?utf8_general_ci?ROW_FORMAT?=?Dynamic;
SET?FOREIGN_KEY_CHECKS?=?1;
新增用戶登錄CRUD
我這里新增以下類,具體內(nèi)容大家都比較熟悉。
UserController.java
IUserBiz.java
IUserBizImpl.java
UserMapper.java
UserMapper.xml
MemberInfoControllerClient.java
MemberInfoControllerClient.java
/**
?*?service-member服務(wù)遠(yuǎn)程調(diào)用接口
?*
?*?@author?wentao.wu
?*/
@FeignClient(name?=?"service-member")
public?interface?MemberInfoControllerClient?{
????/**
?????*?登錄送積分
?????*
?????*?@param?username
?????*?@return
?????*/
????@PostMapping("/member/integral/login/{username}")
????Response?login(@PathVariable("username")String?username);
}
IUserBiz.java
public?interface?IUserBiz?extends?IService?{
????/**
?????*?用戶登錄并且贈(zèng)送第一次登錄積分
?????*
?????*?@param?command
?????*?@return
?????*/
????boolean?login(UserLoginCommand?command);
}
IUserBizImpl.java
package?com.gitee.eample.user.service.biz;
import?com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import?com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import?com.gitee.eample.user.service.controller.command.UserLoginCommand;
import?com.gitee.eample.user.service.dao.UserMapper;
import?com.gitee.eample.user.service.domain.User;
import?com.gitee.eample.user.service.feign.MemberInfoControllerClient;
import?com.gtiee.example.common.exception.Response;
import?io.seata.spring.annotation.GlobalTransactional;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.stereotype.Service;
import?org.springframework.util.ObjectUtils;
import?java.util.Date;
@Service
public?class?IUserBizImpl?extends?ServiceImpl?implements?IUserBiz?{
????@Autowired
????private?MemberInfoControllerClient?client;
????@GlobalTransactional(name?=?"login_add_member_intergral",rollbackFor?=?Exception.class)//開啟分布式事務(wù)
????@Override
????public?boolean?login(UserLoginCommand?command)?{
????????LambdaQueryWrapper?wrapper?=?new?LambdaQueryWrapper<>();
????????wrapper.eq(User::getUsername,?command.getUsername())
????????????????.eq(User::getPwd,?command.getPwd());
????????User?loginUser?=?getOne(wrapper);
????????if?(ObjectUtils.isEmpty(loginUser))?{
????????????return?false;
????????}
????????//調(diào)用會(huì)員登錄接口增加積分
????????Response?response?=?client.login(command.getUsername());
????????if?(response.isOk())?{//增加積分成功,或已增加積分
????????????//調(diào)用積分接口成功,修改當(dāng)前用戶登錄時(shí)間
????????????loginUser.setLastLoginDate(new?Date());
????????????updateById(loginUser);
????????????//假設(shè)此處發(fā)生異常,不但修改當(dāng)前用戶登錄時(shí)間需要回滾并且新增的會(huì)員積分信息也回滾才算正常
????????????int?i?=?0?/?0;
????????????return?true;
????????}?else?{
????????????//增加積分失敗
????????????return?false;
????????}
????}
}
UserController.java
import?com.gitee.eample.user.service.biz.IUserBiz;
import?com.gitee.eample.user.service.controller.command.UserLoginCommand;
import?com.gtiee.example.common.exception.Response;
import?org.slf4j.Logger;
import?org.slf4j.LoggerFactory;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.web.bind.annotation.PostMapping;
import?org.springframework.web.bind.annotation.RequestMapping;
import?org.springframework.web.bind.annotation.RestController;
/**
?*?User?Business?Controller
?*
?*?@author?wentao.wu
?*/
@RestController
@RequestMapping("/users/")
public?class?UserController?{
????private?Logger?logger?=?LoggerFactory.getLogger(UserController.class);
????@Autowired
????private?IUserBiz?userBiz;
????@PostMapping("/login")
????public?Response?login(UserLoginCommand?command)?{
????????try?{
????????????boolean?result?=?userBiz.login(command);
????????????if?(result)?{
????????????????return?Response.createOk("登錄并贈(zèng)送積分成功!",?result);
????????????}else{
????????????????return?Response.createError("賬號(hào)或密碼不存在!",?result);
????????????}
????????}?catch?(Exception?e)?{
????????????logger.error("登錄失敗!",?e);
????????????return?Response.createError("服務(wù)器繁忙請(qǐng)稍后再試!",?false);
????????}
????}
}
運(yùn)行啟動(dòng)類UserServiceApplication.java。
服務(wù)改造成功后主要模擬有三個(gè)場(chǎng)景:
有分布式事務(wù)處理發(fā)生異常場(chǎng)景:調(diào)用積分接口成功,修改當(dāng)前用戶登錄時(shí)間之后發(fā)生異常,用戶表的修改操作進(jìn)行回滾,同時(shí)會(huì)員服務(wù)新增的用戶對(duì)應(yīng)的積分?jǐn)?shù)據(jù)同樣發(fā)生回滾
無(wú)分布式事務(wù)處理發(fā)生異常場(chǎng)景: 調(diào)用積分接口成功,修改當(dāng)前用戶登錄時(shí)間之后發(fā)生異常,用戶表的修改操作進(jìn)行回滾,用戶會(huì)員新增的數(shù)據(jù)并沒有發(fā)生回滾,此處造成數(shù)據(jù)異常。
正常執(zhí)行場(chǎng)景: 調(diào)用積分接口成功,修改當(dāng)前用戶登錄時(shí)間之后未發(fā)生異常,所有操作生效。
有分布式事務(wù)處理發(fā)生異常場(chǎng)景
IUserBizImpl.java中l(wèi)ogin方法增加分布式事務(wù)注解 @GlobalTransactional(name = "login_add_member_intergral",rollbackFor = Exception.class)//開啟分布式事務(wù),name為屬性名稱,rollbackFor 為指定回滾異常。
首先在用戶服務(wù)表中插入一條用戶數(shù)據(jù),作為登錄用戶:
INSERT?INTO?`user_db`.`t_user`(`ID`,?`USERNAME`,?`PWD`,?`ADDR`,?`LAST_LOGIN_DATE`)?VALUES?(1,?'test1',?'123456',?'123',?NULL);

并且當(dāng)前會(huì)員服務(wù)t_member_integral表中還沒有數(shù)據(jù)還沒初始化過數(shù)據(jù),當(dāng)前場(chǎng)景操作會(huì)修改t_user.LAST_LOGIN_DATE,并且向t_member_integral表中插入數(shù)據(jù);但是最后發(fā)生異常導(dǎo)致操作失敗,并且存在分布式事務(wù)注解,此時(shí)會(huì)回滾所有服務(wù)DML操作。
請(qǐng)求用戶登錄接口:

請(qǐng)求成功后查看t_user與t_member_integral依舊沒有發(fā)生任何改變:

表示分布式事務(wù)處理成功無(wú)任何異常。
無(wú)分布式事務(wù)處理發(fā)生異常場(chǎng)景
IUserBizImpl.java中l(wèi)ogin方法注釋掉全局事務(wù)(分布式事務(wù)),并且修改為本地事務(wù):
//@GlobalTransactional(name?=?"login_add_member_intergral",rollbackFor?=?Exception.class)//開啟分布式事務(wù)
@Transactional
請(qǐng)求用戶登錄接口:

此時(shí)發(fā)生的異常導(dǎo)致了用戶服務(wù)中修改LAST_LOGIN_DATE操作被回滾成功,但是t_member_integral表中依然插入了積分?jǐn)?shù)據(jù)并未被回滾:


表示在跨服務(wù)調(diào)用下沒有分布式事務(wù)將會(huì)導(dǎo)致數(shù)據(jù)不一致,事務(wù)異常。
正常執(zhí)行場(chǎng)景
IUserBizImpl.java中l(wèi)ogin方法注釋掉本地事務(wù),并且修改為全局事務(wù)(分布式事務(wù)),這里改不改無(wú)所謂,事務(wù)都是成功的,無(wú)論使用本地事務(wù)與全局事務(wù)都不會(huì)有問題,此處改成全局事務(wù)主要是驗(yàn)證全局事務(wù)不會(huì)影響什么:
@GlobalTransactional(name?=?"login_add_member_intergral",rollbackFor?=?Exception.class)//開啟分布式事務(wù)
//@Transactional
同時(shí)將login方法中的異常處理去除掉:
//假設(shè)此處發(fā)生異常,不但修改當(dāng)前用戶登錄時(shí)間需要回滾并且新增的會(huì)員積分信息也回滾才算正常
int?i?=?0?/?0;
請(qǐng)求用戶登錄接口,此時(shí)所有操作全部成功,用戶服務(wù)修改LAST_LOGIN_DATE成功,并且t_member_integral表中數(shù)據(jù)新增成功;這里就不貼圖了,浪費(fèi)大家流量。

-? ? ?總結(jié)? ? -
每個(gè)業(yè)務(wù)服務(wù)對(duì)應(yīng)的數(shù)據(jù)庫(kù)中都需要包含undo_log表,這個(gè)表主要是記錄全局事務(wù)操作的日志,后續(xù)發(fā)生異常Seata會(huì)通過該日志進(jìn)行事務(wù)回滾補(bǔ)償;
Seata回滾反序列化時(shí)Date類型無(wú)法反序列化,所以要修改Seata的序列化為:kryo;(此問題將在1.5版本 Seata發(fā)布后徹底解決)

-? ? ?源碼代碼存放地址 ? -
gitee:?https://gitee.com/SimpleWu/spring-cloud-alibaba-example.git
? 作者?|??SimpleWu
來(lái)源 |??cnblogs.com/SimpleWu/p/15529920.html
