数据库事务管理与故障恢复

0 阅读21分钟

数据库的可靠性不仅取决于正确的数据模型,更取决于并发访问时的一致性保障、分布式环境下的事务协调、以及面对灾难时的数据恢复能力。本文系统讲解并发控制(锁机制与 MVCC)、分布式事务(2PC/3PC/TCC/Saga/本地消息表)、数据备份策略、故障恢复原理,以及 MySQL 中的实战配置。


一、并发控制

当多个事务同时访问数据库时,如果不加以控制,可能导致数据不一致。并发控制的核心目标是在保证隔离性的前提下,最大化并发度。

1.1 并发问题

问题说明示例
脏读事务 A 读到了事务 B 未提交的数据B 更新余额为 900(未提交),A 读到 900,B 回滚,A 读到的是"脏数据"
不可重复读事务 A 两次读同一行,中间被 B 修改,两次结果不同A 读余额为 1000,B 修改为 900 并提交,A 再读为 900
幻读事务 A 两次执行同一查询,中间被 B 插入新行,结果集不同A 查 age > 20 有 5 行,B 插入 1 行 age=25 并提交,A 再查有 6 行
丢失更新两个事务同时更新同一行,后提交的覆盖先提交的A、B 同时读到余额 1000,A 改为 900,B 改为 800,最终 800,A 的更新丢失

1.2 MySQL 事务隔离级别

隔离级别脏读不可重复读幻读性能
READ UNCOMMITTED❌ 可能❌ 可能❌ 可能最高
READ COMMITTED✅ 防止❌ 可能❌ 可能较高
REPEATABLE READ✅ 防止✅ 防止❌ 可能*中等
SERIALIZABLE✅ 防止✅ 防止✅ 防止最低

*MySQL InnoDB 在 REPEATABLE READ 级别通过 Next-Key Lock 在很大程度上防止了幻读。

-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- my.cnf 全局配置
[mysqld]
transaction_isolation = REPEATABLE-READ

1.3 锁机制

锁的分类

分类维度类型说明
粒度表级锁锁整张表,并发度低,开销小
行级锁锁一行记录,并发度高,开销大
间隙锁锁索引记录之间的间隙,防止插入
Next-Key Lock行锁 + 间隙锁,左开右闭区间
模式共享锁(S Lock)读锁,允许多个事务同时持有
排他锁(X Lock)写锁,独占,阻塞其他 S/X 锁
意向锁(IS/IX)表级锁,表示事务打算加行级 S/X 锁
使用方式隐式锁InnoDB 自动加的锁(写操作自动加 X 锁)
显式锁手动加锁

InnoDB 行锁类型

Record Lock(记录锁):锁定索引中的一条记录
    ↓
Gap Lock(间隙锁):锁定索引记录之间的间隙,防止其他事务在间隙中插入
    ↓
Next-Key Lock(临键锁):Record Lock + Gap Lock,锁定一个区间 [左边界, 右边界)    ↓Insert Intention Lock(插入意向锁):特殊的间隙锁,表示要在间隙中插入

加锁示例

-- 共享锁(读锁)
SELECT * FROM `t_order` WHERE `id` = 1 LOCK IN SHARE MODE;
-- MySQL 8.0+ 推荐写法
SELECT * FROM `t_order` WHERE `id` = 1 FOR SHARE;

-- 排他锁(写锁)
SELECT * FROM `t_order` WHERE `id` = 1 FOR UPDATE;

-- 查看当前锁情况
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- MySQL 8.0+ 使用 performance_schema
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;

锁与索引的关系

关键规则:InnoDB 的行锁是加在索引上的,不是加在数据行上的。如果没有用到索引,行锁会升级为表锁。

-- 假设 user_id 上有索引
SELECT * FROM `t_order` WHERE `user_id` = 1001 FOR UPDATE;
-- 只锁 user_id = 1001 的行 ✅

