基於XA規範的兩階段提交方式
事務在業務的開發中有着至關重要的作用,事務具有的ACID的特性能保證業務處理前後數據的一致性:
原子性(Atomicity): 事務執行的所有操作,要麼全部執行,要麼全部不執行;
一致性(Consistency): 事務的執行前後,數據的完整性保持一致;
隔離性(Isolation): 兩個或多個事務並行執行時是互不干擾的;
持久性(Durability): 事務執行完成後,其對數據庫數據的更改會被永久保存下來;
在單機環境下,數據庫系統對事務的支持是比較完善的;但當對數據進行水平或垂直拆分,一個數據庫節點變爲多個數據庫節點時,分佈式事務就出現了。
XA規範
XA是X/Open組織提出的一個分佈式事務的規範,其定義了一個分佈式事務的處理模型——DTP。在DTP中定義了三個組件:
Application Program(AP):應用程序,即業務層,它定義了事務的邊界,以及構成該事務的特定操作;
Resource Manager(RM):資源管理器,可以理解爲一個DBMS系統,或者消息服務器管理系統;
Transaction Manager(TM):事務管理器,負責協調和管理事務;
AP與RM之間,AP通過RM提供的API進行交互,當需要進行分佈式事務時,則向TM發起一個全局事務,TM與RM之間則通過XA接口進行交互,TM管理了到RM的鏈接,並實現了兩階段提交。
兩階段提交流程(2PC)
XA規範中,多個RM狀態之間的協調通過TM進行,而這個資源協調的過程採用了兩階段提交協議,在兩階段提交中,分爲準備階段和提交階段:
第一階段:
第二階段:
如果在準備階段,有一個RM返回失敗時,則在第二個階段將回滾所有資源
第一階段:
第二階段:
2PC的侷限性
雖然基於 XA 的二階段提交算法基本滿足了事務的 ACID 特性,但其不中之處也是明顯的:
- 在事務的執行過程中,所有的參與節點都是阻塞型的,在併發量高的系統中,性能受限嚴重;
- 如果TM在commit前發生故障,那麼所有參與節點會因爲無法提交事務而處於長時間鎖定資源的狀態;
- 在實際情況中,由於分佈式環境下的複雜性,TM在發送commit請求後,可能因爲局部網絡原因,導致只有部分參與者收到commit請求時,系統便出現了數據不一致的現象;
- XA協議要求所有參與者需要與TM進行直接交互,但在微服務架構下,一個服務與多個RM直接關聯常常是被不允許的;
Atomikos在Spring Boot的使用
Atomikos在XA中作爲一個事務管理器(TM)存在。在Spring Boot應用中,可以通過Atomikos在應用中方便的引入分佈式事務。
下面以一個簡單的訂單創建流程的爲例:
引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
配置數據源
application.yml
spring:
datasource:
druid:
order-db:
name: order-db
url: jdbc:mysql://localhost:3307/order?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: mysql
product-db:
name: order-db
url: jdbc:mysql://localhost:3306/product?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: mysql
jta:
transaction-manager-id: order-product-tx-manager
@Configuration
@MapperScan(basePackages = "gdou.laixiaoming.atomikos.demo.mapper.order", sqlSessionFactoryRef = "orderSqlSessionFactory")
public class OrderDataSourceConfig {
@Bean(name = "druidOrderDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid.order-db")
public DruidXADataSource druidOrderDataSource(){
DruidXADataSource xaDataSource = new DruidXADataSource();
return xaDataSource;
}
@Bean(name = "orderDataSource")
public DataSource orderDataSource(
@Qualifier("druidOrderDataSource") DruidXADataSource druidOrderDataSource) {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setXaDataSource(druidOrderDataSource);
ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
ds.setUniqueResourceName("orderDataSource");
return ds;
}
@Bean
public SqlSessionFactory orderSqlSessionFactory(
@Qualifier("orderDataSource") DataSource orderDataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(orderDataSource);
return sqlSessionFactoryBean.getObject();
}
}
@Configuration
@MapperScan(basePackages = "gdou.laixiaoming.atomikos.demo.mapper.product", sqlSessionFactoryRef = "productSqlSessionFactory")
public class ProductDataSourceConfig {
@Bean(name = "druidProductDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid.product-db")
public DruidXADataSource druidProductDataSource(){
DruidXADataSource xaDataSource = new DruidXADataSource();
return xaDataSource;
}
@Bean(name = "productDataSource")
public DataSource productDataSource(
@Qualifier("druidProductDataSource") DruidXADataSource druidProductDataSource) {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setXaDataSource(druidProductDataSource);
ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
ds.setUniqueResourceName("productDataSource");
return ds;
}
@Bean
public SqlSessionFactory productSqlSessionFactory(
@Qualifier("productDataSource") DataSource productDataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(productDataSource);
return sqlSessionFactoryBean.getObject();
}
}
構建商品服務
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
@Override
public void updateInventory(Long productId) {
//模擬異常流程
if(productId == 2){
throw new RuntimeException("更新庫存失敗");
}
productMapper.updateInventory(productId);
}
}
構建訂單服務
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductService productService;
@Transactional(rollbackFor = RuntimeException.class)
@Override
public void order(Long productId) {
orderMapper.addOrder(productId);
productService.updateInventory(productId);
}
}
測試
@SpringBootTest
@RunWith(SpringRunner.class)
public class ServiceTest {
@Autowired
private OrderService orderService;
@Test
public void test() {
orderService.order(1L);
}
@Test
public void test2() {
orderService.order(1L);
}
}
通過運行測試用例,我們可以發現test()方法在訂單庫以及商品庫的成功完成的修改;而test2方法則因爲商品服務異常進行了回滾,回滾後的訂單庫和商品庫數據都恢復到了事務開啓前的狀態。
參考:
《大型網站系統與Java中間件實踐》
SpringBoot Atomikos 多數據源分佈式事務