分布式篇:初探分布式事务 · 从小白到入门

35 阅读7分钟

什么是事务?

事务就是数据库执行中的一系列操作,这些操作要么都做,要么一个都不做;不同事务之间要互不影响;事务完成后要永久生效;数据库只能从一个状态转到另一个状态,没有中间态,以此来保证程序状态的前后一致性。所以说,事务具有原子性,隔离性,持久性,一致性。

什么是分布式事务?

由于系统规模的不断扩大,出现了分布式系统,甚至出现了数据库分库。这时候就出现了分布式事务问题。这是因为在分布式环境中,涉及的数据可能存储在不同的物理位置或不同的数据库实例中,这就导致了传统事务管理无法直接适用。

例如,在一个电商系统中,订单创建(Order Service)和服务支付(Payment Service)可能是由两个独立的服务负责,并且它们各自拥有自己的数据库。如果用户下单并支付这一过程需要作为一个整体来看待,那么这两个操作就必须作为同一个事务处理,否则就可能出现订单创建成功但支付失败的情况

本质上来说,分布式事务就是为了保证不同数据库的数据一致性

目前的分布式事务方案

由于分布式事务涉及到分布式系统,分布式事务往往比较复杂。以下是分布式事务的常见模型。

两阶段提交

两阶段提交协议(Two-Phase Commit, 2PC) 是一种经典的分布式事务协议,旨在保证多个服务或数据库之间的事务一致性。

在 2PC 中有两个核心角色:

  1. 协调者(Coordinator):负责发起事务并决定最终是提交还是回滚。又称事务管理器
  2. 参与者(Participants):实际执行事务操作的节点,比如数据库或服务。

整个过程分为两个阶段:

  1. 准备阶段(Prepare Phase)

    步骤操作描述
    1协调者向所有参与者发送 prepare 请求。
    2每个参与者收到请求后,会尝试执行本地事务操作,但不会真正提交。
    3参与者将事务状态写入日志,并回复协调者结果:
    - Yes 表示准备好提交;
    - No 表示无法提交。
  2. 提交阶段(Commit Phase)

    根据第一阶段的响应,协调者做出如下两种决策:

    所有参与者都返回 Yes ✅

    步骤操作描述
    1协调者发送 commit 命令给所有参与者。
    2参与者正式提交本地事务,并释放资源。
    3各参与者完成提交后,向协调者确认提交成功。

    任一参与者返回 No ❌

    步骤操作描述
    1协调者发送 rollback 命令给所有参与者。
    2参与者撤销之前所做的操作(通过日志回滚)。
    3各参与者完成回滚后,向协调者确认回滚成功。

优缺点

优点缺点
实现简单,逻辑清晰单点故障风险大(协调者挂掉则整个事务卡住)
能够保证强一致性存在同步阻塞,如某个数据库系统本地事务执行较慢,连累其他系统
适用于小规模集群不支持部分提交,要么全提交,要么全回滚

该方式属于CAP下的CP模式。由于同步阻塞问题无法实现完全的可用性。

三阶段提交

三阶段提交(3PC)是对两阶段提交(2PC)的一种改进,旨在解决2PC在协调者故障或网络延迟时可能导致的阻塞问题。它通过将原来的“准备阶段”进一步拆分为两个阶段,从而减少系统因等待响应而卡住的可能性。

整个过程分为三个阶段:

  1. CanCommit

    协调者向所有参与者发送 canCommit 请求,询问它们是否可以执行事务。参与者收到请求后,检查自身状态,判断是否具备执行能力,并回复 Yes 或 No。通过该阶段,可以快速探测参与者是否准备好,避免不必要的资源锁定。

  2. PreCommit

    类似于二阶段的准备阶段

  3. DoCommit

    类似于二阶段的提交阶段

由于三阶段支持超时,所以相比于二阶段,可用性更好些。

TCC模式

见后文

Saga 模式

见后文

Seata实现的分布式事务

Seata 是一个基于著名的分布式事务的框架,支持多种事务模式:

  • AT 模式(Auto Transaction):自动代理数据库事务,开发者无感知,对数据库无特殊要求。(2PC)
  • XA 模式:基于 XA 协议的强一致性事务,要求数据库支持 XA,开发者无感知。(2PC)
  • TCC 模式(Try-Confirm-Cancel):通过业务逻辑实现补偿机制。
  • Saga 模式:长周期事务处理模式。

