分佈式事務與數據一致性

如果不會mycat和sharding-jdbc一定要看,不然文章看不懂
mycat和sharding-jdbc詳解:https://blog.csdn.net/qq_34886352/article/details/104458171

一、分佈式全局id

1、分庫分表引發的id問題

  在正常的單庫系統下,爲了效率id通常採用自增的方式,但是分庫分表的情況,依舊採用這種方法,那麼每張表每個庫的id都是從0開始的,id就是去了唯一標識的作用,不同的表中會存在相同的id,導致業務數據嚴重混亂

2、解決id問題

(1)UUID

  UUID是通用的唯一識別碼(Universally Unique Identifier),使用UUID爲每一條數據生成id,可以保證所有的id都是不同的,但是UUID有較爲明顯的缺點,只是一個單純的32位無規則字符串,沒有實際意義,長度過長(數據匹配的時候就會慢),生成較慢。
  mycat不支持UUID,sharding-jdbc支持UUID
表結構,t_order表:

字段 中文解釋
order_id 訂單id
total_amount 價格
order_status 訂單狀態
user_id 用戶id

1)sharding-jdbc使用UUID自動添加id
  如果主鍵不是分庫分表的關鍵字段,直接在springboot的配置文件中添加兩個屬性就可以完成配置,在插入數據的時候就不要在填寫主鍵字段的值了

#指定主建字段,其中t_order是邏輯表名,order_id是字段名稱 
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
#指定主鍵生成規則
spring.shardingsphere.sharding.tables.t_order.key-generator.type=UUID

  如果主鍵是分庫分表的關鍵字段,例如:order表使用order_id作爲分表關鍵字段,但是order_id同時也是主鍵需要用UUID,這時候就不能用簡單的分表分表規則,來錄入數據。
同樣的還是需要先指定主鍵字段,和主鍵生成規則

#指定主建字段,其中t_order是邏輯表名,order_id是字段名稱 
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
#指定主鍵生成規則
spring.shardingsphere.sharding.tables.t_order.key-generator.type=UUID

然後指定分表字段,和分表規則

#指定分表字段
spring.shardingsphere.sharding.tables.t_order.table-standard.sharding-column=order_id
#指定分表規則的具體類
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.example.shardingdemo.MySharding

創建一個類MySharding

public class MySharding implements PreciseShardingAlgorithm<String>{
    /**
     * availableTargetNames:可用的分片表(這條數據對應的多有的分片表)
     * ShardingValue:當前的分片值(也就是分表分庫的關鍵字段的值)
     */
    @Override
    public String doSharding(Collection<String> availableTargetNames,PreciseShardingValue<String> shardingValue){
        String id = shardingValue.getValue();
        int mode = id.hashCode()%availableTargetNames.size();
        
        Stirng[] strings=availableTargetNames.toArray(new String[0]);
        
        return string[mode];
    }
}

這樣就完成了,可用正常的使用了
附:springxml的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:sharding="http://shardingsphere.apache.org/schema/shardingsphere/sharding"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://shardingsphere.apache.org/schema/shardingsphere/sharding
                        http://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context.xsd
                        http://www.springframework.org/schema/tx
                        http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    <!-- 添加數據源 -->
    <bean id="ds0" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <!-- 數據庫驅動 -->
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
        <property name="username" value="用戶名"/>
        <property name="password" value="密碼"/>
        <property name="jdbcUrl" value="jdbc:mysql://192.168.85.200:3306/sharding_order?serverTimezone=Asia/Shanghai&amp;useSSL=false"/>
    </bean>
    
    <!-- 第二個數據源 -->
    <bean id="ds1" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <!-- 數據庫驅動 -->
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
        <property name="username" value="用戶名"/>
        <property name="password" value="密碼"/>
        <property name="jdbcUrl" value="jdbc:mysql://192.168.85.201:3306/sharding_order?serverTimezone=Asia/Shanghai&amp;useSSL=false"/>
    </bean>
    
    <!-- 配置sharding-jdbc -->
    <sharding:data-source id="sharding-data-source">
        <!-- 配置數據源 -->
        <sharding:sharding-rule data-source-name="ds0,ds1">
            <sharding:table-rules>
                <!-- logic-table :分片表的邏輯表名 -->
                <!-- atcual-data-nodes :實際的數據節點   ds$->{0..1}:分爲兩個部分ds是數據源的前綴,$->{0..1}是佔位符,等同於${} -->
                <!--  database-strategy-ref :庫的分片策略 -->
                <!--  table-strategy-ref :表的分片策略 -->
                <!--  重點:key-generator-ref:主鍵規則-->
                <sharding:table-rule logic-table="t_order" 
                atcual-data-nodes="ds$->{0..1}.t_order_$->{1..2}"
                database-strategy-ref="databaseStrategy"
                table-strategy-ref="standard" 
                key-generator-ref="uuid"
                />
            </sharding:table-rules>
        </sharding:sharding-rule>
    </sharding:data-source>
    
    <!--  重點:指定主鍵和生成規則 -->
    <sharding:key-generator id="uuid" column="order_id" type="UUID">
    
    <!-- 數據庫的分片規則 -->
    <!-- sharding-column:分庫使用的字段 -->
    <!-- algorithm-expression:分片規則,對user_id取模 -->
    <sharding:inline-strategy id="databaseStrategy" sharding-column="user_id" algorithm-expression="ds$->{user_id%2}"/>
    
    <bean id="myShard" class="com.example.shardingdemo.MySharding"/>
    
    <!-- 表的分片規則 -->
    <sharding:standard-strategy id="standard" sharding-column="order_id" precise-algorithm-ref="myShard"/>
    
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="sharding-data-source"/>
        <property name="mapperLocations" value="classpath*:/mybatis/*.xml"/>
    </bean>
