CC 咖啡猫的工作空间 Coding Space

数据库事务深入原理

事务是数据库操作的基本单位,保证一组操作要么全成功、要么全失败。本文档从本地事务到分布式事务,详解事务的原理与实践。


一、本地事务

1.1 ACID 四大特性

特性 说明 实现原理
Atomic(原子性) 事务是最小执行单位,不可分割 Undo Log(回滚日志)
Consistency(一致性) 事务执行前后,数据库状态一致 其他三个特性保证
Isolation(隔离性) 并发执行时,事务之间互不影响 锁 + MVCC
Durability(持久性) 事务提交后,数据永久保存 Redo Log(重做日志)

1.2 事务隔离级别

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(未提交读)
READ COMMITTED(已提交读)
REPEATABLE READ(可重复读) ❌(InnoDB 通过 Next-Key Lock 解决)
SERIALIZABLE(串行化)

MySQL 默认隔离级别:REPEATABLE READ

1.3 MVCC 原理(多版本并发控制)

快照读 vs 当前读

快照读:读取历史版本(不加锁)
  SELECT * FROM users  -- 快照读

当前读:读取最新数据(加锁)
  SELECT * FROM users FOR UPDATE  -- 当前读
  INSERT / UPDATE / DELETE        -- 当前读

MVCC 三要素

隐藏列:
  - DB_TRX_ID:最近修改的事务ID
  - DB_ROLL_PTR:指向 Undo Log 的指针
  - DB_ROW_ID:行ID(无主键时生成)

ReadView(读取视图):
  - m_ids:活跃事务ID列表
  - min_trx_id:最小活跃事务ID
  - max_trx_id:创建 ReadView 时最大事务ID
  - creator_trx_id:当前事务ID

版本链:
  每行数据通过 DB_ROLL_PTR 串起 Undo Log 版本链
  新事务修改 → 生成新版本 → 指向前一个版本

MVCC 查询逻辑

事务A开启 → 生成 ReadView
    ↓
SELECT:读取数据时
    ↓
检查数据行的事务ID(DB_TRX_ID)
    ↓
DB_TRX_ID < min_trx_id?
    是 → 数据在事务A开启前已提交 → 可读取 ✅
    否 → 进入下一步
    ↓
DB_TRX_ID 在 m_ids 中?
    是 → 数据是并发事务修改 → 沿着版本链找 ✅
    否 → 数据是已提交事务修改 → 可读取 ✅

可见性判断

-- 事务A(隔离级别 RR)开启
BEGIN;
SELECT * FROM users WHERE id = 1;
-- 读取到的是事务A开启时的快照

-- 事务B在事务A开启后修改了 id=1 的数据
UPDATE users SET name = 'new' WHERE id = 1;
COMMIT;

-- 事务A再次查询
SELECT * FROM users WHERE id = 1;
-- 仍然读到旧值(因为 ReadView 未变)
-- 实现了可重复读

1.4 快照读与当前读

-- 快照读(MVCC,不加锁)
SELECT * FROM orders WHERE status = 'pending';

-- 当前读(加锁)
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;

-- 写入操作(自动当前读)
INSERT INTO orders ...
UPDATE orders ...
DELETE FROM orders ...

1.5 Next-Key Lock(临键锁)解决幻读

InnoDB 在 RR 级别下,使用 Next-Key Lock 锁定索引范围,防止幻读:

-- 假设 orders 表有 id=1,3,5 三条记录

-- 事务A执行:
BEGIN;
SELECT * FROM orders WHERE id > 1 AND id < 5 FOR UPDATE;
-- 锁定:(1,3] 和 (3,5] 两个区间
-- 同时锁定了 id=3 这条记录

-- 事务B尝试插入 id=4 的记录 → 被阻塞
-- 事务B尝试更新 id=3 的记录 → 被阻塞

锁的分类

锁类型 作用范围 说明
Record Lock 单行 只锁一行
Gap Lock 区间 锁两个值之间的间隙
Next-Key Lock 区间+行 Record Lock + Gap Lock
意向锁 表级 快速判断是否有行锁

二、分布式事务

2.1 CAP 定理

分布式系统最多只能同时满足两个特性:

CAP 三角:

    Consistency(一致性)
       /        \
      /          \
     /            \
    /   P(分区容错) \
   /                \
  /__________________\
  A(可用性)
特性 说明
C(一致性) 所有节点同一时刻看到相同数据
A(可用性) 每个请求都能在有限时间内得到响应
P(分区容错) 网络分区时,系统仍能运行

为什么只能同时满足两个?

网络分区不可避免 → 必须满足 P → 要么保证 C(等待同步完成,牺牲 A) → 要么保证 A(继续服务,牺牲 C)

2.2 BASE 理论

对 CAP 的补充,实际分布式系统的妥协方案:

BASE:
  - Basically Available(基本可用):允许暂时不一致
  - Soft state(软状态):数据可以处于中间状态
  - Eventually consistent(最终一致):经过一段时间后达到一致

2.3 两阶段提交(2PC)

流程

协调者                      参与者A                    参与者B
   |                           |                         |
   |---- prepare ---- -------->|                         |
   |                           |                         |
   |<--- prepare OK ----------|                         |
   |                           |                         |
   |------------------- prepare ------------------------>|
   |                           |                         |
   |<------------------ prepare OK ----------------------|
   |                                                   |
   |========= commit ============================|
   |                                                   |
   |<-------------- commit OK ----------------------|
   |                                                   |

问题

问题 说明
同步阻塞 参与者 prepare 后一直等待,直到 commit/rollback
单点故障 协调者挂了,参与者一直阻塞
数据不一致 部分参与者收到 commit,部分没收到