-- 假设 amount 上没有索引
SELECT * FROM `t_order` WHERE `amount` > 100 FOR UPDATE;
-- 无法使用索引,锁住整张表 ❌

-- 优化:给 amount 加索引
ALTER TABLE `t_order` ADD INDEX `idx_amount` (`amount`);
SELECT * FROM `t_order` WHERE `amount` > 100 FOR UPDATE;
-- 只锁满足条件的行 ✅

死锁

两个事务互相持有对方需要的锁,导致都无法继续:

时间线:
    事务A                        事务B
T1  LOCK id=1 (获取X锁)
T2                                LOCK id=2 (获取X锁)
T3  LOCK id=2 (等待B释放)
T4                                LOCK id=1 (等待A释放) ← 死锁!

InnoDB 的死锁检测:

-- 查看死锁日志
SHOW ENGINE INNODB STATUS;

-- 配置死锁检测(默认开启)
SET GLOBAL innodb_deadlock_detect = ON;

-- 配置锁等待超时(秒),超时自动回滚
SET innodb_lock_wait_timeout = 50;

避免死锁的最佳实践:

  1. 按固定顺序访问表和行:如总是先锁 id 小的再锁 id 大的
  2. 保持事务简短:减少锁持有时间
  3. 使用低隔离级别:RC 比 RR 的间隙锁更少
  4. 合理使用索引:避免行锁升级为表锁
  5. 显式加锁时指定 NOWAIT / SKIP LOCKED(MySQL 8.0+)
-- NOWAIT:获取不到锁立即返回错误,不等待
SELECT * FROM `t_order` WHERE `id` = 1 FOR UPDATE NOWAIT;

-- SKIP LOCKED:跳过被锁的行,返回未被锁的行
SELECT * FROM `t_order` WHERE `status` = 0 FOR UPDATE SKIP LOCKED;

1.4 MVCC(多版本并发控制)

MVCC 是 InnoDB 实现高并发的核心机制,让读操作不阻塞写操作,写操作不阻塞读操作。

核心组件

组件说明
隐藏列每行有 DB_TRX_ID(最后修改的事务ID)、DB_ROLL_PTR(回滚指针,指向 undo log)
Undo Log存储数据的历史版本,形成版本链
Read View事务开始时创建的"可见性判断快照"

版本链

当前行:{id=1, name='王五', DB_TRX_ID=303, DB_ROLL_PTR=→undo_2}
                                                        |
undo_2: {name='李四', DB_TRX_ID=202, DB_ROLL_PTR=→undo_1}
                                                        |
undo_1: {name='张三', DB_TRX_ID=101, DB_ROLL_PTR=NULL}

Read View 的可见性规则

Read View 包含四个关键字段:

字段含义
m_ids创建 Read View 时,当前活跃(未提交)的事务 ID 列表
min_trx_idm_ids 中的最小值
max_trx_id系统分配给下一个事务的 ID(当前最大事务 ID + 1)
creator_trx_id创建该 Read View 的事务 ID

判断某版本是否可见:

如果 DB_TRX_ID == creator_trx_id → 可见(自己的修改)

如果 DB_TRX_ID < min_trx_id → 可见(在 Read View 创建前已提交)

如果 DB_TRX_ID >= max_trx_id → 不可见(在 Read View 创建后才开始的事务)

如果 min_trx_id <= DB_TRX_ID < max_trx_id:
    如果 DB_TRX_ID 在 m_ids 中 → 不可见(事务未提交)
    如果 DB_TRX_ID 不在 m_ids 中 → 可见(事务已提交)

RC 与 RR 的 MVCC 差异

隔离级别Read View 创建时机效果
READ COMMITTED每次 SELECT 都创建新的 Read View可以看到其他事务已提交的最新数据,不可重复读
REPEATABLE READ事务中第一次 SELECT 创建,后续复用整个事务中看到的数据快照一致,可重复读
RC 级别:
T1: SELECT (创建 RV1) → 看到 A=100
T2: UPDATE A=200; COMMIT;
T1: SELECT (创建 RV2) → 看到 A=200  ← 两次读结果不同