</beans>
(2)統一ID序列生成(使用mycat生成id)

再次提醒如果不會mycat和sharding-jdbc一定要看,不然文章看不懂
https://blog.csdn.net/qq_34886352/article/details/104458171

  ID的值統一的從一個集中的ID序列生成器中獲取,例如:每一個mysql都連接mycat,mycat生成所有的id分發給mysql。
MyCat有兩種方式生成id:

  • 本地文件方式:將id序列通過零時文件的方式存在本地的文件中,每次使用都從文件中讀取,但是mycat重啓之後id就會重新計算,一般用於測試環境
  • 數據庫方式:將id存放在數據庫中,mycat 重啓後不會重新計算,一般用於生存環境

統一id生成器有個非常明顯的缺點,當併發量大的時候,id生成器壓力就會非常巨大,如果生成器宕機,整個數據庫就可能無法使用

表結構,o_order表:

字段 中文解釋
id id
total_amount 價格
order_status 訂單狀態

1)修改mycat的server.xml

#修改checkSQLschema的值爲false,這裏是個bug,如果不修改爲false,在執行分表id插入的時候就會報sql語句語法錯誤,mycat應該在後期會修復
checkSQLschema="false"


#找到sequnceHandlerType,整個是控制id生成方式的
#0:本地文件方式
#1:數據庫方式
#2:本地時間戳的方式(雪花算法)
<property name="sequnceHandlerType">0</property>

2)修改mycat的schema.xml(可選)
指定表的主鍵,並且是自增的

#autoIncrement:屬性是否自增
#primaryKey:指定主鍵的字段
<table name="o_order" autoIncrement="true" primaryKey="id" dataNode="dn200,nd201" rule="auto-sharding-long">

3)修改mycat的sequence_conf.properties

  • sequence_conf.properties:本地文件方式的配置文件
  • sequence_db_conf.properties:數據庫方式的配置文件
  • sequence_distributed_conf.properties:分佈式方式的配置文件,基於zookeeper
  • sequence_time_conf.properties:本地時間戳的方式的配置文件
#default global sequence
#GLOBAL是全局的配置

#歷史的id
GLOBAL.HISIDS=
#最小的id數
GLOBAL.MINID=10001
#最大的id數
GLOBAL.MAXID=20000
#當前的id
GLOBAL.CURID=10000

#O_ORDER是表的名稱,這裏一定要和數據的表對應上,多個表就要寫多組
O_ORDER.HISIDS=
O_ORDER.MINID=10001
O_ORDER.MAXID=20000
O_ORDER.CURID=10000

4)啓動mycat,並插入數據

insert into o_order(id,total_amount,order_status)values(
    --這句話就是從mycat裏面取id,mycatseq_是固定的 後面跟上在sequence_conf.properties中配置的前綴
    next value FOR mycatseq_O_ORDER,
    88,
    3
)

如果配置了步驟2,可以不需要寫id

insert into o_order(total_amount,order_status)values(88,3);

如果是文件方式的配置到這裏就結束了,下面就是數據庫方式的配置,這裏接上第2部,3不用做了,別忘記第二步sequnceHandlerType要設置爲1

5)在mycat的conf文件夾下找到dbseq.sql

dbseq.sql裏面有數據庫方式需要的建表語句,在任意的一個庫中執行語句,新建出表MYCAT_SEQUENCE,同時還會創建出4個函數:mycat_seq_currval、mycat_seq_nextval、mycat_seq_nextvals、mycat_seq_setval
在表中添加,一條數據,之前的不要刪

--配置的表前綴,當前id值,間隔數
insert into MYCAT_SEQUENCE(name,current_value,increment)values(O_ORDER,1,1);

6)修改mycat的sequence_db_conf.properties

#dn200是配置在schema.xml中數據節點(dataNode),就是MYCAT_SEQUENCE所在的數據庫,
GLOBAL=dn200
O_ORDER=dn200

配置好後重啓mycat,這樣就配置好了,使用參看步驟4

(3)雪花算法

mycat的配置參考使用mycat生成id,需要修改的配置都在下面,重複的步驟就不寫了
同樣的sharding-jdbc參考上面的uuid的配置
  雪花算法(SnowFlake)是由Twitter剔除的分佈式id算法,一個64bit(64bit是指2進制)的long型數字,引入了時間戳,保證了id的遞增性質
以下爲每位的解釋

  • 第一位爲0,表示是正數,固定爲0
  • 接着是41位的時間戳,記錄的是一段時間,雪花算法要求填寫一個開始時間,用開始時間減去當前時間,得到時間戳
  • 跟着5位機房id
  • 跟着5位機器id,可以和之前的5位組合在一起使用(做多可以配置1024個機器編碼,讓不同機器使用同一個機器碼,就不保證絕對不重複了)
  • 最後12位不規則序列,保證在同一時間點上,有2^12次方個併發量

  在使用雪花算法的時候需要注意時間回調的問題,之前說過雪花算法需要設定一個開始時間,如果系統已經運行一段實際生產了id後,調整了設定的時間,就有可能出現重複的數據,