其中AT与XA模式使用简单,TCC与Saga较为复杂

使用

Java

AT

得益于Java的注解,使用AT模式,只需要使用@GlobalTransaction,即可使用分布式事务,开发者无感知,使用 AT 模式可以最小程度减少业务改造成本。例如:

import io.seata.spring.annotation.GlobalTransactional; 
import org.springframework.stereotype.Service; 
@Service 
public class OrderService { 
    @GlobalTransactional 
    public void createOrder() { 
        // 调用库存服务扣减库存 
        inventoryService.reduceStock(); 
        // 调用支付服务扣款 
        paymentService.deductMoney(); 
    } 
}
XA

XA 同样作为开发者无感知的模式。同样只需要使用@GlobalTransaction,即可使用分布式事务。XA 模式使用起来与 AT 模式基本一致,用法上的唯一区别在于数据源代理的替换:使用 DataSourceProxyXA 来替代 DataSourceProxy

public class DataSourceProxy {
    @Bean("dataSourceProxy")
    public DataSource dataSource(DruidDataSource druidDataSource) {
        // DataSourceProxyXA  XA 
        return new DataSourceProxyXA(druidDataSource);
        // DataSourceProxy  AT 
        // return new DataSourceProxy(druidDataSource);
    }
}
TCC 与 Saga

见后文

GO

AT

GO不同于Java,没有注解。其使用依赖于seata-go的api 首先创建数据源

// sql2.SeataATMySQLDriver
sqlDB, err := sql.Open(sql2.SeataATMySQLDriver, "root:root@tcp(127.0.0.1:3306)/seata_go?multiStatements=true&interpolateParams=true")
gormDB, err = gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{})

使用时,将执行事务的方法作为参数传递


import (
	"context"
	"database/sql"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"seata.apache.org/seata-go/pkg/client"
	sql2 "seata.apache.org/seata-go/pkg/datasource/sql"
	"seata.apache.org/seata-go/pkg/tm"
)
func main() {
	client.InitPath("./conf/seatago.yml")
	// insert
	tm.WithGlobalTx(context.Background(), &tm.GtxConfig{
		Name:    "ATGlobalTx",
		Timeout: time.Second * 30,
	}, insertData)
}
        
func insertData(ctx context.Context) error {
	data := OrderTblModel{
		Id:            1,
		UserId:        "NO-100003",
		CommodityCode: "C100001",
		Count:         101,
		Money:         11,
		Descs:         "insert desc",
	}

	return gormDB.WithContext(ctx).Table("order_tbl").Create(&data).Error
}
XA

XA模式的使用类似于AT,唯一的区别为创建数据源的方式

// sql2.SeataXAMySQLDriver
sqlDB, err := sql.Open(sql2.SeataXAMySQLDriver, "root:root@tcp(127.0.0.1:3306)/seata_go?multiStatements=true&interpolateParams=true")
gormDB, err = gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{})
TCC 与 Saga

见后文

附录

分布式理论

一致性

强一致性

无论何时,数据一直保持一致,每个节点中的数据始终是一致的

弱一致性

简而言之,就是允许节点之间出现一定程度的数据不一致

最终一致性

不要求数据始终保持一致,允许一段时间的数据不一致。但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。最终,节点间的数据会达到一致状态

CAP理论

与分布式系统相关的理论中,包括CAP理论

C:一致性,分布式系统中各节点数据保持一致
A:可用性,分布式系统中部分节点故障,其他节点依然可以维持系统运行
P:分区容错性,不同节点之间得通信可能存在延迟或断联。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,分布式系统必须可以容忍这种现象发生并作出处理。

CAP理论中,三者是不可得兼的,只能得其二。需要在三种特性中做出权衡:

组合特点
CA一致 + 可用,不考虑分区容错(单机)
CP一致 + 容错,牺牲可用性
AP可用 + 容错,牺牲一致性

BASE理论

除了CAP理论,还有BASE理论,该理论是对CAPAP组合的延伸。该理论核心思想是即便无法做到强一致性,但应该采用适合的方式保证最终一致性。

BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用 \

S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性 \

E:Eventual Consistency 最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态