RR 级别:
T1: SELECT (创建 RV1) → 看到 A=100
T2: UPDATE A=200; COMMIT;
T1: SELECT (复用 RV1) → 看到 A=100  ← 两次读结果相同

二、分布式事务

当业务操作涉及多个数据库或多个服务时,本地事务无法保证跨节点的一致性,需要分布式事务机制。

2.1 分布式事务的挑战

挑战说明
网络不可靠节点间通信可能超时、丢失、乱序
节点故障参与者可能随时宕机
时钟不同步不同节点的系统时钟可能有偏差
CAP 定理一致性(C)、可用性(A)、分区容错(P)不可兼得

2.2 CAP 与 BASE

CAP 定理

C(Consistency):所有节点看到的数据一致
A(Availability):每个请求都能得到非错误响应
P(Partition Tolerance):网络分区时系统仍能运行

三选二?实际上 P 必须保证,所以是 CP 或 AP:
  CP:保证一致性,牺牲可用性(如 ZooKeeper)
  AP:保证可用性,牺牲强一致性(如 Eureka)

BASE 理论(AP 方案的实践基础):

原则含义
Basically Available基本可用,允许响应时间变长或功能降级
Soft State软状态,允许中间状态存在
Eventually Consistent最终一致性,一段时间后数据达到一致

2.3 两阶段提交(2PC)

最经典的分布式事务协议,引入协调者(Coordinator)角色。

阶段一:准备(Prepare)
    协调者 ──→ 参与者1:请准备提交
    协调者 ──→ 参与者2:请准备提交
    参与者1 ──→ 协调者:OK(已锁定资源)
    参与者2 ──→ 协调者:OK(已锁定资源)

阶段二:提交(Commit)
    协调者 ──→ 参与者1:提交
    协调者 ──→ 参与者2:提交
    参与者1 ──→ 协调者:已提交
    参与者2 ──→ 协调者:已提交
优点缺点
实现相对简单同步阻塞:准备阶段锁住资源直到提交
强一致性单点故障:协调者宕机则参与者永久阻塞
太保守:任一参与者准备失败则全部回滚

2.4 三阶段提交(3PC)

在 2PC 基础上增加"预询问"阶段,减少阻塞范围:

阶段一:CanCommit(预询问)
    协调者 ──→ 参与者:能否提交?(不锁资源)
    参与者 ──→ 协调者:Yes / No

阶段二:PreCommit(预提交)
    协调者 ──→ 参与者:请准备提交(锁资源)
    参与者 ──→ 协调者:ACK

阶段三:DoCommit(正式提交)
    协调者 ──→ 参与者:提交
    参与者 ──→ 协调者:已提交

3PC 相比 2PC 的改进:

  • 增加超时机制:参与者在 PreCommit 后如果未收到 DoCommit,超时后自动提交
  • CanCommit 阶段不锁资源,减少阻塞时间

实际应用:3PC 仍然存在网络分区下的数据不一致风险,工业界很少使用,更多采用 TCC 或 Saga。

2.5 TCC(Try-Confirm-Cancel)

业务层面的分布式事务方案,将一个操作分为三个阶段:

Try(预留资源):
    扣款服务:冻结金额(不实际扣款)
    积分服务:预增积分(标记为"待确认")

Confirm(确认执行):
    扣款服务:实际扣减冻结金额
    积分服务:确认积分生效

Cancel(取消回滚):
    扣款服务:解冻金额
    积分服务:取消预增积分

代码示例(扣款服务):

/**
 * TCC 扣款服务
 */
@Service
public class DeductBalanceTccService {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private FreezeMapper freezeMapper;

