該類事務須要知足四大特性:ACID
(原子性、一致性、隔離性、持久性),僅限於對單一數據庫資源的訪問控制。java
Atomicity
):指事務做爲總體來執行,要麼所有執行,要麼所有不執行。Consistency
):指事務應確保數據從一個一致的狀態轉變爲另外一個一致狀態。Isolation
):指多個事務併發時,一個事務的執行不該影響其它事務的執行。Durability
):指已提交的事務修改數據會被持久保存。若是將實現了 ACID
的四大事務特性的事務成爲剛性事務的話,那麼基於 BASE
事務要素的事務則成爲柔性事務。node
BASE
是基本可用、柔性狀態和最終一致性這三個特性的縮寫。mysql
Basically Available
):容許分佈式事務參與方不必定要同時在線。Soft state
):則容許系統狀態更新有必定的延時。Eventually consistent
):一般是經過消息傳遞的方式保證系統的 最終一致性。在 ACID
事務中對隔離性的要求很高,在事務執行過程當中,必須將全部的資源鎖定。而柔性事務的理念則是經過業務邏輯將互斥鎖操做從資源層面移至業務層面。經過放寬對 強一致性 的要求,來換取系統吞吐量的提高。git
咱們能夠把一個分佈式事務理解成一個包含了 若干分支事務的全局事務,全局事務的職責是協調其下管轄的分支事務達成一致,要麼一塊兒成功提交,要麼一塊兒失敗回滾。此外,一般分支事務自己就是一個知足 ACID
的本地事務。這是咱們對分佈式事務結構的基本認識,與 XA
是一致的。github
傳統的單體應用中,一個業務操做可能須要調用三個模塊完成,此時數據的一致性有本地事務來保證。redis
隨着業務需求的變化,單體應用被拆分紅微服務應用,業務操做須要調用三個服務來完成,原來的三個模塊被拆分紅三個獨立的應用,分別使用獨立的數據源。此時每一個服務內部的數據一致性由本地事務來保證,可是全局的數據一致性問題沒法保證。spring
在微服務分佈式架構中因爲全局數據一致性無法保證所產生的問題就是分佈式事務問題。簡單來講,一次業務操做須要操做多個數據源或須要進行遠程調用,就會產生分佈式事務問題。sql
這裏咱們會建立三個服務,分別是訂單服務、庫存服務、帳戶服務。當用戶下單時,會在 訂單服務 中建立一個訂單,而後經過遠程調用 庫存服務 扣減當前商品的庫存,再經過遠程調用 帳戶服務 來扣減用戶帳戶裏面的餘額。該業務操做經過兩次遠程調用,跨越三個數據庫,明顯存在分佈式事務問題。數據庫
Seata
是一款開源的分佈式事務解決方案,提供高性能和簡單易用的分佈式事務服務,提供了 AT
、SAGA
和 XA
事務模式。bash
TC
事務協調者:維護全局和分支事務的狀態,驅動全局事務提交或回滾。TM
事務管理器:定義全局事務的範圍,從開始全局事務 > 提交或回滾事務。RM
資源管理器:管理分支事務處理的資源,與 TC
合做以註冊分支事務和報告分支事務的狀態,驅動分支事務提交或回滾。先從官網下載 seata server
,下載地址:https://github.com/seata/seata/releases。
解壓安裝包到指定目錄,修改 conf
目錄下的 file.conf
配置文件。主要修改自定義事務名稱、事務日誌存儲模式爲 db
以及數據庫鏈接信息。
service { #vgroup->rgroup vgroup_mapping.fsp_tx_group = "default" #修改事務組名稱爲:fsp_tx_group,和客戶端自定義的名稱對應 #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } ## transaction log store store { ## store mode: file、db mode = "db" #修改此處將事務信息存儲到數據庫中 ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://localhost:3306/seat-server" #修改數據庫鏈接地址 user = "root" #修改數據庫用戶名 password = "123456" #修改數據庫密碼 min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } }
因爲使用了 db
模式的存儲事務日誌,因此咱們須要建立一個 seata server
數據庫,運行在 seata server
安裝包中的 /conf/db_store.sql
文件。
修改 conf
目錄下的 registry.conf
配置文件,指明配置中心爲 nacos
,並配置 nacos
鏈接信息。
nacos
的安裝及使用能夠參考:使用 Spring Cloud Alibaba Nacos Discovery 實現服務註冊與發現
nacos server
和 seata server
安裝包中的 /bin/seata-server.bat
。seata-order
:存儲訂單的數據庫。seata-storage
:存儲庫存的數據庫。seata-count
:存儲帳戶信息的數據庫。order 表
CREATE TABLE `order` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `user_id` bigint(11) DEFAULT NULL COMMENT '用戶id', `product_id` bigint(11) DEFAULT NULL COMMENT '產品id', `count` int(11) DEFAULT NULL COMMENT '數量', `money` decimal(11,0) DEFAULT NULL COMMENT '金額', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '訂單狀態:0:建立中;1:已完結' AFTER `money` ;
storage 表
CREATE TABLE `storage` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `product_id` bigint(11) DEFAULT NULL COMMENT '產品id', `total` int(11) DEFAULT NULL COMMENT '總庫存', `used` int(11) DEFAULT NULL COMMENT '已用庫存', `residue` int(11) DEFAULT NULL COMMENT '剩餘庫存', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
account 表
CREATE TABLE `account` ( `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `user_id` bigint(11) DEFAULT NULL COMMENT '用戶id', `total` decimal(10,0) DEFAULT NULL COMMENT '總額度', `used` decimal(10,0) DEFAULT NULL COMMENT '已用餘額', `residue` decimal(10,0) DEFAULT '0' COMMENT '剩餘可用額度', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
最後還須要在每一個數據庫的表中建立建立事務日誌表,運行在 seata server
安裝包中的 /conf/db_undo_log.sql
文件。
完成後全部數據庫表如圖所示:
對 seata-order-service
、seata-storage-service
和 seata-account-service
三個服務進行配置大體相同,以 seata-account-service
爲例。
在 application.yml
文件中主要加入如下配置:
spring: cloud: alibaba: seata: tx-service-group: fsp_tx_group #自定義事務組名稱須要與 seata-server 中的對應
文件完整內容以下
server: port: 8081 spring: application: name: seata-account-service cloud: alibaba: seata: tx-service-group: fsp_tx_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/seata-account username: root password: 123456 mybatis: mapperLocations: classpath:mapper/*.xml logging: level: io: seata: info
建立 file.conf
文件,主要修改自定義事務名稱:
service { #vgroup->rgroup vgroup_mapping.fsp_tx_group = "default" #修改自定義事務組名稱 #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" disableGlobalTransaction = false }
建立 registry.conf
配置文件,主要指明 nacos
註冊中心:
registry { # file 、nacos 、eureka、redis、zk type = "nacos" #修改成nacos nacos { serverAddr = "localhost:8848" #修改成nacos的鏈接地址 namespace = "" cluster = "default" } }
在啓動類中取消自動建立數據源
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableDiscoveryClient @EnableFeignClients public class SeataOrderServiceApplication { public static void main(String[] args) { SpringApplication.run(SeataOrderServiceApplication.class, args); } }
建立 DataSourceProxyConfig
配置文件使用 Seata
對數據源進行代理
/** * 使用 Seata 對數據源進行代理 */ @Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
在業務實現類中使用 @GlobalTransaction
註解開啓分佈式事務。
/** * 訂單業務實現類 */ @Service public class OrderServiceImpl implements OrderService { private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class); @Autowired private OrderDao orderDao; @Autowired private StorageService storageService; @Autowired private AccountService accountService; /** * 建立訂單->調用庫存服務扣減庫存->調用帳戶服務扣減帳戶餘額->修改訂單狀態 */ @Override @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) public void create(Order order) { LOGGER.info("開始下單"); //當前服務建立訂單 orderDao.create(order); //遠程調用庫存服務扣減庫存 LOGGER.info("order-service 中扣減庫存開始"); storageService.decrease(order.getProductId(),order.getCount()); LOGGER.info("order-service 中扣減庫存結束"); //遠程調用帳戶服務扣減餘額 LOGGER.info("order-service 中扣減餘額開始"); accountService.decrease(order.getUserId(),order.getMoney()); LOGGER.info("order-service 中扣減餘額結束"); //修改訂單狀態爲已完成 LOGGER.info("order-service 中修改訂單狀態開始"); orderDao.update(order.getUserId(),0); LOGGER.info("order-service 中修改訂單狀態結束"); LOGGER.info("下單結束"); } }
文章做者:彭超
本文首發於我的博客:https://antoniopeng.com/2020/07/20/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E4%B9%8B%20Alibaba%20Seata/
- 版權聲明:本博客全部文章除特別聲明外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 彭超的博客 | Antonio Blog