XA兩階段提交協議 - 手寫XA, Atomikos 與 Spring boot 集成

因爲事務需要實現ACID,即原子性、一致性、隔離性、持久性,所以需要採用一定的機制來保證,通常採用的是分階段提交的方式。
XA:XA協議,規定事務管理器和資源管理器接口,採用二階段提交協議。

一階段提交協議

一階段提交協議相對簡單。當然,前提是開啓了事務,然後在應用程序發出提交/回滾請求後,數據庫執行操作,而後將成功/失敗返回給應用程序,程序繼續執行。
一階段提交協議相對簡單,簡單帶來的優點就是,它不用再與其他的對象交互,節省了判斷步驟和時間,所以在性能上是在階段提交協議中對好的。

二階段提交協議

一階段提交協議有其優點,但缺點也很明顯:

數據庫確認執行事務的時間較長,出問題的可能性就隨之增大。
如果有多個數據源,一階段提交協議無法協調他們之間的關係。
所以在一階段協議的基礎上,有了二階段協議,二階段協議的好處是添加了一個管理者角色,如下:
在這裏插入圖片描述
很明顯,二階段協議通過將兩層變爲三層,增加了中間的管理者角色,從而協調多個數據源之間的關係,二階段提交協議分爲兩個階段。
爲什麼要分兩步執行?一是因爲分兩步,就有了事務管理器統一管理的機會;二儘可能晚地提交事務,讓事務在提交前儘可能地完成所有能完成的工作,這樣,最後的提交階段將是耗時極短,耗時極短意味着操作失敗的可能性也就降低。
同時,二階段提交協議爲了保證事務的一致性,不管是事務管理器還是各個資源管理器,每執行一步操作,都會記錄日誌,爲出現故障後的恢復準備依據。
二階段提交協議的存在的弊端是阻塞,因爲事務管理器要收集各個資源管理器的響應消息,如果其中一個或多個一直不返回消息,則事務管理器一直等待,應用程序也被阻塞,甚至可能永久阻塞。

手寫XA,理解其原理

事務管理器

public class TM {

    public  void execute(Connection accountConn,Connection redConn) throws SQLException {
        //打印XA的事務日誌 true 代表打印
        boolean logXaCommands = true;
        //獲取RM1 的接口實例
        XAConnection xaConn1 = new MysqlXAConnection((com.mysql.jdbc.ConnectionImpl) accountConn, logXaCommands);
        XAResource rm1 = xaConn1.getXAResource();

        //獲取RM2 的接口實例
        XAConnection xaConn2 = new MysqlXAConnection((com.mysql.jdbc.ConnectionImpl) redConn, logXaCommands);
        XAResource rm2 = xaConn2.getXAResource();


        //生成一個全局事務ID
        byte[] globalid = "agan12345".getBytes();
        int formatId = 1;
        try {
            //TM 把rm1的事務分支id,註冊到全局事務ID
            byte[] bqual1 = "b00001".getBytes();
            Xid xid1 = new MysqlXid(globalid, bqual1, formatId);
            //start...end 開始 結束 rm1的本地事務
            rm1.start(xid1, XAResource.TMNOFLAGS);
            //模擬購物買一個物品,用餘額支付90㡰
            String sql="update capital_account set balance_amount=balance_amount-90 where user_id=1";
            PreparedStatement ps1 = accountConn.prepareStatement(sql);
            ps1.execute();
            rm1.end(xid1, XAResource.TMSUCCESS);


            //TM 把rm2的事務分支id,註冊到全局事務ID
            byte[] bqual2 = "b00002".getBytes();
            Xid xid2 = new MysqlXid(globalid, bqual2, formatId);
            //start...end 開始 結束 rm2的本地事務
            rm2.start(xid2, XAResource.TMNOFLAGS);
            //模擬購物一個物品,用紅包支付10元。
            sql="update red_packet_account set balance_amount=balance_amount-10 where user_id=1";
            PreparedStatement ps2 = redConn.prepareStatement(sql);
            ps2.execute();
            rm2.end(xid2, XAResource.TMSUCCESS);

            //2階段提交中得第一個階段:準備提交
            int rm1_prepare = rm1.prepare(xid1);
            int rm2_prepare = rm2.prepare(xid2);

            //2階段提交中得第二個階段:真正提交
            if (rm1_prepare == XAResource.XA_OK && rm2_prepare == XAResource.XA_OK) {

                boolean onePhase = false;
                rm1.commit(xid1, onePhase);//提交事務
                rm2.commit(xid2, onePhase);//提交事務
            } else {//全部回滾
                rm1.rollback(xid1);
                rm1.rollback(xid2);
            }
        } catch (XAException e) {
            // 如果出現異常,也要進行回滾
            e.printStackTrace();
        }
    }
}