    /**
     * Try 阶段:冻结金额
     */
    public boolean tryDeduct(Long userId, BigDecimal amount, String txId) {
        // 1. 检查余额是否充足
        Account account = accountMapper.selectByUserId(userId);
        if (account.getAvailableBalance().compareTo(amount) < 0) {
            return false;
        }

        // 2. 冻结金额
        accountMapper.freezeBalance(userId, amount);

        // 3. 记录冻结记录
        FreezeRecord record = new FreezeRecord();
        record.setTxId(txId);
        record.setUserId(userId);
        record.setAmount(amount);
        record.setStatus("FROZEN");
        freezeMapper.insert(record);

        return true;
    }

    /**
     * Confirm 阶段:实际扣款
     */
    public boolean confirmDeduct(String txId) {
        FreezeRecord record = freezeMapper.selectByTxId(txId);
        if (record == null || !"FROZEN".equals(record.getStatus())) {
            return false;
        }

        // 1. 实际扣减冻结金额
        accountMapper.deductFrozenBalance(record.getUserId(), record.getAmount());

        // 2. 更新冻结记录状态
        freezeMapper.updateStatus(txId, "CONFIRMED");

        return true;
    }

    /**
     * Cancel 阶段:解冻金额
     */
    public boolean cancelDeduct(String txId) {
        FreezeRecord record = freezeMapper.selectByTxId(txId);
        if (record == null) {
            return true; // 幂等处理
        }
        if (!"FROZEN".equals(record.getStatus())) {
            return true; // 幂等处理
        }

        // 1. 解冻金额
        accountMapper.unfreezeBalance(record.getUserId(), record.getAmount());

        // 2. 更新冻结记录状态
        freezeMapper.updateStatus(txId, "CANCELLED");

        return true;
    }
}

TCC 的关键要求:

要求说明
幂等性Confirm/Cancel 可能被重试,必须保证多次执行结果一致
空回滚Try 未执行但收到了 Cancel 请求,需要正确处理(返回成功)
防悬挂Cancel 比 Try 先执行,Cancel 执行后 Try 才到,需要阻止 Try 执行

2.6 Saga 模式

将长事务拆分为多个本地短事务,每个短事务有对应的补偿操作:

正向流程(T1 → T2 → T3):
T1: 创建订单      → 补偿C1: 取消订单
T2: 扣减库存      → 补偿C2: 恢复库存
T3: 扣减余额      → 补偿C3: 恢复余额

如果 T3 失败,逆向补偿:
T1 ✅ → T2 ✅ → T3 ❌ → C2(恢复库存)→ C1(取消订单)

两种编排方式:

方式说明优劣
编排式(Choreography)每个服务自己决定下一步操作去中心化,但流程不直观,调试困难
协调式(Orchestration)中央协调器统一调度各服务流程清晰,但协调器是单点

协调式 Saga 示例:

/**
 * 订单创建 Saga 协调器
 */
@Service
public class OrderCreateSaga {

    @Autowired
    private OrderService orderService;

    @Autowired
    private StockService stockService;

    @Autowired
    private AccountService accountService;

    /**
     * 执行创建订单的 Saga 流程
     */
    @Transactional
    public void createOrder(OrderDTO orderDTO) {
        try {
            // T1: 创建订单
            orderService.createOrder(orderDTO);

            // T2: 扣减库存
            stockService.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());

            // T3: 扣减余额
            accountService.deductBalance(orderDTO.getUserId(), orderDTO.getAmount());

        } catch (Exception e) {
            // 逆向补偿
            compensate(orderDTO);
            throw new RuntimeException("创建订单失败", e);
        }
    }

    /**
     * 逆向补偿
     */
    private void compensate(OrderDTO orderDTO) {
        try {
            accountService.refundBalance(orderDTO.getUserId(), orderDTO.getAmount());
        } catch (Exception ignored) {}

        try {
            stockService.restoreStock(orderDTO.getProductId(), orderDTO.getQuantity());
        } catch (Exception ignored) {}

        try {
            orderService.cancelOrder(orderDTO.getOrderNo());
        } catch (Exception ignored) {}
    }
}

