SpringBoot+Dubbo+Seata分布式事務(wù)實(shí)戰(zhàn)詳解
前言
Seata 是 阿里巴巴開(kāi)源的分布式事務(wù)中間件,以高效并且對(duì)業(yè)務(wù)0侵入的方式,解決微服務(wù)場(chǎng)景下面臨的分布式事務(wù)問(wèn)題。
事實(shí)上,官方在GitHub已經(jīng)給出了多種環(huán)境下的Seata應(yīng)用示例項(xiàng)目,地址:https://github.com/seata/seata-samples。
為什么筆者要重新寫一遍呢,主要原因有兩點(diǎn):
- 官網(wǎng)代碼示例中,依賴太多,分不清哪些有什么作用
- Seata相關(guān)資料較少,筆者在搭建的過(guò)程中,遇到了一些坑,記錄一下
一、環(huán)境準(zhǔn)備
本文涉及軟件環(huán)境如下:
- SpringBoot 2.1.6.RELEASE
- Dubbo 2.7.1
- Mybatis 3.5.1
- Seata 0.6.1
- Zookeeper 3.4.10
1、業(yè)務(wù)場(chǎng)景
為了簡(jiǎn)化流程,我們只需要訂單和庫(kù)存兩個(gè)服務(wù)。創(chuàng)建訂單的時(shí)候,調(diào)用庫(kù)存服務(wù),扣減庫(kù)存。
涉及的表設(shè)計(jì)如下:
CREATE TABLE `t_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(255) DEFAULT NULL, `user_id` varchar(255) DEFAULT NULL, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT '0', `amount` double(14,2) DEFAULT '0.00', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8; CREATE TABLE `t_storage` ( `id` int(11) NOT NULL AUTO_INCREMENT, `commodity_code` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `commodity_code` (`commodity_code`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 另外還需要一個(gè)回滾日志表: CREATE TABLE `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, `context` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=67 DEFAULT CHARSET=utf8;
2、Seata下載安裝
打開(kāi)https://github.com/seata/seata/releases,目前最新版本是v0.6.1。
下載解壓后,到seata-server-0.6.1\distribution\bin目錄下可以看到seata-server.bat和seata-server.sh,選擇一個(gè)雙擊執(zhí)行。
不出意外的話,當(dāng)你看到-Server started ...等字樣,就正常啟動(dòng)了。
3、Maven依賴
由于是Dubbo項(xiàng)目,我們先引入Dubbo相關(guān)依賴。
<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>2.7.1</version> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.1</version> </dependency> Dubbo的服務(wù)要注冊(cè)到Zookeeper,引入curator客戶端。 <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.13.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.13.0</version> </dependency>
最后,引入Seata。
<dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.6.1</version> </dependency>
當(dāng)然了,還有其他的如Mybatis、mysql-connector等就不粘了,自行引入即可。
二、項(xiàng)目配置
1、application.properties
這里只需要配置數(shù)據(jù)庫(kù)連接信息和Dubbo相關(guān)信息即可。
server.port=8011 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata spring.datasource.username=root spring.datasource.password=root dubbo.application.name=order-service dubbo.registry.address=zookeeper://127.0.0.1:2181 dubbo.protocol.name=dubbo dubbo.protocol.port=20881 dubbo.consumer.timeout=9999999 dubbo.consumer.check=false
2、數(shù)據(jù)源
Seata 是通過(guò)代理數(shù)據(jù)源實(shí)現(xiàn)事務(wù)分支,所以需要先配置一個(gè)數(shù)據(jù)源的代理,否則事務(wù)不會(huì)回滾。
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
注意,這里的DataSourceProxy類位于io.seata.rm.datasource包內(nèi)。
3、Seata配置
還需要配置全局事務(wù)掃描器。有兩個(gè)參數(shù),一個(gè)是應(yīng)用名稱,一個(gè)是事務(wù)分組。
@Bean
public GlobalTransactionScanner globalTransactionScanner() {
return new GlobalTransactionScanner("springboot-order", "my_test_tx_group");
}
事實(shí)上,關(guān)于Seata事務(wù)的一系列初始化工作都在這里完成。
4、配置注冊(cè)中心
Seata連接到服務(wù)器的時(shí)候需要一些配置項(xiàng),這時(shí)候有一個(gè)registry.conf文件可以指定注冊(cè)中心和配置文件是什么。
這里有很多可選性,比如file、nacos 、apollo、zk、consul。
后面4個(gè)都是業(yè)界成熟的配置注冊(cè)中心產(chǎn)品,為啥還有個(gè)file呢?
官方的初衷是在不依賴第三方配置注冊(cè)中心的基礎(chǔ)上快速集成測(cè)試seata功能,但是file類型本身不具備注冊(cè)中心的動(dòng)態(tài)發(fā)現(xiàn)和動(dòng)態(tài)配置功能。
registry.conf文件內(nèi)容如下:
registry {
type = "file"
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul
type = "file"
file {
name = "file.conf"
}
}
如果你選擇了file類型,通過(guò)name屬性指定了file.conf,這個(gè)文件中指定了客戶端或服務(wù)器的配置信息。比如傳輸協(xié)議、服務(wù)器地址等。
service {
#vgroup->rgroup
vgroup_mapping.my_test_tx_group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
}
三、業(yè)務(wù)代碼
1、庫(kù)存服務(wù)
在庫(kù)存服務(wù)中,拿到商品編碼和購(gòu)買總個(gè)數(shù),扣減即可。
<update id="decreaseStorage">
update t_storage set count = count-${count} where commodity_code = #{commodityCode}
</update>
然后用Dubbo將庫(kù)存服務(wù)扣減接口暴露出去。
2、訂單服務(wù)
在訂單服務(wù)中,先扣減庫(kù)存,再創(chuàng)建訂單。最后拋出異常,然后去數(shù)據(jù)庫(kù)檢查事務(wù)是否回滾。
@GlobalTransactional
public void createOrder(OrderDTO orderDTO) {
System.out.println("開(kāi)始全局事務(wù)。XID="+RootContext.getXID());
StorageDTO storageDTO = new StorageDTO();
storageDTO.setCount(orderDTO.getCount());
storageDTO.setCommodityCode(orderDTO.getCommodityCode());
//1、扣減庫(kù)存
storageDubboService.decreaseStorage(storageDTO);
//2、創(chuàng)建訂單
orderDTO.setId(order_id.incrementAndGet());
orderDTO.setOrderNo(UUID.randomUUID().toString());
Order order = new Order();
BeanUtils.copyProperties(orderDTO,order);
orderMapper.createOrder(order);
throw new RuntimeException("分布式事務(wù)異常..."+orderDTO.getOrderNo());
}
值得注意的是,在訂單服務(wù)事務(wù)開(kāi)始的方法上,需要標(biāo)注@GlobalTransactional。另外,在庫(kù)存服務(wù)的方法里,不需要此注解,事務(wù)會(huì)通過(guò)Dubbo進(jìn)行傳播。
四、注意事項(xiàng)
1、數(shù)據(jù)源
請(qǐng)切記,Seata 是通過(guò)代理數(shù)據(jù)源實(shí)現(xiàn)事務(wù)分支,一定不要忘記配置數(shù)據(jù)源代理。
2、主鍵自增
在數(shù)據(jù)庫(kù)中,表里的主鍵ID字段都是自增的。如果你的字段不是自增的,那么在Mybatis的insert SQL中,要將列名寫完整。
比如我們可以這樣寫SQL:
INSERT INTO table_name VALUES (值1, 值2,....)
那么這時(shí)候就要寫成:
INSERT INTO table_name (列1, 列2,...) VALUES (值1, 值2,....)
3、序列化問(wèn)題
在訂單表中,amount字段類型為double。在seata0.6.1版本中,默認(rèn)的序列化方式為fastjson,但它會(huì)將這個(gè)字段序列化成bigdecimal類型,會(huì)導(dǎo)致后面類型不匹配。
但是在后續(xù)的seata0.7.0版本中(還未發(fā)布),已經(jīng)將默認(rèn)的序列化方式改為了jackson。
不過(guò)無(wú)需擔(dān)心,這個(gè)問(wèn)題一般不會(huì)出現(xiàn)。筆者是因?yàn)橐e(cuò)了一個(gè)包,才導(dǎo)致發(fā)現(xiàn)這問(wèn)題。
4、本文代碼
本文示例代碼在:https://github.com/taoxun/springboot-dubbo-zookeeper-seata。
5、其他
歡迎有問(wèn)題及時(shí)交流~
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JavaWeb項(xiàng)目音頻資源播放實(shí)現(xiàn)方法詳解
這篇文章主要介紹了JavaWeb項(xiàng)目音頻資源播放實(shí)現(xiàn)方法詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10
詳解Java?redis中緩存穿透?緩存擊穿?雪崩三種現(xiàn)象以及解決方法
緩存穿透是指緩存和數(shù)據(jù)庫(kù)中都沒(méi)有的數(shù)據(jù),而用戶不斷發(fā)起請(qǐng)求,如發(fā)起為id為“-1”的數(shù)據(jù)或id為特別大不存在的數(shù)據(jù)。這時(shí)的用戶很可能是攻擊者,攻擊會(huì)導(dǎo)致數(shù)據(jù)庫(kù)壓力過(guò)大2022-01-01
Redis緩存及熱點(diǎn)key問(wèn)題解決方案
這篇文章主要介紹了Redis緩存及熱點(diǎn)key問(wèn)題解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
關(guān)于Spring?Ioc和DI注解的問(wèn)題
這篇文章主要介紹了Spring?Ioc和DI注解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
SpringBoot jackson 精度處理問(wèn)題解決
由于JavaScript處理的最大數(shù)值限制,較大的雪花ID在JS中容易溢出,為解決此問(wèn)題,可在SpringMVC或SpringBoot中使用@RequestBody注解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-10-10