在使用雪花算法之前,要注意數據庫表的主鍵是否是bigint類型的,長度是否有19位

1)mycat設置雪花算法

  1. 修改server.xm
#設置爲2
<property name="sequnceHandlerType">0</property>
  1. 修改schema.xml
    修改分片表的分片規則
<!-- mod-long:取模的方式分表 -->
<table name="o_order" autoIncrement="true" primaryKey="id" dataNode="dn200,nd201" rule="mod-long>
  1. 修改sequence_time_conf.properties
#這兩個值都要小於32
#機房id
WORKIN=01
#機器id
DATAACENTERID=01

其他的按照使用mycat生成id的方法做就行了
刷新配置,或者重啓mycat

2)sharding-jdbc設置雪花算法
1.修改springboot的配置文件

#指定主建字段,其中t_order是邏輯表名,order_id是字段名稱 
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
#指定主鍵生成規則
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
#有可能會報紅 沒有關係,可以使用
#worker.id:機房id+機器id一共10位數的2進制,轉換成10進制最大爲1024,這個值不能超過1024 
spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=234
#最大回調時間,單位毫秒
spring.shardingsphere.sharding.tables.t_order.key-generator.props.max.tolerate.time.difference.milliseconds=10

2.修改MySharding類

public class MySharding implements PreciseShardingAlgorithm<Long>{
    /**
     * availableTargetNames:可用的分片表(這條數據對應的多有的分片表)
     * ShardingValue:當前的分片值(也就是分表分庫的關鍵字段的值)
     */
    @Override
    public String doSharding(Collection<String> availableTargetNames,PreciseShardingValue<Long> shardingValue){
        Long id = shardingValue.getValue();
        Long mode = id%availableTargetNames.size();
        
        Stirng[] strings=availableTargetNames.toArray(new String[0]);
        
        return string[(int)mode];
    }
}

設置完成,正常插入數據就可以了

附:springxml的配置方法

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:sharding="http://shardingsphere.apache.org/schema/shardingsphere/sharding"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://shardingsphere.apache.org/schema/shardingsphere/sharding
                        http://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context.xsd
                        http://www.springframework.org/schema/tx
                        http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    <!-- 添加數據源 -->
    <bean id="ds0" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <!-- 數據庫驅動 -->
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
        <property name="username" value="用戶名"/>
        <property name="password" value="密碼"/>
        <property name="jdbcUrl" value="jdbc:mysql://192.168.85.200:3306/sharding_order?serverTimezone=Asia/Shanghai&amp;useSSL=false"/>
    </bean>
    
    <!-- 第二個數據源 -->
    <bean id="ds1" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <!-- 數據庫驅動 -->
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
        <property name="username" value="用戶名"/>
        <property name="password" value="密碼"/>
        <property name="jdbcUrl" value="jdbc:mysql://192.168.85.201:3306/sharding_order?serverTimezone=Asia/Shanghai&amp;useSSL=false"/>
    </bean>
    
    <!-- 配置sharding-jdbc -->
    <sharding:data-source id="sharding-data-source">
        <!-- 配置數據源 -->
        <sharding:sharding-rule data-source-name="ds0,ds1">
            <sharding:table-rules>
                <!-- logic-table :分片表的邏輯表名 -->
                <!-- atcual-data-nodes :實際的數據節點   ds$->{0..1}:分爲兩個部分ds是數據源的前綴,$->{0..1}是佔位符,等同於${} -->
                <!--  database-strategy-ref :庫的分片策略 -->
                <!--  table-strategy-ref :表的分片策略 -->
                <!--  重點:key-generator-ref:主鍵規則-->
                <sharding:table-rule logic-table="t_order" 
                atcual-data-nodes="ds$->{0..1}.t_order_$->{1..2}"
                database-strategy-ref="databaseStrategy"
                table-strategy-ref="standard" 
                key-generator-ref="snow"
                />
            </sharding:table-rules>
        </sharding:sharding-rule>
    </sharding:data-source>
    
    <!--  重點:指定主鍵和生成規則 -->
    <sharding:key-generator id="snow" column="order_id" type="SNOWFLAKE" props-ref="snowprop">

    <!-- 重點:配置雪花算法的參數 -->
    <bean:properties id="snowprop">
        <!-- worker.id:機房id+機器id一共10位數的2進制,轉換成10進制最大爲1024,這個值不能超過1024 -->
        <prop key="worker.id">678</prop>
        <!-- 可以容忍的最大回調時間,單位毫秒 -->
        <prop key="max.tolerate.time.difference.milliseconds">10</prop>
    </bean>
   
    
    <!-- 數據庫的分片規則 -->
    <!-- sharding-column:分庫使用的字段 -->
    <!-- algorithm-expression:分片規則,對user_id取模 -->
    <sharding:inline-strategy id="databaseStrategy" sharding-column="user_id" algorithm-expression="ds$->{user_id%2}"/>
    
    <bean id="myShard" class="com.example.shardingdemo.MySharding"/>
    
    <!-- 表的分片規則 -->
    <sharding:standard-strategy id="standard" sharding-column="order_id" precise-algorithm-ref="myShard"/>
    
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="sharding-data-source"/>
        <property name="mapperLocations" value="classpath*:/mybatis/*.xml"/>
    </bean>