注意:Saga 的补偿操作也必须保证幂等性。且补偿操作本身可能失败,需要重试机制。

2.7 本地消息表

一种实用的最终一致性方案,通过本地事务 + 消息表保证消息一定发送成功。

1. 业务操作与消息写入在同一个本地事务中
2. 后台定时任务扫描消息表,发送未处理的消息
3. 消费者消费消息,成功后通知发送方
4. 发送方更新消息状态为"已完成"

表设计:

-- 本地消息表
CREATE TABLE `t_local_message` (
    `id`           BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `message_id`   VARCHAR(64)     NOT NULL COMMENT '消息唯一ID',
    `topic`        VARCHAR(128)    NOT NULL COMMENT '消息主题',
    `content`      TEXT            NOT NULL COMMENT '消息内容',
    `status`       TINYINT         NOT NULL DEFAULT 0 COMMENT '状态:0待发送 1已发送 2已消费 3失败',
    `retry_count`  INT             NOT NULL DEFAULT 0 COMMENT '重试次数',
    `create_time`  DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time`  DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_message_id` (`message_id`),
    KEY `idx_status` (`status`)
) COMMENT '本地消息表';

代码示例:

/**
 * 本地消息表方案实现
 */
@Service
public class LocalMessageService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private LocalMessageMapper messageMapper;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 创建订单(本地事务 + 消息表)
     */
    @Transactional(rollbackFor = Exception.class)
    public void createOrderWithMessage(OrderDTO orderDTO) {
        // 1. 业务操作:创建订单
        Order order = new Order();
        order.setOrderNo(orderDTO.getOrderNo());
        order.setUserId(orderDTO.getUserId());
        order.setAmount(orderDTO.getAmount());
        orderMapper.insert(order);

        // 2. 写入本地消息表(与业务操作在同一事务中)
        LocalMessage message = new LocalMessage();
        message.setMessageId(UUID.randomUUID().toString());
        message.setTopic("order-created");
        message.setContent(JSON.toJSONString(orderDTO));
        message.setStatus(0); // 待发送
        messageMapper.insert(message);
    }

    /**
     * 定时任务:扫描并发送未处理的消息
     */
    @Scheduled(fixedRate = 5000)
    public void sendMessageTask() {
        List<LocalMessage> messages = messageMapper.selectByStatus(0);
        for (LocalMessage msg : messages) {
            try {
                // 发送消息到 MQ
                rocketMQTemplate.convertAndSend(msg.getTopic(), msg.getContent());
                // 更新状态为已发送
                messageMapper.updateStatus(msg.getId(), 1);
            } catch (Exception e) {
                // 更新重试次数
                messageMapper.incrementRetryCount(msg.getId());
                // 超过最大重试次数,标记为失败
                if (msg.getRetryCount() >= 5) {
                    messageMapper.updateStatus(msg.getId(), 3);
                }
            }
        }
    }
}

2.8 各方案对比

方案一致性性能复杂度适用场景
2PC强一致低(同步阻塞)传统数据库中间件
3PC强一致理论研究,实际少用
TCC最终一致高(无锁)资金类、高并发场景
Saga最终一致长流程业务编排
本地消息表最终一致跨服务异步通知

选型建议:优先使用本地消息表(实现简单、可靠性高);对一致性要求极高的资金场景使用 TCC;长流程业务使用 Saga。


三、数据备份

数据备份是数据库安全的最后一道防线,确保在灾难发生时能恢复数据。

3.1 备份分类