2.4 三阶段提交(3PC)

阶段1:CanCommit(询问阶段)
  协调者问参与者:你们能提交吗?
  参与者检查自己状态,返回 Yes/No

阶段2:PreCommit(预提交)
  协调者收到所有 Yes → 发送 PreCommit
  协调者收到任何 No → 发送 Abort

阶段3:DoCommit(真正提交)
  收到 PreCommit 的参与者执行提交

改进

  • 引入超时机制,避免无限阻塞
  • CanCommit 阶段参与者可以自由放弃

仍然存在的问题:网络分区时,仍可能出现数据不一致。

2.5 TCC(Try-Confirm-Cancel)

Try:预留资源(冻结)
  检查库存、预留座位、锁定账户

Confirm:确认执行
  真正扣库存、出票、扣钱

Cancel:取消回滚
  释放预留的资源
// 订单服务 TCC 示例
@LocalTCC
public interface OrderTccService {

    @Try(course = "tryOrder")
    void tryOrder(@BusinessContext Context context, Long userId, Long productId);

    @Confirm(course = "confirmOrder")
    void confirmOrder(@BusinessContext Context context, Long orderId);

    @Cancel(course = "cancelOrder")
    void cancelOrder(@BusinessContext Context context, Long orderId);
}

TCC 问题

问题 说明
空回滚 Try 没执行,Cancel 却执行了
幂等性 Confirm/Cancel 可能重复执行
悬挂 Cancel 先于 Confirm 执行

2.6 Saga 模式

长事务拆分为多个子事务,每个子事务有对应的补偿操作:

订单创建 → 扣库存 → 支付 → 发货
   ↓        ↓       ↓       ↓
  取消订单  回退库存  退款   撤回发货

适用于:

  • 补偿事务可以逆向操作
  • 长时间跨度的业务流程

2.7 Seata 分布式事务

阿里开源的分布式事务解决方案,支持 AT、TCC、Saga、XA 模式。

AT 模式(自动模式,最常用)

全局事务开启
    ↓
各分支事务执行,生成前后置快照(undo_log)
    ↓
提交成功后,异步删除 undo_log
    ↓
回滚时,根据 undo_log 生成反向 SQL 回滚

Seata AT 与 2PC 的区别

对比 2PC Seata AT
协调者 数据库自己 Seata Server
性能 差(同步阻塞) 好(异步执行)
侵入性 低(数据库支持) 低(框架自动处理)
复杂度

三、事务实践

3.1 本地事务最佳实践

@Transactional(rollbackFor = Exception.class)  // 必须指定 rollbackFor
public void createOrder(Long userId, List<Long> productIds) {
    // 1. 创建订单
    Order order = new Order();
    order.setUserId(userId);
    orderMapper.insert(order);

    // 2. 扣库存(如果抛出异常,自动回滚)
    for (Long productId : productIds) {
        productMapper.decreaseStock(productId, 1);
    }
}

常见问题

// 问题1:rollbackFor 不指定
// 默认只回滚 RuntimeException 和 Error
@Transactional  // ❌ 受检异常不会回滚
public void createOrder() throws IOException {
    // 这个不会回滚
}

// 解决:
@Transactional(rollbackFor = Exception.class)  // ✅

// 问题2:自调用导致事务失效
@Service
public class OrderService {
    public void createOrder() {
        this.doCreate();  // ❌ this 调用不走代理,事务不生效
    }

    @Transactional
    public void doCreate() { ... }
}

// 解决:注入自身,或通过 AopContext.currentProxy()

3.2 分布式事务实战

Seata AT 模式配置

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
seata:
  tx-service-group: my_tx_group
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
  config:
    type: nacos
// 使用 @GlobalTransactional 开启全局事务
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder(Long userId, List<Long> productIds) {
    // Seata 自动管理分支事务
    orderService.create(userId);
    productService.decreaseStock(productIds);
    paymentService.pay(userId, amount);
}

RocketMQ 事务消息(最终一致性)

发送方:
  1. 发送半消息(Half Message)到 MQ → 消息对消费者不可见
  2. 执行本地事务
  3. 提交 or 回滚半消息

MQ:
  4. 提交 → 消费者可见
  5. 回滚 → 丢弃

补偿机制:
  6. 未收到确认 → MQ 回查发送方本地事务状态
  7. 发送方根据本地事务结果回复

四、事务与锁的关系

4.1 更新丢失(Lost Update)

两个事务同时读取并更新,后者覆盖了前者的更新:

T1:读取 x=100
T2:读取 x=100
T1:x=100+50=150,写回
T2:x=100+80=180,写回
结果:x=180(丢失了 T1 的更新)❌

解决:悲观锁(SELECT ... FOR UPDATE)或乐观锁(版本号)。

4.2 死锁

T1:持有 A 锁,等待 B 锁
T2:持有 B 锁,等待 A 锁
→ 死锁

MySQL 死锁处理

  • InnoDB 自动检测死锁,回滚代价最小的事务
  • 业务层重试
-- 死锁日志查看
SHOW ENGINE INNODB STATUS;

4.3 乐观锁实现

-- 版本号方式
UPDATE orders
SET status = 'completed', version = version + 1
WHERE id = ? AND version = ?

-- 如果 version 不匹配,说明被其他事务更新了,返回失败

五、隔离级别与现象

现象 说明
脏读 读取到其他事务未提交的数据
不可重复读 同一事务两次读取同一行,结果不同(其他事务提交了)
幻读 同一事务两次查询,结果集不同(其他事务插入了新行)
隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ ✅(InnoDB 解决了)
SERIALIZABLE