</beans>

二、分佈式事物

  分佈式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不同的分佈式系統的不同節點之上。簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分佈在不同的服務器上,且屬於不同的應用,分佈式事務需要保證這些小操作要麼全部成功,要麼全部失敗。本質上來說,分佈式事務就是爲了保證不同數據庫的數據一致性。

1、基本理論

(1)CAP原理

一致性(Consistency)
訪問所有的節點得到的數據應該是一樣的。注意,這裏的一致性指的是強一致性,也就是數據更新完,訪問任何節點看到的數據完全一致,要和弱一致性,最終一致性區分開來。

可用性(Availability)
所有的節點都保持高可用性。注意,這裏的高可用還包括不能出現延遲,比如如果節點B由於等待數據同步而阻塞請求,那麼節點B就不滿足高可用性。也就是說,任何沒有發生故障的服務必須在有限的時間內返回合理的結果集。

分區容忍性(Partiton tolerence)
這裏的分區是指網絡意義上的分區,由於網絡是不可靠的,所有節點之間很可能出現無法通訊的情況,在節點不能通信時,要保證系統可以繼續正常服務。

CAP原理說,一個數據分佈式系統不可能同時滿足C和A和P這3個條件。所以系統架構師在設計系統時,不要將精力浪費在如何設計能滿足三者的完美分佈式系統,而是應該進行取捨。由於網絡的不可靠性質,大多數開源的分佈式系統都會實現P,也就是分區容忍性,之後在C和A中做抉擇。

(2)ACID原理

原子性(Atomicity)
  原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾,這和前面兩篇博客介紹事務的功能是一樣的概念,因此事務的操作如果成功就必須要完全應用到數據庫,如果操作失敗則不能對數據庫有任何影響。

一致性(Consistency)
  一致性是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。
  拿轉賬來說,假設用戶A和用戶B兩者的錢加起來一共是5000,那麼不管A和B之間如何轉賬,轉幾次賬,事務結束後兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。

隔離性(Isolation)
  隔離性是當多個用戶併發訪問數據庫時,比如操作同一張表時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
  即要達到這麼一種效果:對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始之前就已經結束,要麼在T1結束之後纔開始,這樣每個事務都感覺不到有其他事務在併發地執行。
  關於事務的隔離性數據庫提供了多種隔離級別,稍後會介紹到。

持久性(Durability)
  持久性是指一個事務一旦被提交了,那麼對數據庫中的數據的改變就是永久性的,即便是在數據庫系統遇到故障的情況下也不會丟失提交事務的操作。
  例如我們在使用JDBC操作數據庫時,在提交事務方法後,提示用戶事務操作完成,當我們程序執行完成直到看到提示後,就可以認定事務以及正確提交,即使這時候數據庫出現了問題,也必須要將我們的事務完全執行完成,否則就會造成我們看到提示事務處理完畢,但是數據庫因爲故障而沒有執行事務的重大錯誤

(3)BASE原理

基本可用(Basically Available)
  基本可用指分佈式系統在出現故障時,系統允許損失部分可用性,即保證核心功能或者當前最重要功能可用。對於用戶來說,他們當前最關注的功能或者最常用的功能的可用性將會獲得保證,但是其他功能會被削弱。

軟狀態(Soft-state)
  軟狀態允許系統數據存在中間狀態,但不會影響系統的整體可用性,即允許不同節點的副本之間存在暫時的不一致情況。

最終一致性(Eventually Consistent)
  最終一致性要求系統中數據副本最終能夠一致,而不需要實時保證數據副本一致。例如,銀行系統中的非實時轉賬操作,允許 24 小時內用戶賬戶的狀態在轉賬前後是不一致的,但 24 小時後賬戶數據必須正確。
  最終一致性是 BASE 原理的核心,也是 NoSQL 數據庫的主要特點,通過弱化一致性,提高系統的可伸縮性、可靠性和可用性。而且對於大多數 Web 應用,其實並不需要強一致性,因此犧牲一致性而換取高可用性,是多數分佈式數據庫產品的方向。

2、分佈式事物的解決方案

(1)XA協議的兩段提交

  XA是由X/Open組織提出的分佈式事物的規範,由一個事物管理器(TM)和多個資源管理器(RM)組成,提交分爲兩個階段:prepare和commit
1)prepare第一階段,準備階段
在這裏插入圖片描述
在第一階段中,事物管理器(TM)會告訴資源管理器(RM)需要做什麼,當資源管理器(RM)完成之後,告訴事務管理器已經完成,此時每個資源管理器(RM)的事務並沒有提交。
2)commit第二階段,提交階段
在這裏插入圖片描述
所有的資源管理器(RM)都告訴事物管理器(TM)任務已經完成併成功之後,事物管理器(TM)再次發出通知,讓所有的資源管理器(RM)提交事物,完成一次分佈式事物管理。如果有一個資源管理器(RM)告訴事物管理器(TM)執行失敗,事物管理器(TM)就會通知所有的資源管理器(RM)回滾。
如果commit階段出現問題,事務出現不一致,需要人工處理

