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

二、整合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這三種數據庫生成undo-log逆向日志回滾表的表創(chuàng)建腳本。
- 在你項目的參與全局事務的數據庫中加入undo_log這張表。undo_log表腳本根據自身數據庫類型來選擇。
 
- -- 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事務相關表
下載Seata1.2的源碼后解壓如上圖,目前支持mysql、oracle、postgresql這三種數據庫,上述三種腳本是針對Seata的Sever端在協調處理分布式事務時所需要的3張表,提供了不同數據庫的global_table表、branch_table表、lock_table表創(chuàng)建腳本,根據自身數據庫執(zhí)行對應的sql腳本執(zhí)行即可。
這里以mysql為例,在你的mysql數據庫中創(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 如果微服務是SpringCloud
- <!-- 分布式事務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 如果微服務是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相關屬性參數。
- ##配置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模式相關參數配置。
- ##配置seata-server的數據存儲方式,支持本地文檔和數據庫。
 - ## 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控制臺,如果有需要修改參數,可直接通過登錄nacos控制臺修改。
操作如下圖:

8. application.yml配置
從官方github倉庫:https://github.com/seata/seata/tree/develop/script/client 拿到參考配置做修改,加到你項目的application.yml文件中。
- #Seata分布式事務配置(AT模式)
 - seata:
 - enabled: true
 - application-id: ${spring.application.name}
 - #客戶端和服務端在同一個事務組
 - 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"
 - #服務注冊到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,成功后,運行自己的服務提供者,服務參與者。在全局事務調用者(發(fā)起全局事務的服務)的接口上加入@GlobalTransactional注解
到此為止,整合SpringCloud整合seata1.2及seata1.2整合nacos的配置與注冊中心全部整合完成了。
三、項目準備
如果你經過前面的步驟搭建Seata環(huán)境完成了,那么你可以嘗試一下啟動項目,控制臺無異常則搭建成功。
那么下面準備以Seata官方文檔上的一個經典例子為題,模擬用戶下單,創(chuàng)建訂單同時扣減庫存數量這一過程中產生的分布式事務問題,然后使用Seata解決,正好使用以下Seata的特性。
1. 訂單服務
- OrderController
 
- /**
 - * @desc: 訂單服務
 - * @author: cao_wencao
 - * @date: 2020-09-22 23:27
 - */
 - @RestController
 - @Slf4j
 - @RequestMapping("/order")
 - public class OrderController {
 - @Autowired
 - private OrderServiceImpl orderService;
 - /**
 - * 用戶購買下單,模擬全局事務提交
 - * @param pid
 - * @return
 - */
 - @RequestMapping("/purchase/commit/{pid}")
 - public Order orderCommit(@PathVariable("pid") Integer pid) {
 - return orderService.createOrderCommit(pid);
 - }
 - /**
 - * 用戶購買下單,模擬全局事務回滾
 - * @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;
 - //用戶下單,模擬全局事務提交
 - public Order createOrderCommit(Integer pid) {
 - log.info("接收到{}號商品的下單請求,接下來調用商品微服務查詢此商品信息", pid);
 - //1 調用商品微服務,查詢商品信息
 - Product product = productService.findByPid(pid);
 - log.info("查詢到{}號商品的信息,內容是:{}", 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;
 - }
 - //用戶下單,模擬全局事務回滾
 - @GlobalTransactional//全局事務控制
 - public Order createOrderRollback(Integer pid) {
 - log.info("接收到{}號商品的下單請求,接下來調用商品微服務查詢此商品信息", pid);
 - //1 調用商品微服務,查詢商品信息
 - Product product = productService.findByPid(pid);
 - log.info("查詢到{}號商品的信息,內容是:{}", 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;
 - }
 - }
 
- 商品服務的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);
 - //扣減庫存,模擬全局事務提交
 - //參數一: 商品標識
 - //參數二:扣減數量
 - @RequestMapping("/product/reduceInventory/commit")
 - void reduceInventoryCommit(@RequestParam("pid") Integer pid,
 - @RequestParam("number") Integer number);
 - //扣減庫存,模擬全局事務回滾
 - //參數一: 商品標識
 - //參數二:扣減數量
 - @RequestMapping("/product/reduceInventory/rollback")
 - void reduceInventoryRollback(@RequestParam("pid") Integer pid,
 - @RequestParam("number") Integer number);
 - }
 
2. 商品服務
- ProductController
 
- /**
 - * @desc:
 - * @author: cao_wencao
 - * @date: 2020-09-22 23:16
 - */
 - @RestController
 - @Slf4j
 - @RequestMapping("/product")
 - public class ProductController {
 - @Autowired
 - private ProductService productService;
 - /**
 - * 扣減庫存,正常->模擬全局事務提交
 - * @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);
 - }
 - /**
 - * 扣減庫存,異常->模擬全局事務回滾
 - * @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("商品信息查詢成功,內容為{}", JSON.toJSONString(product));
 - return product;
 - }
 - }
 
- ProductService接口類
 
- /**
 - * @desc: 商品接口
 - * @author: cao_wencao
 - * @date: 2020-09-22 23:18
 - */
 - public interface ProductService {
 - //根據pid查詢商品信息
 - Product findByPid(Integer pid);
 - //扣減庫存,正常->模擬全局事務提交
 - void reduceInventoryCommit(Integer pid, Integer number);
 - //扣減庫存,異常->模擬全局事務回滾
 - void reduceInventoryRollback(Integer pid, Integer number);
 - }
 
- ProductServiceImpl 接口實現類
 
- /**
 - * @desc: 商品服務實現類
 - * @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();
 - }
 - /**
 - * 扣減庫存,正常->模擬全局事務提交
 - * @param pid
 - * @param number
 - */
 - @Override
 - public void reduceInventoryCommit(Integer pid, Integer number) {
 - //查詢
 - Product product = productDao.findById(pid).get();
 - //省略校驗
 - //內存中扣減
 - product.setStock(product.getStock() - number);
 - //保存扣減庫存
 - productDao.save(product);
 - }
 - /**
 - * 扣減庫存,異常->模擬全局事務回滾
 - * @param pid
 - * @param number
 - */
 - @Transactional(rollbackFor = Exception.class) //服務提供方本地事務注解
 - @Override
 - public void reduceInventoryRollback(Integer pid, Integer number) {
 - //查詢
 - Product product = productDao.findById(pid).get();
 - //省略校驗
 - //內存中扣減
 - product.setStock(product.getStock() - number);
 - //模擬異常
 - int i = 1 / 0;
 - //保存扣減庫存
 - productDao.save(product);
 - }
 - }
 
四、參考文檔
seata官網:
- 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
 




















 
 
 

















 
 
 
 