兩個不同的數據源,紅包賬戶和餘額賬戶扣錢

public class AP {

    public Connection getRmAccountConn(){

        try {
            Connection conn = DriverManager.getConnection("jdbc:mysql://192.168.0.138:3307/xa_account?characterEncoding=utf8&useSSL=false&autoReconnect=true", "root", "agan");
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Connection getRmRedConn(){
        try {
            Connection conn = DriverManager.getConnection("jdbc:mysql://192.168.0.138:3308/xa_red_account?characterEncoding=utf8&useSSL=false&autoReconnect=true", "root", "agan");
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
}

測試代碼:

public class XaTest {

    AP ap=new AP();
    TM tm=new TM();


    @Test
    public void test(){
        try {
            tm.execute(ap.getRmAccountConn(),ap.getRmRedConn());
        } catch (SQLException e) {
            e.printStackTrace();
        }

    }
}

使用Atomikos

第一步:導入核心的依賴包
、、、 org.springframework.boot spring-boot-starter-jta-atomikos

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>${druid.version}</version>
</dependency>

、、、

第二步: 改配置多數據域
配置atomikos 事務管理器,並配置druid作爲數據源並且進行監控 application.properties 文件中配置druid的2個數據源。

##account表數據庫配置
spring.atomikos.datasource.account.max-pool-size=25
spring.atomikos.datasource.account.min-pool-size=3
spring.atomikos.datasource.account.max-lifetime=20000
spring.atomikos.datasource.account.borrow-connection-timeout=10000
spring.atomikos.datasource.account.unique-resource-name=account
spring.atomikos.datasource.account.xa-properties.url=jdbc:mysql://192.168.0.138:3307/xa_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
spring.atomikos.datasource.account.xa-properties.username=root
spring.atomikos.datasource.account.xa-properties.password=agan
spring.atomikos.datasource.account.xa-properties.driverClassName=com.mysql.jdbc.Driver
# 初始化大小,最小,最大
spring.atomikos.datasource.account.xa-properties.initialSize=10
spring.atomikos.datasource.account.xa-properties.minIdle=20
spring.atomikos.datasource.account.xa-properties.maxActive=100
## 配置獲取連接等待超時的時間
spring.atomikos.datasource.account.xa-properties.maxWait=60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
spring.atomikos.datasource.account.xa-properties.timeBetweenEvictionRunsMillis=60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
spring.atomikos.datasource.account.xa-properties.minEvictableIdleTimeMillis=300000
spring.atomikos.datasource.account.xa-properties.testWhileIdle=true
spring.atomikos.datasource.account.xa-properties.testOnBorrow=false
spring.atomikos.datasource.account.xa-properties.testOnReturn=false
# 打開PSCache,並且指定每個連接上PSCache的大小
spring.atomikos.datasource.account.xa-properties.poolPreparedStatements=true
spring.atomikos.datasource.account.xa-properties.maxPoolPreparedStatementPerConnectionSize=20
# 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆
spring.atomikos.datasource.account.xa-properties.filters=stat,slf4j,wall
spring.atomikos.datasource.account.xa-data-source-class-name=com.alibaba.druid.pool.xa.DruidXADataSource

#------------------------------ 分隔符-------------------------------------
##redpacket表數據庫配置
spring.atomikos.datasource.redpacket.max-pool-size=25
spring.atomikos.datasource.redpacket.min-pool-size=3
spring.atomikos.datasource.redpacket.max-lifetime=20000
spring.atomikos.datasource.redpacket.borrow-connection-timeout=10000
spring.atomikos.datasource.redpacket.unique-resource-name=redpacket
spring.atomikos.datasource.redpacket.xa-properties.url=jdbc:mysql://192.168.0.138:3308/xa_red_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
spring.atomikos.datasource.redpacket.xa-properties.username=root
spring.atomikos.datasource.redpacket.xa-properties.password=agan
spring.atomikos.datasource.redpacket.xa-properties.driverClassName=com.mysql.jdbc.Driver
spring.atomikos.datasource.redpacket.xa-properties.initialSize=10
spring.atomikos.datasource.redpacket.xa-properties.minIdle=20
spring.atomikos.datasource.redpacket.xa-properties.maxActive=100
spring.atomikos.datasource.redpacket.xa-properties.maxWait=60000
spring.atomikos.datasource.redpacket.xa-properties.timeBetweenEvictionRunsMillis=60000
spring.atomikos.datasource.redpacket.xa-properties.minEvictableIdleTimeMillis=300000
spring.atomikos.datasource.redpacket.xa-properties.testWhileIdle=true
spring.atomikos.datasource.redpacket.xa-properties.testOnBorrow=false
spring.atomikos.datasource.redpacket.xa-properties.testOnReturn=false
spring.atomikos.datasource.redpacket.xa-properties.poolPreparedStatements=true
spring.atomikos.datasource.redpacket.xa-properties.maxPoolPreparedStatementPerConnectionSize=20
# 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆
spring.atomikos.datasource.redpacket.xa-properties.filters=stat,slf4j,wall
spring.atomikos.datasource.redpacket.xa-data-source-class-name=com.alibaba.druid.pool.xa.DruidXADataSource


#jta相關參數配置
#spring.jta.transaction-manager-id=txManager
#spring.jta.log-dir=transaction-logs-agan


logging.level.root=INFO

第三步:將配置的數據庫連接信息,注入數據源,並且設置druid的監控中心。
MybatisConfiguration 的目的是配置DataSource

@Configuration
@EnableConfigurationProperties
@EnableTransactionManagement(proxyTargetClass = true)
public class MybatisConfiguration {
    /**
     * account數據庫配置前綴.
     */
    final static String ACCOUNT_PREFIX = "spring.atomikos.datasource.account";
    /**
     * redpacket數據庫配置前綴.
     */
    final static String REDPACKET_PREFIX = "spring.atomikos.datasource.redpacket";

    /**
     * The constant logger.
     */
    final static Logger logger = LoggerFactory.getLogger(MybatisConfiguration.class);

    /**
     * 配置druid顯示監控統計信息
     * 開啓Druid的監控平臺 http://localhost:8080/druid
     *
     * @return servlet registration bean
     */
    @Bean
    public ServletRegistrationBean druidServlet() {
        logger.info("Init Druid Servlet Configuration ");
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
        // IP白名單,不設默認都可以
//        servletRegistrationBean.addInitParameter("allow", "192.168.2.25,127.0.0.1");
        // IP黑名單(共同存在時,deny優先於allow)
        servletRegistrationBean.addInitParameter("deny", "192.168.1.100");
        //控制檯管理用戶
        servletRegistrationBean.addInitParameter("loginUsername", "root");
        servletRegistrationBean.addInitParameter("loginPassword", "agan");
        //是否能夠重置數據 禁用HTML頁面上的“Reset All”功能
        servletRegistrationBean.addInitParameter("resetEnable", "false");
        return servletRegistrationBean;
    }

    /**
     * 註冊一個filterRegistrationBean
     *
     * @return filter registration bean
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
        //添加過濾規則
        filterRegistrationBean.addUrlPatterns("/*");
        //添加不需要忽略的格式信息
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }

    /**
     * 配置Account數據庫的數據源
     *
     * @return the data source
     */
    @Bean(name = "AccountDataSource")
    @ConfigurationProperties(prefix = ACCOUNT_PREFIX)  // application.properties中對應屬性的前綴
    public DataSource accountDataSource() {
        return new AtomikosDataSourceBean();
    }

    /**
     * 配置RedPacket數據庫的數據源
     *
     * @return the data source
     */
    @Bean(name = "RedPacketDataSource")
    @ConfigurationProperties(prefix = REDPACKET_PREFIX)  // application.properties中對應屬性的前綴
    public DataSource redPacketDataSource() {
        return new AtomikosDataSourceBean();
    }
}

AccountDataSourceConfiguration 作用:配置account的數據源的sessionfactory ,同時關聯mybaits RedAccountDataSourceConfiguration 作用:配置RedAccount的數據源的sessionfactory ,同時關聯mybaits

@Configuration
@MapperScan(basePackages = {"com.agan.dtp.atomikos.mapper.account.mapper"}, sqlSessionFactoryRef = "accountSqlSessionFactory")
public class AccountDataSourceConfiguration {
    /**
     * The constant MAPPER_XML_LOCATION.
     */
    public static final String MAPPER_XML_LOCATION = "classpath*:com/agan/dtp/atomikos/mapper/account/mapper/*.xml";

    /**
     * The Open plat form data source.
     */
    @Autowired
    @Qualifier("AccountDataSource")
    DataSource accountDataSource;

    /**
     * 配置Sql Session模板
     *
     * @return the sql session template
     * @throws Exception the exception
     */
    @Bean
    public SqlSessionTemplate springSqlSessionTemplate() throws Exception {
        return new SqlSessionTemplate(accountSqlSessionFactory());
    }

    /**
     * 配置SQL Session工廠
     *
     * @return the sql session factory
     * @throws Exception the exception
     */
    @Bean
    public SqlSessionFactory accountSqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(accountDataSource);
        //指定XML文件路徑
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_LOCATION));
        return factoryBean.getObject();
    }
}

第四步:service體驗 atomikos
PayServiceImpl 作用:模擬下訂單的同時扣除,賬戶餘額,紅包餘額的錢。

@Service
public class PayServiceImpl implements PayService {