兩階段提交的方式效率低下,性能與本地事務相差10倍多

MySql5.7及以上均支持XA協議,Mysql Connector/J 5.0以上支持XA協議,java系統中數據源一般採用Atomikos

1)Atomikos實現兩階段事物

Springboot官方文檔中對Atomikos集成:https://docs.spring.io/spring-boot/docs/2.2.4.RELEASE/reference/html/spring-boot-features.html#boot-features-jta

數據庫說明:
兩個數據庫
數據庫地址:192.168.85.200:
  庫名:xa_200
  表名:xa_200
  字段:id,name
數據庫地址:192.168.85.201
  庫名:xa_201
  表名:xa_201
  字段:id,name

  1. 添加maven
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

別忘了,mysql、mybatis的jar包也要引入

  1. 使用javabean的方式創數據源
    創建一個ConfigDB200類,我這裏放在com.example.xa.config包下
@Configuration
//dao(mapper類的位置)
@MapperScan(value="com.example.xa.dao200",sqlSessionFactoryRef="sqlSessionFactoryBean200")
public class ConfigDB200{
    /*
     * 配置數據源
     */
    @Bean("db200")
    public DataSource db200(){
        //這裏使用的是mysql的XA數據源,因爲Atomikos是XA協議的
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        xaDataSource.setUser("用戶名");
        xaDataSource.setPassword("密碼");
        xaDataSource.setUrl("jdbc:mysql://192.168.85.200:3306/xa_200");
        
        //使用Atomikos統一的管理數據源
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(xaDataSource);
        
        return atomikosDataSourceBean;
    }
    
    /*
     * 配置SqlSessionFactoryBean
     */
     @Bean("sqlSessionFactoryBean200")
     public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db200")DataSource dataSource)throws IOException{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        //配置mybatis的xml位置
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db200/*.xml"))
        
        return sqlSessionFactoryBean;
     }
     
     /*
     * 配置事物管理器,這個只需要配置一個就可以了
     */
     @Bean("xaTransaction")
     public JtaTransactionManager jtaTransactionManager(){
        UserTransaction userTransaction = new UserTransactionImp();
        UserTranSactionManager userTransactionManager = new UserTransactionManager();
        
        return new JtaTransactionManager(userTransaction,userTransactionManager);
     }
}

再創建另外一個數據庫的配置ConfigDB201類

@Configuration
//dao(mapper類的位置)
@MapperScan(value="com.example.xa.dao201",sqlSessionFactoryRef="sqlSessionFactoryBean201")
public class ConfigDB201{
    /*
     * 配置數據源
     */
    @Bean("db201")
    public DataSource db201(){
        //這裏使用的是mysql的XA數據源,因爲Atomikos是XA協議的
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        xaDataSource.setUser("用戶名");
        xaDataSource.setPassword("密碼");
        xaDataSource.setUrl("jdbc:mysql://192.168.85.201:3306/xa_201");
        
        //使用Atomikos統一的管理數據源
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(xaDataSource);
        
        return atomikosDataSourceBean;
    }
    
    /*
     * 配置SqlSessionFactoryBean
     */
     @Bean("sqlSessionFactoryBean201")
     public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db201")DataSource dataSource)throws IOException{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        //配置mybatis的xml位置
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db201/*.xml"))
        
        return sqlSessionFactoryBean;
     }
}

測試配置是否成功,mybatis的內容就不寫了,不然太多了 看上去反而容易亂,如果mybatis都不會!速度補習

@Service
public class XAService{
    //重點就在這裏  設置事物管理器爲xa的事務管理器
    @Transactional(transactionManager = "xaTransaction")
    public void testXA(){
        XA200 xa200 = new XA200();
        xa200.setId(1);
        xa200.setName("xa_200");
        xa200Mapper.insert(xa200);
        
        XA201 xa201 = new XA201();
        xa201.setId(1);
        xa201.setName("xa_201");
        xa201Mapper.insert(xa201);
    }
}
2)MyCat分佈式事物

修改mycat的server.xml配置文件

vim server.xml

找到handleDistributedTransactions的配置項

<!--- 分佈式事務開關,0爲不過濾分佈式事物  1爲過濾分佈式事務(如果分佈式事務內只涉及全局表,則不過濾) 2爲不過濾分佈式事物,但是記錄分佈式事物日誌  -->
<property name="handleDistributedTransactions">0</property>

測試類

@Transactional(rollbackFor = Exception.class)
public void testUser(){
    //插入服務器200的數據庫
    User user1= new User();
    user1.setId();
    user1.setUsername("奇數");
    userMapper.insert(user1);
    
    //插入服務器201的數據庫
    //username字段長度只有2,所以這條數據會插入失敗,最後結果是兩個數據庫都會回滾
    User user2= new User();
    user2.setId();
    user2.setUsername("奇數11111");
    userMapper.insert(user2);
}
3)Sharding-JDBC分佈式事物

Sharding-JDBC如果正常的配置,默認就支持分佈式事物,直接使用就可以使用,和正常的單機事務使用方法一致加上@Transactional(rollbackFor = Exception.class)就能開啓事務

(2)事務補償機制(不推薦使用,對程序員壓力很大!!!)

事務補償即在事務鏈中的任何一個正向事務操作,都必須存在一個完全符合回滾規則的可逆事務。如果是一個完整的事務鏈,則必須事務鏈中的每一個業務服務或操作都有對應的可逆服務。當事務中一個階段發生異常,事務中的其他階段就需要調用對應的逆向操作進行補償。

  • 優點:邏輯清晰、流程簡單
  • 缺點:數據一致性比XA還要差,可能出錯的點比較多,TCC屬於應用層的一種補償方法,程序員需要寫大量代碼

數據庫說明:
兩個數據庫
數據庫地址:192.168.85.200:
  庫名:xa_200
  表名:accout_a
  字段:id,name,balance(餘額)
數據庫地址:192.168.85.201
  庫名:xa_201
  表名:accout_b
  字段:id,name,balance(餘額)

1)配置數據源

數據源200

@Configuration
//dao(mapper類的位置)
@MapperScan(value="com.example.xa.dao200",sqlSessionFactoryRef="sqlSessionFactoryBean200")
public class ConfigDB200{
    /*
     * 配置數據源
     */
    @Bean("db200")
    public DataSource db200(){
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("用戶名");
        dataSource.setPassword("密碼");
        dataSource.setUrl("jdbc:mysql://192.168.85.200:3306/xa_200");
        
        return dataSource;
    }
    
