SpringCloud 整合Seata 解決分布式事務(wù)(搭建+源碼)
seata官網(wǎng):http://seata.io/zh-cn/
前言
在當(dāng)下微服務(wù)架構(gòu)比較火熱時,新一代微服務(wù)解決方案Spring Cloud Alibaba提供的開源分布式事務(wù)解決框架Seata無疑成為了我們在解決分布式事務(wù)時的首要之選,前面兩篇文章分別介紹了常見的分布式解決方案和成熟的框架以及關(guān)于Seata概念的入門介紹,沒有過分布式事務(wù)處理的小伙伴可以先有個大致的入門了解:
- SpringCloud Alibaba微服務(wù)架構(gòu)(十一)- 常見分布式事務(wù)解決方案及理論基礎(chǔ)篇
- SpringCloud Alibaba微服務(wù)架構(gòu)(十二)- 分布式事務(wù)解決框架之Seata概念入門篇
那么在本篇Spring Cloud整合Seata之前,你必須要了解一下Spring Cloud Alibaba與Spring Boot、Spring Cloud之間的版本對應(yīng)關(guān)系。
版本選擇: Spring Cloud Alibaba與Spring Boot、Spring Cloud版本對應(yīng)關(guān)系
一、版本要求
坑點1: 如果項目中使用了druid數(shù)據(jù)庫連接池,引入的是SpringBoot的Starter依賴druid-spring-boot-starter,那么需要把druid-spring-boot-starter依賴換成druid1.1.23,因為seata源碼中引入的druid依賴跟druid-spring-boot-starter的自動裝配類沖突了,沖突的情況下項目啟動出現(xiàn)異常,異常如下:

二、整合Seata環(huán)境配置
1. 下載seata-server-1.2.0和seata-1.2.0源碼
seate-server下載: https://seata.io/zh-cn/blog/download.html,下載我們需要使用的seata1.2壓縮包。
seata-1.2.0源碼下載: https://github.com/seata/seata/releases
在這里插入圖片描述
2. 創(chuàng)建undo_log日志表
在seata1.2源碼seata-1.2.0\script\client\at\db目錄下有提供針對mysql、oracle、postgresql這三種數(shù)據(jù)庫生成undo-log逆向日志回滾表的表創(chuàng)建腳本。
- 在你項目的參與全局事務(wù)的數(shù)據(jù)庫中加入undo_log這張表。undo_log表腳本根據(jù)自身數(shù)據(jù)庫類型來選擇。
- -- for AT mode you must to init this sql for you business database. the seata server not need it.
- CREATE TABLE IF NOT EXISTS `undo_log`
- (
- `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
- `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
- `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
- `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
- `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
- `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
- `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
- UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
- ) ENGINE = InnoDB
- AUTO_INCREMENT = 1
- DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
3.創(chuàng)建seata事務(wù)相關(guān)表
下載Seata1.2的源碼后解壓如上圖,目前支持mysql、oracle、postgresql這三種數(shù)據(jù)庫,上述三種腳本是針對Seata的Sever端在協(xié)調(diào)處理分布式事務(wù)時所需要的3張表,提供了不同數(shù)據(jù)庫的global_table表、branch_table表、lock_table表創(chuàng)建腳本,根據(jù)自身數(shù)據(jù)庫執(zhí)行對應(yīng)的sql腳本執(zhí)行即可。
這里以mysql為例,在你的mysql數(shù)據(jù)庫中創(chuàng)建名為seata的庫,并執(zhí)行以下sql,將會生成三張表:
- -- -------------------------------- The script used when storeMode is 'db' --------------------------------
- -- 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;
4. 項目中引入seata依賴
4.1 如果微服務(wù)是SpringCloud
- <!-- 分布式事務(wù)seata包 -->
- <!--seata begin-->
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
- <version>2.1.3.RELEASE</version>
- <exclusions>
- <exclusion>
- <groupId>io.seata</groupId>
- <artifactId>seata-spring-boot-starter</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>io.seata</groupId>
- <artifactId>seata-spring-boot-starter</artifactId>
- <version>1.2.0</version>
- </dependency>
- <!--seata end-->
4.2 如果微服務(wù)是Dubbo
- <dependency>
- <groupId>io.seata</groupId>
- <artifactId>seata-spring-boot-starter</artifactId>
- <version>1.2.0</version>
- </dependency>
5. 更改seata-server中的registry.conf
配置registry.conf注冊中心為nacos,配置nacos相關(guān)屬性參數(shù)。
- ##配置seata-server的注冊中心,支持file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
- registry {
- # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
- type = "nacos"
- nacos {
- application = "seata-server"
- serverAddr = "127.0.0.1:8848"
- group = "SEATA_GROUP"
- namespace = "public"
- username = "nacos"
- cluster = "default"
- password = "nacos"
- }
- file {
- name = "file.conf"
- }
- }
- ##配置seata-server的配置中心,支持file、nacos 、apollo、zk、consul、etcd3
- config {
- # file、nacos 、apollo、zk、consul、etcd3
- type = "nacos"
- nacos {
- serverAddr = "127.0.0.1:8848"
- namespace = "public"
- group = "SEATA_GROUP"
- username = "nacos"
- password = "nacos"
- }
- file {
- name = "file.conf"
- }
- }
6. 修改seata-server中的file.config
配置file.config的DB模式相關(guān)參數(shù)配置。
- ##配置seata-server的數(shù)據(jù)存儲方式,支持本地文檔和數(shù)據(jù)庫。
- ## transaction log store, only used in seata-server
- store {
- ## store mode: file、db、redis
- mode = "db"
- ## file store property
- file {
- ## store location dir
- dir = "sessionStore"
- # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
- maxBranchSessionSize = 16384
- # globe session size , if exceeded throws exceptions
- maxGlobalSessionSize = 512
- # file buffer size , if exceeded allocate new buffer
- fileWriteBufferCacheSize = 16384
- # when recover batch read size
- sessionReloadReadSize = 100
- # async, sync
- flushDiskMode = async
- }
- ## database store property
- db {
- ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
- datasource = "druid"
- ## mysql/oracle/postgresql/h2/oceanbase etc.
- dbType = "mysql"
- driverClassName = "com.mysql.jdbc.Driver"
- url = "jdbc:mysql://127.0.0.1:3306/seata"
- user = "root"
- password = "root"
- minConn = 5
- maxConn = 30
- globalTable = "global_table"
- branchTable = "branch_table"
- lockTable = "lock_table"
- queryLimit = 100
- maxWait = 5000
- }
- ## redis store property
- redis {
- host = "127.0.0.1"
- port = "6379"
- password = ""
- database = "0"
- minConn = 1
- maxConn = 10
- queryLimit = 100
- }
- }
7. 修改提交nacos腳本到nacos控制臺
運行你下載的nacos,并參考:https://github.com/seata/seata/tree/develop/script/config-center 下的config.txt文件并修改:
- service.vgroupMapping.my_test_tx_group=default
- store.mode=db
- 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
- store.db.user=username
- store.db.password=password
- store.db.minConn=5
- store.db.maxConn=30
- store.db.globalTable=global_table
- store.db.branchTable=branch_table
- store.db.queryLimit=100
- store.db.lockTable=lock_table
- store.db.maxWait=5000
運行倉庫:https://github.com/seata/seata/tree/develop/script/config-center/nacos 中提供的nacos腳本nacos-config.sh,將以上信息提交到nacos控制臺,如果有需要修改參數(shù),可直接通過登錄nacos控制臺修改。
操作如下圖:

8. application.yml配置
從官方github倉庫:https://github.com/seata/seata/tree/develop/script/client 拿到參考配置做修改,加到你項目的application.yml文件中。
- #Seata分布式事務(wù)配置(AT模式)
- seata:
- enabled: true
- application-id: ${spring.application.name}
- #客戶端和服務(wù)端在同一個事務(wù)組
- tx-service-group: my_test_tx_group
- enable-auto-data-source-proxy: true
- service:
- vgroup-mapping:
- my_test_tx_group: default
- config:
- type: nacos
- nacos:
- namespace: "public"
- serverAddr: 127.0.0.1:8848
- group: SEATA_GROUP
- username: "nacos"
- password: "nacos"
- #服務(wù)注冊到nacos
- registry:
- type: nacos
- nacos:
- application: seata-server
- server-addr: 127.0.0.1:8848
- group: SEATA_GROUP
- namespace: "public"
- username: "nacos"
- password: "nacos"
- cluster: default
9. 運行seata-server
啟動運行seata-server,成功后,運行自己的服務(wù)提供者,服務(wù)參與者。在全局事務(wù)調(diào)用者(發(fā)起全局事務(wù)的服務(wù))的接口上加入@GlobalTransactional注解
到此為止,整合SpringCloud整合seata1.2及seata1.2整合nacos的配置與注冊中心全部整合完成了。
三、項目準備
如果你經(jīng)過前面的步驟搭建Seata環(huán)境完成了,那么你可以嘗試一下啟動項目,控制臺無異常則搭建成功。
那么下面準備以Seata官方文檔上的一個經(jīng)典例子為題,模擬用戶下單,創(chuàng)建訂單同時扣減庫存數(shù)量這一過程中產(chǎn)生的分布式事務(wù)問題,然后使用Seata解決,正好使用以下Seata的特性。
1. 訂單服務(wù)
- OrderController
- /**
- * @desc: 訂單服務(wù)
- * @author: cao_wencao
- * @date: 2020-09-22 23:27
- */
- @RestController
- @Slf4j
- @RequestMapping("/order")
- public class OrderController {
- @Autowired
- private OrderServiceImpl orderService;
- /**
- * 用戶購買下單,模擬全局事務(wù)提交
- * @param pid
- * @return
- */
- @RequestMapping("/purchase/commit/{pid}")
- public Order orderCommit(@PathVariable("pid") Integer pid) {
- return orderService.createOrderCommit(pid);
- }
- /**
- * 用戶購買下單,模擬全局事務(wù)回滾
- * @param pid
- * @return
- */
- @RequestMapping("/purchase/rollback/{pid}")
- public Order orderRollback(@PathVariable("pid") Integer pid) {
- return orderService.createOrderRollback(pid);
- }
- }
- OrderServiceImpl
- /**
- * @desc:
- * @author: cao_wencao
- * @date: 2020-09-22 23:30
- */
- @Service
- @Slf4j
- public class OrderServiceImpl {
- @Autowired
- private OrderDao orderDao;
- @Autowired
- private ProductService productService;
- //用戶下單,模擬全局事務(wù)提交
- public Order createOrderCommit(Integer pid) {
- log.info("接收到{}號商品的下單請求,接下來調(diào)用商品微服務(wù)查詢此商品信息", pid);
- //1 調(diào)用商品微服務(wù),查詢商品信息
- Product product = productService.findByPid(pid);
- log.info("查詢到{}號商品的信息,內(nèi)容是:{}", pid, JSON.toJSONString(product));
- //2 下單(創(chuàng)建訂單)
- Order order = new Order();
- order.setUid(1);
- order.setUsername("測試用戶");
- order.setPid(pid);
- order.setPname(product.getPname());
- order.setPprice(product.getPprice());
- order.setNumber(1);
- orderDao.save(order);
- log.info("創(chuàng)建訂單成功,訂單信息為{}", JSON.toJSONString(order));
- //3 扣庫存m
- productService.reduceInventoryCommit(pid, order.getNumber());
- return order;
- }
- //用戶下單,模擬全局事務(wù)回滾
- @GlobalTransactional//全局事務(wù)控制
- public Order createOrderRollback(Integer pid) {
- log.info("接收到{}號商品的下單請求,接下來調(diào)用商品微服務(wù)查詢此商品信息", pid);
- //1 調(diào)用商品微服務(wù),查詢商品信息
- Product product = productService.findByPid(pid);
- log.info("查詢到{}號商品的信息,內(nèi)容是:{}", pid, JSON.toJSONString(product));
- //2 下單(創(chuàng)建訂單)
- Order order = new Order();
- order.setUid(1);
- order.setUsername("測試用戶");
- order.setPid(pid);
- order.setPname(product.getPname());
- order.setPprice(product.getPprice());
- order.setNumber(1);
- orderDao.save(order);
- log.info("創(chuàng)建訂單成功,訂單信息為{}", JSON.toJSONString(order));
- //3 扣庫存m
- productService.reduceInventoryRollback(pid, order.getNumber());
- return order;
- }
- }
- 商品服務(wù)的Feign類ProductService
- /**
- * @desc:
- * @author: cao_wencao
- * @date: 2020-09-22 23:43
- */
- @FeignClient(value = "product-service",configuration = FeignRequestInterceptor.class)
- public interface ProductService {
- //@FeignClient的value + @RequestMapping的value值 其實就是完成的請求地址 "http://product-service/product/" + pid
- //指定請求的URI部分
- @RequestMapping("/product/product/{pid}")
- Product findByPid(@PathVariable Integer pid);
- //扣減庫存,模擬全局事務(wù)提交
- //參數(shù)一: 商品標識
- //參數(shù)二:扣減數(shù)量
- @RequestMapping("/product/reduceInventory/commit")
- void reduceInventoryCommit(@RequestParam("pid") Integer pid,
- @RequestParam("number") Integer number);
- //扣減庫存,模擬全局事務(wù)回滾
- //參數(shù)一: 商品標識
- //參數(shù)二:扣減數(shù)量
- @RequestMapping("/product/reduceInventory/rollback")
- void reduceInventoryRollback(@RequestParam("pid") Integer pid,
- @RequestParam("number") Integer number);
- }
2. 商品服務(wù)
- ProductController
- /**
- * @desc:
- * @author: cao_wencao
- * @date: 2020-09-22 23:16
- */
- @RestController
- @Slf4j
- @RequestMapping("/product")
- public class ProductController {
- @Autowired
- private ProductService productService;
- /**
- * 扣減庫存,正常->模擬全局事務(wù)提交
- * @param pid
- * @param number
- */
- @RequestMapping("/reduceInventory/commit")
- public void reduceInventoryCommit(Integer pid, Integer number) {
- String token = ServletUtils.getRequest().getHeader("token");
- log.info("從head請求頭透傳過來的值為token:"+ token);
- productService.reduceInventoryCommit(pid, number);
- }
- /**
- * 扣減庫存,異常->模擬全局事務(wù)回滾
- * @param pid
- * @param number
- */
- @RequestMapping("/reduceInventory/rollback")
- public void reduceInventoryRollback(Integer pid, Integer number) {
- productService.reduceInventoryRollback(pid, number);
- }
- //商品信息查詢
- @RequestMapping("/product/{pid}")
- public Product product(@PathVariable("pid") Integer pid) {
- log.info("接下來要進行{}號商品信息的查詢", pid);
- Product product = productService.findByPid(pid);
- log.info("商品信息查詢成功,內(nèi)容為{}", JSON.toJSONString(product));
- return product;
- }
- }
- ProductService接口類
- /**
- * @desc: 商品接口
- * @author: cao_wencao
- * @date: 2020-09-22 23:18
- */
- public interface ProductService {
- //根據(jù)pid查詢商品信息
- Product findByPid(Integer pid);
- //扣減庫存,正常->模擬全局事務(wù)提交
- void reduceInventoryCommit(Integer pid, Integer number);
- //扣減庫存,異常->模擬全局事務(wù)回滾
- void reduceInventoryRollback(Integer pid, Integer number);
- }
- ProductServiceImpl 接口實現(xiàn)類
- /**
- * @desc: 商品服務(wù)實現(xiàn)類
- * @author: cao_wencao
- * @date: 2020-09-22 23:20
- */
- @Service
- public class ProductServiceImpl implements ProductService {
- @Autowired
- private ProductDao productDao;
- @Override
- public Product findByPid(Integer pid) {
- return productDao.findById(pid).get();
- }
- /**
- * 扣減庫存,正常->模擬全局事務(wù)提交
- * @param pid
- * @param number
- */
- @Override
- public void reduceInventoryCommit(Integer pid, Integer number) {
- //查詢
- Product product = productDao.findById(pid).get();
- //省略校驗
- //內(nèi)存中扣減
- product.setStock(product.getStock() - number);
- //保存扣減庫存
- productDao.save(product);
- }
- /**
- * 扣減庫存,異常->模擬全局事務(wù)回滾
- * @param pid
- * @param number
- */
- @Transactional(rollbackFor = Exception.class) //服務(wù)提供方本地事務(wù)注解
- @Override
- public void reduceInventoryRollback(Integer pid, Integer number) {
- //查詢
- Product product = productDao.findById(pid).get();
- //省略校驗
- //內(nèi)存中扣減
- product.setStock(product.getStock() - number);
- //模擬異常
- int i = 1 / 0;
- //保存扣減庫存
- productDao.save(product);
- }
- }
四、參考文檔
seata官網(wǎng):
- http://seata.io/zh-cn/
Seata常見問題:
- http://seata.io/zh-cn/docs/overview/faq.html
Seata整合1.2教程:
- https://www.bilibili.com/video/BV12Q4y1A7Nt
升級1.3教程:
- https://www.bilibili.com/video/BV1Cf4y1X7vR
- https: //mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g
springcloud整合demo:
- https://gitee.com/itCjb/spring-cloud-alibaba-seata-demo
五、完整源碼
- https://github.com/Thinkingcao/SpringCloudLearning/tree/master/springcloud-seata