维度类型说明
数据范围全量备份备份所有数据
增量备份只备份自上次备份以来变化的数据
差异备份只备份自上次全量备份以来变化的数据
运行状态热备份数据库运行中备份,不影响业务
温备份数据库只读,可查不可改
冷备份数据库停止后备份
备份内容物理备份直接复制数据文件(ibdata、.ibd 等)
逻辑备份导出 SQL 语句(INSERT、CREATE TABLE 等)

3.2 三种备份策略对比

全量备份(Full Backup):
    周日:全量
    周一:全量
    周二:全量
    ...
    恢复:只需最近一次全量
    优点:恢复简单   缺点:备份量大、耗时长

增量备份(Incremental Backup):
    周日:全量
    周一:增量(基于周日)
    周二:增量(基于周一)
    ...
    恢复:周日全量 + 周一增量 + 周二增量 + ...
    优点:备份量小   缺点:恢复复杂,任一增量丢失则后续无法恢复

差异备份(Differential Backup):
    周日:全量
    周一:差异(相对周日)
    周二:差异(相对周日)
    ...
    恢复:周日全量 + 最近一次差异
    优点:恢复较简单   缺点:备份量逐渐增大

3.3 MySQL 物理备份:mysqlbackup 与 XtraBackup

XtraBackup(开源,最常用)

# 全量备份
xtrabackup --backup --target-dir=/backup/full \
    --user=root --password=123456

# 准备备份(使备份文件一致性可恢复)
xtrabackup --prepare --target-dir=/backup/full

# 增量备份
xtrabackup --backup --target-dir=/backup/inc1 \
    --incremental-basedir=/backup/full \
    --user=root --password=123456

# 恢复全量备份
# 1. 先准备全量
xtrabackup --prepare --apply-log-only --target-dir=/backup/full
# 2. 再合并增量
xtrabackup --prepare --apply-log-only --target-dir=/backup/full \
    --incremental-dir=/backup/inc1
# 3. 恢复到数据目录
xtrabackup --copy-back --target-dir=/backup/full
# 4. 修改文件权限
chown -R mysql:mysql /var/lib/mysql

3.4 MySQL 逻辑备份:mysqldump

# 全库备份
mysqldump -u root -p --all-databases --single-transaction > all_db.sql

# 单库备份
mysqldump -u root -p --single-transaction mydb > mydb.sql

# 单表备份
mysqldump -u root -p mydb t_order > t_order.sql

# 仅表结构
mysqldump -u root -p --no-data mydb > schema.sql

# 仅数据
mysqldump -u root -p --no-create-info mydb > data.sql

关键参数:

参数说明
--single-transactionInnoDB 热备份,设置 RR 隔离级别并开启一致性快照
--master-data=2记录备份时的 binlog 位置,注释形式写在 SQL 中
--routines备份存储过程和函数
--triggers备份触发器(默认开启)
--set-gtid-purged=OFF不在备份中写 GTID 信息

恢复:

mysql -u root -p mydb < mydb.sql

3.5 Binlog 备份

Binlog 记录了所有数据变更操作,是实现时间点恢复(PITR)的关键:

# my.cnf 开启 binlog
[mysqld]
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7    # 保留7天
max_binlog_size = 100M  # 单个文件最大100M
# 查看当前 binlog 文件列表
SHOW BINARY LOGS;

# 查看当前正在写入的 binlog
SHOW MASTER STATUS;

# 手动刷新 binlog(生成新文件)
FLUSH LOGS;

# 定期备份 binlog 到远程存储
cp /var/lib/mysql/mysql-bin.* /backup/binlog/

3.6 备份策略设计

推荐策略:全量 + 增量 + Binlog

每周日 02:00  全量备份
每天 02:00    增量备份(周一到周六)
持续          实时 binlog 同步到备份存储
数据恢复时间线:
                          全量备份     增量备份     当前时间
                             |           |            |
|────────────────────────────|───────────|────────────|
                             |← 恢复至此 →|
                                          |← binlog回放 →|

关键指标:

指标说明建议
RPO(Recovery Point Objective)可容忍的最大数据丢失量金融:秒级;普通业务:分钟级
RTO(Recovery Time Objective)可容忍的最大恢复时间金融:< 5 分钟;普通业务:< 1 小时

四、故障恢复

4.1 故障类型

故障类型原因影响范围恢复方式
事务故障事务执行中的逻辑错误、死锁回滚单个事务撤销(Undo)该事务
系统故障断电、OS 崩溃、MySQL 进程崩溃所有未提交事务重做(Redo)已提交 + 撤销未提交
介质故障磁盘损坏、数据文件损坏部分或全部数据从备份恢复 + 重做日志
网络分区网络中断分布式节点间脑裂处理、主从切换

4.2 InnoDB 恢复机制

InnoDB 通过 Redo LogUndo Log 实现 ACID 中的持久性和原子性。

Redo Log(重做日志)

保证持久性:事务提交后,即使系统崩溃,已提交的修改也不会丢失。

数据修改流程:
1. 修改数据页(在 Buffer Pool 中)
2. 写 Redo Log(顺序写,WAL 原则)
3. 事务提交时,确保 Redo Log 刷盘
4. 数据页由后台线程异步刷盘(Checkpoint)
Redo Log 工作机制:
    写入 → Redo Log Buffer → OS Cache → 磁盘
              ↑                  ↑           ↑
         innodb_log_buffer_size  |    innodb_flush_log_at_trx_commit
                                |         1=每次提交刷盘
                                |         2=每次提交写OS缓存
                                |         0=异步刷盘

Undo Log(回滚日志)

保证原子性:事务回滚或崩溃恢复时,撤销未完成事务的修改。

Undo Log 作用:
1. 事务回滚:根据 Undo Log 恢复数据到事务开始前的状态
2. MVCC:为读操作提供历史版本
3. 崩溃恢复:对未提交事务执行 Undo 操作

WAL 原则(Write-Ahead Logging)

核心原则:数据页的修改必须先写入日志(Redo Log),再写入磁盘。事务提交只需确保 Redo Log 持久化,数据页可以异步刷盘。

WAL 的优势:

  • 顺序写优于随机写:Redo Log 是顺序追加写,数据页是随机写
  • 组提交:多个事务的 Redo Log 可以合并一次刷盘
  • 快速提交:事务提交只需等 Redo Log 写入,不等数据页刷盘

4.3 崩溃恢复流程

MySQL 崩溃后重启
    |
    v
1. 读取 Redo Log,重放所有已提交但未刷盘的修改(Redo)
    |
    v
2. 读取 Undo Log,回滚所有未提交事务的修改(Undo)
    |
    v
3. 数据库恢复到一致性状态
    |
    v
4. 开始接受新连接

4.4 时间点恢复(PITR)

利用全量备份 + Binlog 将数据库恢复到任意时间点:

# 1. 恢复全量备份
mysql -u root -p < /backup/full/mydb.sql

# 2. 从 binlog 中提取并重放指定时间段的操作
mysqlbinlog --start-datetime="2026-07-01 02:00:00" \
            --stop-datetime="2026-07-07 14:30:00" \
            /var/lib/mysql/mysql-bin.000123 | mysql -u root -p

# 或基于位置恢复
mysqlbinlog --start-position=154 \
            --stop-position=1024 \
            /var/lib/mysql/mysql-bin.000123 | mysql -u root -p

关键:如果要恢复到某个错误操作之前的状态,--stop-datetime 应设为错误操作发生之前的时刻。

4.5 主从切换与高可用

MySQL 主从复制

主库(Master)
    |
    +-- 写入 Binlog
    |
    v
从库(Slave)
    |
    +-- IO 线程:拉取主库 Binlog → 写入 Relay Log
    +-- SQL 线程:执行 Relay Log → 应用到本地数据
-- 主库配置
[mysqld]
server-id = 1
log_bin = mysql-bin
binlog_format = ROW