    /*
     * 配置SqlSessionFactoryBean
     */
     @Bean("sqlSessionFactoryBean200")
     public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db200")DataSource dataSource) throws IOException{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        //配置mybatis的xml位置
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db200/*.xml"))
        
        return sqlSessionFactoryBean;
     }
     
    /*
     * 配置事務管理器
     */
     @Bean("tm200")
     public PlatformTransactionManager transactionManager(@Qualifier("db200")DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
     }
}

數據源201

@Configuration
//dao(mapper類的位置)
@MapperScan(value="com.example.xa.dao201",sqlSessionFactoryRef="sqlSessionFactoryBean201")
public class ConfigDB201{
    /*
     * 配置數據源
     */
    @Bean("db201")
    public DataSource db201(){
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("用戶名");
        dataSource.setPassword("密碼");
        dataSource.setUrl("jdbc:mysql://192.168.85.200:3306/xa_201");
        
        return dataSource;
    }
    
    /*
     * 配置SqlSessionFactoryBean
     */
     @Bean("sqlSessionFactoryBean201")
     public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db201")DataSource dataSource) throws IOException{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        //配置mybatis的xml位置
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db201/*.xml"))
        
        return sqlSessionFactoryBean;
     }
     
     /*
     * 配置事務管理器
     */
     @Bean("tm201")
     public PlatformTransactionManager transactionManager(@Qualifier("db201")DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
     }
}
2)編制轉賬服務

將數據庫200的用戶的錢,轉給數據庫201的用戶賬戶上

    @Service
    public class AccountService{
        @Resource
        private AccountAmapper  accountAmapper;
        
        @Resource
        private AccountBmapper  accountBmapper;
        
        @Transactional(transactionManager="tm200" ,rollbackFor = Exception.class)
        public void transferAccount(){
            //查詢數據庫200中  id爲1的數據
            AccountA accountA=accountAMapper.selectByPrimaryKey(1);
            //將用戶accountA的餘額扣除200
            accountA.setBalance(accountA.getBalance.subtract(new BigDecimal(200)));
            accountAmapper.updateById(accountA);
            
           
             //查詢數據庫201中  id爲2的數據
            AccountB accountB=AccountBmapper.selectByPrimaryKey(2);
            //將用戶accountB的餘額加上200
            accountB.setBalance(accountB.getBalance.add(new BigDecimal(200)));
            accountBmapper.updateById(accountB);
             try{
               
                //模擬拋出異常,不能在更新accountB之前就檢測異常,不然餘額還沒有增加200元,這裏又扣了200元
                int i = 1/0
            }catch (Exception e){
                //這裏就是補償機制,如果這裏還是出現了異常,補償機制就會失敗!!!,需要人工接入
                //報錯的話,需要將accountB減去200元
                //查詢數據庫201中  id爲2的數據
                AccountB accountB=AccountBmapper.selectByPrimaryKey(2);
                //將用戶accountB的餘額加上200
                accountB.setBalance(accountB.getBalance.subtract(new BigDecimal(200)));
                accountBmapper.updateById(accountB);
                //異常還需要重新拋出,不然程序就正常結束了
                throw e;
            }
        }
    }

(3)本地消息表的最終一致性方案

  採用BASE原理,保證事物最終一致,在一致性方面,允許一段時間內的不一致,但最終會一致。基於本地消息表中的方案,將本事務外操作,記錄在消息表中,其他的事務提供接口,定時任務輪詢本地消息表,將未執行的消息發送給操作接口。例如:銀行從A賬戶轉賬給B賬戶200元,我們先將A賬戶減去200元,然後向本地事務表中添加一條記錄,記錄需要把B賬戶增加200元,定時任務會輪詢的掃描本地事務表,發現新增了一條記錄,根據要求調用接口將B賬戶增加200元,如果調用接口失敗,不會取消掉本地消息表的事務,下次輪詢的時候再次執行。

  • 優點:將分佈式事務,拆分成多個單獨的事務,實現最終一致性
  • 缺點:要注意重試時的冪等性操作

