一. 分佈式事務前言
1. 數據庫管理系統中事務(transaction)的四個特性:簡稱ACID(這種特性簡稱剛性事物)
原子性(Atomicity):原子性是指事務是一個不可再分割的工作單元,事務中的操作要麼都發生,要麼都不發生。
一致性(Consistency):一致性是指在事務開始之前和事務結束以後,數據庫的完整性約束沒有被破壞;這是說數據庫事務不能破壞關係數據的完整性以及業務邏輯上的一致性。
隔離性(Isolation):多個事務併發訪問時,事務之間是隔離的,一個事務不應該影響其它事務運行效果。
持久性(Durability):持久性,意味着在事務完成以後,該事務所對數據庫所作的更改便持久的保存在數據庫之中,並不會被回滾。(完成的事務是系統永久的部分,對系統的影響是永久性的,該修改即使出現致命的系統故障也將一直保持)
2. CAP理論(帽子原理)
由於對系統或者數據進行了拆分,我們的系統不再是單機系統,而是分佈式系統,針對分佈式系統的CAP原理包含如下三個元素:
C:Consistency 一致性:在分佈式系統中的所有數據 備份,在同一時刻具有同樣的值,所有節點在同一時刻讀取的數據都是最新的數據副本(例如:Redis主從複製)
A:Availability 可用性:好的響應性能。完全的可用性指的是在任何故障模型下,服務都會在有限的時間內處理完成並進行響應(例如:Ngnix+tomcat負載均衡)
P: Partition tolerance 分區容忍性:儘管網絡上有部分消息丟失,但系統仍然可繼續工作
CAP原理指的是,這三個要素最多隻能同時實現兩點,不可能三者兼顧。因此在進行分佈式架構設計時,必須做出取捨。而對於分佈式數據系統,分區容忍性是基本要求,否則就失去了價值,所以一般而言P是必須要滿足的(即可以容忍宕機或者網絡故障,因爲P是大概率事件,有些情況不可避免)。因此設計分佈式數據系統,就是在一致性和可用性之間取一個平衡。(分佈式系統中,網絡出現故障,不可能同時保持一致性+可用性)。
對於大多數web應用,其實並不需要強一致性,因此犧牲一致性而換取高可用性,是目前多數分佈式數據庫產品的方向。 當然,犧牲一致性,並不是完全不管數據的一致性,否則數據是混亂的,那麼系統可用性再高分佈式再好也沒有了價值。犧牲一致性,只是不再要求關係型數據庫中的強一致性,而是隻要系統能達到最終一致性即可,考慮到客戶體驗,這個最終一致的時間窗口,要儘可能的對用戶透明,也就是需要保障“用戶感知到的一致性”。通常是通過數據的多份異步複製來實現系統的高可用和數據的最終一致性的,“用戶感知到的一致性”的時間窗口則取決於數據複製到一致狀態的時間。
3. Base理論
BASE理論是指,Basically Available(基本可用)、Soft-state( 軟狀態/柔性事務)、Eventual Consistency(最終一致性)。是基於CAP定理演化而來,是對CAP中一致性和可用性權衡的結果。
核心思想:即使無法做到強一致性,但每個業務根據自身的特點,採用適當的方式來使系統達到最終一致性。
① 基本可用:指分佈式系統在出現故障的時候,允許損失部分可用性,保證核心可用。但不等價於不可用。比如:搜索引擎0.5秒返回查詢結果,但由於故障,2秒響應查詢結果;網頁訪問過大時,部分用戶提供降級服務等。
② 軟狀態:軟狀態是指允許系統存在中間狀態,並且該中間狀態不會影響系統整體可用性。即允許系統在不同節點間副本同步的時候存在延時。
③ 最終一致性:系統中的所有數據副本經過一定時間後,最終能夠達到一致的狀態,不需要實時保證系統數據的強一致性。最終一致性是弱一致性的一種特殊情況。BASE理論面向的是大型高可用可擴展的分佈式系統,通過犧牲強一致性來獲得可用性。ACID是傳統數據庫常用的概念設計,追求強一致性模型。
4. 柔性事務和剛性事務
柔性事務滿足BASE理論(基本可用,最終一致),剛性事務滿足ACID理論。
5. 兩段提交協議 - 2PC(Two-PhaseCommit)
第一階段: 準備階段:協調者向參與者發起指令,參與者評估自己的狀態,如果參與者評估指令可以完成,則會寫redo或者undo日誌,讓後鎖定資源,執行操作,但並不提交。
第二階段:如果每個參與者明確返回準備成功,則協調者向參與者發送提交指令,參與者釋放鎖定的資源,如何任何一個參與者明確返回準備失敗,則協調者會發送中指指令,參與者取消已經變更的事務,釋放鎖定的資源。
兩階段提交方案應用非常廣泛,幾乎所有商業OLTP數據庫都支持XA協議。但是兩階段提交方案鎖定資源時間長,對性能影響很大,基本不適合解決微服務事務問題。
缺點:如果協調者宕機,參與者沒有協調者指揮,則會一直阻塞。
三段提交協議 - 3PC(Three-PhaseCommit)
核心:在2pc的基礎上增加了一個詢問階段(第一階段),確認網絡,避免阻塞,二三階段就是上面的2pc
三階段提交協議是兩階段提交協議的改進版本。它通過超時機制解決了阻塞的問題,並且把兩個階段增加爲三個階段:
詢問階段:協調者詢問參與者是否可以完成指令,協調者只需回答是還是不是,而不需要做真正的操作,這個階段超時導致中止
準備階段:如果在詢問階段所有的參與者都返回可以執行操作,協調者向參與者發送預執行請求,然後參與者寫redo和undo日誌,執行操作,但是不提交操作;如果在詢問階段任何參與者返回不能執行操作的結果,則協調者向參與者發送中止請求,這裏的邏輯與兩階段提交協議的的準備階段是相似的,這個階段超時導致成功
提交階段:如果每個參與者在準備階段返回準備成功,也就是預留資源和執行操作成功,協調者向參與者發起提交指令,參與者提交資源變更的事務,釋放鎖定的資源;如果任何一個參與者返回準備失敗,也就是預留資源或者執行操作失敗,協調者向參與者發起中止指令,參與者取消已經變更的事務,執行undo日誌,釋放鎖定的資源,這裏的邏輯與兩階段提交協議的提交階段一致
二. Seata簡介
Seata:簡易可擴展的自治式分佈式事務管理框架,其前身是fescar。是一種簡單分佈式事務的解決方案。
Seata給用戶提供了AT、TCC、SAGA和XA事務模式,AT模式是阿里雲中推出的商業版本GTS全局事務服務,目前Seata的版本已經到了1.0,我們本篇用是0.9版本。官網:https://github.com/seata/seata
Seata由3部分組成:
1.事務協調器(TC):維護全局事務和分支事務的狀態,驅動全局提交或回滾,相當於LCN的協調者。
2.事務管理器(TM):定義全局事務的範圍:開始全局事務,提交或回滾全局事務,相當於LCN中發起方。
3.資源管理器(RM):管理分支事務正在處理的資源,與TC進行對話以註冊分支事務並報告分支事務的狀態,並驅動分支事務的提交或回滾,相當於是LCN中的參與方。
白話文分析Seata實現原理:(與LCN基本一致,LCN前面博客有講)
1. 發起方(TM)和我們的參與方(RM)項目啓動之後和協調者TC保持長連接;
2. 發起方(TM)調用接口之前向 TC 獲取一個全局的事務的id 爲xid,註冊到Seata中.Aop實現
3. 使用Feign客戶端調用接口的時候,Seata重寫了Feign客戶端,在請求頭中傳遞該xid。
4. 參與方(RM)從請求頭中獲取到該xid,方法執行完後不會立馬提交,而是等待發起方調完接口後將狀態提交到協調者,由協調者再告知參與方狀態。
三. Seata環境搭建
下載對應Jar包並解壓
首先在訂單庫和派單庫(三中的業務庫)分別導入conf目錄下的undo_log.sql(專門做回滾用的),新建一個seata庫,並把db_store.sql導入到seata庫,該庫主要是存放seata服務端的一些信息。
接下來修改register.conf,type改爲nacos,nacos裏面localhost後面加上:8848,詳細如下圖:
最後修改file.conf,store裏面mode值改爲db,並修改MySQL連接信息,注意庫爲上面建立的seata庫:
雙擊bin目錄下的seata-server.bat,啓動成功如下:(需先啓動Nacos)
四. 客戶端整合SeataServer
分佈式事務解決方案有很多,如RabbitMQ最終一致性,RocketMQ事務消息,開源框架LCN,以及阿里Seata等。
業務場景:與前面博客RocketMQ解決分佈式事務場景一致:
如圖所示,相信我們都定過外賣,當提交訂單後會在數據庫生成一條訂單,然後等待分配騎手送餐。
該業務在SpringCloud微服務架構拆分爲兩個服務,訂單服務service-order和派單服務service-distribute,訂單服務添加訂單後,通過feign客戶端調用派單服務的接口進行分配騎手,那麼分佈式事務問題就來了,當訂單服務調用完第二行代碼,派單接口執行完畢,咔嚓,第三行報了個錯,那麼訂單接口會回滾,而派單則已提交事務,那麼就造成數據不一致問題,故分佈式事務問題,本文我們用Seata框架解決。
準備工作:分別建立訂單表(左:order_table),派單表(右:distribute_table)
由於我們是SpringCloudAlibaba系列串講,在前面博客建立好的service-impl添加如下依賴:
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.4</version>
</dependency>
application.yml或者bootstrap.yml 添加Seata配置,且將上面修改好的file.conf和registry.conf拷貝到resources目錄下:
訂單服務,派單服務啓動類分別移除默認DataSource配置:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
在訂單服務和派單服務分別建立配置文件:
package com.xyy.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
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.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
接下來編寫核心業務,首先編寫派單服務:
public interface DistributeService {
@RequestMapping("/distributeOrder")
String distributeOrder(@RequestParam("orderNumber") String orderNumber);
}
@RestController
public class DistributeServiceImpl implements DistributeService {
@Autowired
private DispatchMapper dispatchMapper;
@Override
public String distributeOrder(String orderNumber) {
DispatchEntity dispatchEntity = new DispatchEntity(orderNumber,136L);
dispatchMapper.insertDistribute(dispatchEntity);
return "派單成功";
}
}
@Mapper
public interface DispatchMapper {
// 新增派單任務
@Insert("insert into distribute_table values (null,#{orderNumber},#{userId})")
@Options(useGeneratedKeys=true)
int insertDistribute(DispatchEntity distributeEntity);
}
接下來編寫訂單服務:
@RestController
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private DistributeServiceFeign distributeServiceFeign;
@RequestMapping("/insertOrder")
@GlobalTransactional
public String insertOrder(int age) {
String orderNumber = UUID.randomUUID().toString(); // 用uuid暫時代替雪花算法
OrderEntity orderEntity = createOrder(orderNumber);
// 1.向訂單數據庫表插入數據
int result = orderMapper.insertOrder(orderEntity);
if (result < 0) {
return "插入訂單失敗";
}
// 2.調用派單服務,實現對該筆訂單派單 遠程調用派單接口
String resultDistribute = distributeServiceFeign.distributeOrder(orderNumber);
// 判斷調用接口失敗的代碼...
int i = 1 / age;
return resultDistribute;
}
public OrderEntity createOrder(String orderNumber) {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderName("騰訊視頻vip-年費");
orderEntity.setCreateTime(new Date());
orderEntity.setOrderMoney(new BigDecimal(300));
orderEntity.setOrderStatus(0); // 未支付
orderEntity.setGoodsId(101L); // 模擬商品id爲101
orderEntity.setOrderNumber(orderNumber);
return orderEntity;
}
}
@FeignClient("service-distribute")
public interface DistributeServiceFeign extends DistributeService {
}
@Mapper
public interface OrderMapper {
@Insert("insert into order_table values (null,#{orderNumber}, #{orderName}, #{orderMoney}, #{orderStatus}, #{goodsId},#{createTime})")
@Options(useGeneratedKeys=true)
Integer insertOrder(OrderEntity orderEntity);
}
分別啓動Nacos,Seata,service-order,service-distribute,正常訪問訂單接口,則訂單,派單表分別新增一條數據:
且兩個控制檯都會打印Commited日誌:
異常訪問訂單接口,則訂單,派單表事務都會回滾,都不會新增數據,控制檯都會打印Rollbacked:
【總結】:目前主流分佈式事務解決方案有很多,如RabbitMQ最終一致性,RocketMQ事務消息,LCN假關閉,阿里Seata,可以根據業務合理選擇解決方案,畢竟先把技術Get到,項目技術選型也會多一種選擇 ~