-- 从库配置
[mysqld]
server-id = 2
relay_log = relay-bin
read_only = ON
-- 从库设置主库信息
CHANGE MASTER TO
    MASTER_HOST = '192.168.1.100',
    MASTER_PORT = 3306,
    MASTER_USER = 'repl',
    MASTER_PASSWORD = 'repl123',
    MASTER_LOG_FILE = 'mysql-bin.000001',
    MASTER_LOG_POS = 154;

-- 启动复制
START SLAVE;

-- 查看复制状态
SHOW SLAVE STATUS\G
-- 关注:Slave_IO_Running / Slave_SQL_Running / Seconds_Behind_Master

主从切换

计划内切换(优雅切换):
1. 主库设为只读:SET GLOBAL read_only = ON;
2. 等待从库追平:SHOW SLAVE STATUS 直到 Seconds_Behind_Master = 0
3. 从库提升为主库:STOP SLAVE; RESET SLAVE ALL; SET GLOBAL read_only = OFF;
4. 旧主库设为新从库:CHANGE MASTER TO ...

计划外切换(故障切换):
1. 确认主库确实不可用
2. 选择数据最完整的从库提升为主库
3. 其他从库指向新主库
4. 应用层切换连接

高可用方案

方案说明适用场景
MHA自动主从故障切换,从库提升为主库传统主从复制架构
MGRMySQL Group Replication,基于 Paxos 的多主复制MySQL 原生高可用
InnoDB ClusterMGR + MySQL Router + MySQL Shell官方推荐完整方案
Orchestrator自动拓扑管理、故障检测与恢复大规模 MySQL 集群

4.6 数据损坏恢复

# 检查表是否损坏
CHECK TABLE t_order;

# 修复 MyISAM 表
REPAIR TABLE t_order;

# InnoDB 表损坏的恢复步骤
# 1. 尝试 innodb_force_recovery 启动
[mysqld]
innodb_force_recovery = 1   # 从 1 开始递增,最大 6

# 2. 导出数据
mysqldump --single-transaction mydb > recovery.sql

# 3. 重建数据库
mysql -e "DROP DATABASE mydb;"
mysql -e "CREATE DATABASE mydb;"
mysql mydb < recovery.sql

# 4. 去掉 innodb_force_recovery 重启

innodb_force_recovery 级别:

级别说明
1最轻,忽略损坏页的检查
2不运行后台任务
3不运行回滚恢复
4不运行插入缓冲合并
5不查看 Undo Log
6不运行 Redo Log 前滚

注意innodb_force_recovery > 0 时数据库为只读模式,只用于数据导出。


五、总结

5.1 并发控制要点

  1. InnoDB 行锁加在索引上,没有索引则升级为表锁
  2. MVCC 实现了读写不互斥,RC 每次读创建 Read View,RR 复用第一次的
  3. 避免死锁:按固定顺序访问、保持事务简短、合理使用索引
  4. MySQL 8.0+ 优先用 FOR SHARE / FOR UPDATE / NOWAIT / SKIP LOCKED

5.2 分布式事务要点

  1. 2PC 是强一致但同步阻塞的方案,适合传统中间件
  2. TCC 性能好但编码复杂,适合资金类高并发场景
  3. Saga 适合长流程业务,补偿操作必须幂等
  4. 本地消息表 实现简单、可靠性高,推荐作为默认方案
  5. CAP 决定了分布式事务无法同时满足强一致和高可用

5.3 备份与恢复要点

  1. 全量 + 增量 + Binlog 是最完整的备份策略
  2. WAL 原则:先写日志再写数据,保证崩溃恢复的正确性
  3. Redo Log 保证持久性,Undo Log 保证原子性
  4. PITR 依赖 Binlog,可恢复到任意时间点
  5. 备份要定期演练恢复,未验证的备份等于没有备份

5.4 参考文档