數據庫說明:
兩個數據庫
數據庫地址:192.168.85.200:
  庫名:xa_200
  表名:accout_a(用戶賬戶表)
  字段:id,name,balance(餘額)

  表名:payment_msg(本地消息表)
  字段:id,order_id,status(0:未發送,1:發送成功,2:超過最大失敗次數),falure_cnt(失敗次數)

數據庫地址:192.168.85.201
  庫名:xa_201
  表名:t_order(訂單表)
  字段:id,order_status,order_amount

這裏模擬 用戶已經下單了,但是還沒有付款的步驟,從用戶庫中扣除商品金額,然後再去訂單庫中,修改訂單狀態
所以我們在訂單表中預先就插入一條數據

insert into t_order values (1, 0,200);

還要預先插入一條用戶的數據

insert into accout_a values (1, "用戶A",1000);
1)配置數據源

參考上面的,完全一樣

2)編寫支付接口
@Service
public class PaymentService{
    @Resource
    private AccountAMapper accountAMapper;
    @Resource
    private PaymentMsgMapper paymentMsgMapper;
    
    /*
     * userId:用戶id
     * orderId:訂單id
     * amount:訂單金額
     */
     @Transactional(transactionManager = "tm200")
    public int pament(int userId,int orderId, BigDecimal amount){
        //支付操作
        AccountA accountA=accountAMapper.selectByPrimaryKey(userId);
        //用戶不存在
        if(accountA == null)return 1;
        //餘額不足
        if(accountA.getBalance().compareTo(amount)<0)return 2;
        accountA.setBalance(accountA.getBalance().subtract(amount));
        accountAMapper.updateByPrimaryKey(accountA);
       
       //記錄本地消息表
       PaymentMsg paymentMsg = new PaymentMsg();
       paymentMsg.setOrderId(orderId);
       paymentMsg.setStatus(0);//未發送
       paymentMsg.setFalureCnt(0);//失敗次數
       paymentMsgMapper.insertSelective(paymentMsg);
       
       //成功
       return 0;
    }
}

Controller層提供接口

@RestController
public class PaymentController{
    @Autowire
    private PaymentService paymentService;
    
    /*
     * userId:用戶id
     * orderId:訂單id
     * amount:訂單金額
     */
     @RequestMapping("/pament")
    public String pament(int userId,int orderId, BigDecimal amount){
       int result = paymentService.pament(userId,orderId,amount);
       return "支付結果:"+result;
    }
}
3)訂單操作接口
@Service
public class OrderService{
    @Resource
    private OrderMapper orderMapper;
    
    /*
     * orderId:訂單id
     */
    @Transactional(transactionManager = "tm201")
    public int handleOrder(int orderId){
        //查詢出訂單,之前預先插入的那條信息
        Order order=OrderMapper.selectByPrimaryKey(orderId);
        //訂單不存在
        if(order == null)return 1;
        //修改訂單支付狀態
        order.setOrderStatus(1); //已支付
        orderMapper.updateByPrimaryKey(order);
       
       //成功
       return 0;
    }
}

Controller層提供接口

@RestController
public class OrderController{
    @Autowire
    private OrderService orderService;
    
    /*
     * orderId:訂單id
     */
     @RequestMapping("/handleOrder")
    public String handleOrder(int orderId){
        try{
            int result = orderService.handleOrder(orderId);
            if(result == 0){
                return "success";
            }else{
                return "fail";
            }
        }catch(Exception e){
            //發生異常
            return "fail";
        }
       
    }
}
4)編寫定時任務,將支付接口和訂單接口起來
@Service
public class OrderScheduler(){
    
    //十秒執行一次
    @Scheduled(cron="0/10 * * * * ?")
    private void orderNotify()throws IOException{
        //查詢所有未發送的本地消息表,可以用其他方式實現,效果一樣就行了,查詢消息表裏面所有未發送的消息
        PaymentMsgExample paymentMsgExample = new PaymentMsgExample();
        paymentMsgExample.createCriteria().andStatusEqualTo(0);//未發送
        List<PaymentMsg> paymentMsgs = paymentMsgMapper.selectByExample(paymentMsgExample);
        
        for(PaymentMsg paymentMsg : paymentMsgs){
            int order = paymentMsg.getOrderId();
            
            //處理調用訂單接口的參數  這裏用的是httpClient,別忘記引入jar包
            CloseableHttpClient httpClient = HttpClientBuilder.create().build();
            HttpPost httpPost= new HttpPost("http://localhost:8080/handleOrder");
            NameValuePair orderIdPair = new BasicNameValuePair("orderId",order+"");
            List<NameValuePair> list = new ArrayList<>();
            list.add(orderIdPair);
            HttpEntity httpEntity = new UrlEncodeFormEntity(list);
            httpPost.setEntity(httpEntity);
            //調用訂單接口,並且得到返回值
            CloseableHttpResponse execute = httpClient.execute(httpPost);
            String s= EntityUtils.toString(response.getEntity());
            
            if("success".equals(s)){
                paymentMsg.setStatus(1);//發送成功
            }else{
                Integer falureCnt = paymentMsg.getFalureCnt();
                falureCnt++;
                //重試超過5次,就失敗
                if(falureCnt>5){
                    paymentMsg.setStatus(2);//失敗
                }
            }
            paymentMsgMapper.updateByPrimaryKey(paymentMsg);
            
        }
    }

}
5)進行測試