    @Autowired
    private CapitalAccountMapper capitalAccountMapper;
    @Autowired
    private RedPacketAccountMapper redPacketAccountMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void pay(int userId,int account, int redAccount) {
        CapitalAccount ca=new CapitalAccount();
        ca.setUserId(userId);
        CapitalAccount capitalDTO=this.capitalAccountMapper.selectOne(ca);
        System.out.println(capitalDTO);
        //賬戶餘額扣除
        capitalDTO.setBalanceAmount(capitalDTO.getBalanceAmount()-account);
        this.capitalAccountMapper.updateByPrimaryKey(capitalDTO);

        RedPacketAccount red= new RedPacketAccount();
        red.setUserId(userId);
        RedPacketAccount redDTO=this.redPacketAccountMapper.selectOne(red);
        System.out.println(redDTO);
        //紅包餘額扣除
        redDTO.setBalanceAmount(redDTO.getBalanceAmount()-redAccount);
        this.redPacketAccountMapper.updateByPrimaryKey(redDTO);
        //int n=9/0;
    }
}

第五步: junit 測試

@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomikosTests {

    @Autowired
    PayService payService;

    @Test
    //@Transactional
    public void contextLoads() {
        try {
           this.payService.pay(1,10,10);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

第六步: 查看Atomikos的日誌
1.如何測試atomikos的事務運行結果? 查看Atomikos的日誌,默認情況下,在項目的根目錄下會自動創建transaction-logs文件夾,每個Atomikos實例都會有一個全局ID,這個ID爲Atomikos運行機器的IP地址; 這個唯一ID會自動關聯多個數據庫的事務信息,也就是會關聯分支事務id.

2.atomikos日誌的自定義配置 spring.jta.log-dir=transaction-logs-agan

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章