執行一下支付的接口

localhost:8080/payment?userId=1&orderId=10010&amount=200

(4)MQ的最終一致性方案

  原理、流程與本地消息表類似,主要的不同點是將本地消息表改爲MQ、定時任務改爲MQ的消費者
依舊採用之前的示例做演示,依舊採用消息表的庫,同意也要準備數據

1)安裝rocketmq,其他mq也一樣

這裏在windows下安裝,需要安裝jdk1.8及以上版本
下載地址:http://rocketmq.apache.org/dowloading/releases/

  1. 選擇Binary的包下載,解壓壓縮包
  2. 設置環境變量,新建環境變量ROCKETMQ_HOME,指向rocketmq的解壓目錄(D:\rocketmq-all-4.5.2-bin-release)
  3. 啓動nameserver,在rocketmq的bin目錄下執行mqnamesrv.cmd
  4. 啓動broker(隊列),在rocketmq的bin目錄下執行mqbroker.cmd -n localhost:9876
2)配置生產者和消費者
  1. 引入maven
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.5.2</version >
</dependency>
  1. 配置mq
    使用spring統一管理mq的生命週期,這裏消費者和生產者配置在一起,真實的工作中,多數情況是分開的
@Configuration
public class RocketMQConfig{

    @Bean(initMethod = "start",destroyMethod = "shutdown")
    public DefaultMQProducer producer(){
        DefaultMQProducer producer = new DefaultMQProducer("paymentGroup");
        producer.setNamesrvAddr("localhost:9876");
        
        return producer;
    }
    
    @Bean(initMethod = "start",destroyMethod = "shutdown")
    public DefaultMQPushConsumer consumer(@Qualifier("messageListener")MessageListenerConcurrently messageListener) throws MQClientException{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("paymentConsumerGroup");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("payment","*");
        consumer.registerMessageListener(messageListener);
        return consumer;
    }
}
3)編寫代碼
  1. 支付接口
@Service
public class PaymentService{
    @Resource
    private AccountAMapper accountAMapper;
    @Autowire
    private DefaultMQProducer producer;
    
    /*
     * userId:用戶id
     * orderId:訂單id
     * amount:訂單金額
     */
    @Transactional(transactionManager = "tm200",rollbackFor = Exception.class)
    public int pament(int userId,int orderId, BigDecimal amount) throws Exception{
        //支付操作
        AccountA accountA=accountAMapper.selectByPrimaryKey(userId);
        //用戶不存在
        if(accountA == null)return 1;
        //餘額不足
        if(accountA.getBalance().compareTo(amount)<0)return 2;
        accountA.setBalance(accountA.getBalance().subtract(amount));
        accountAMapper.updateByPrimaryKey(accountA);
       
        //放入消息隊列
        Message message = new Message();
        message.setTopic("payment");
        message.setKeys(orderId+"");
        message.setBody("訂單已支付".getBytes());
        producer.send(message);
       
        try{
            SendResult result = producer.send(message);
            if(result.getSendStatus() == SendStatus.SEND_OK){
                //成功
                return 0;
            }else{
                //消息發送失敗,拋出異常讓事務回滾
                throw new Exception("消息發送失敗!");
            }
        }cath(Exception e){
            e.printStackTrace();
            throw e;
        }
       
    }
}

Controller層提供接口

@RestController
public class PaymentController{
    @Autowire
    private PaymentService paymentService;
    
    /*
     * userId:用戶id
     * orderId:訂單id
     * amount:訂單金額
     */
     @RequestMapping("/pament")
    public String pament(int userId,int orderId, BigDecimal amount){
       int result = paymentService.pament(userId,orderId,amount);
       return "支付結果:"+result;
    }
}
  1. 實現mq消費者
@Component
public class ChangeOrderStatus implements MessageListenerConcurrently{
    @Autowired
    private OrderMapper orderMapper;
    
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt list,
            ConsumeConcurrentlyContext consumeConcurrentlyContext>){
        if(list == null || list.size ==) {
            //消費成功
            return CONSUME_SUCCESS;
        }
        //實際上 如果不修改默認配置,這個list只會有一個消息
        for(MessagesExt messageExt: list){
            String orderId = messageExt.getKeys();
            String msg =new String(messageExt.getBody()); 
            System.out.println("msg="+msg);
            Order order = orderMapper.selectByPrimaryKey(Integer.parseInt(orderId));
            //訂單不存在,再次消費
            if(order == null)return RECONSUME_LATER;
            try{
                 //修改訂單支付狀態
                order.setOrderStatus(1); //已支付
                orderMapper.updateByPrimaryKey(order);
            }catch(Exception e){
                e.printStackTrace();
                return RECONSUME_LATER;
            }
        }
        return CONSUME_SUCCESS;
    }
}
4)進行測試

執行一下支付的接口

localhost:8080/payment?userId=1&orderId=10010&amount=200
···
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章