数据库的可靠性不仅取决于正确的数据模型,更取决于并发访问时的一致性保障、分布式环境下的事务协调、以及面对灾难时的数据恢复能力。本文系统讲解并发控制(锁机制与 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;
避免死锁的最佳实践:
- 按固定顺序访问表和行:如总是先锁 id 小的再锁 id 大的
- 保持事务简短:减少锁持有时间
- 使用低隔离级别:RC 比 RR 的间隙锁更少
- 合理使用索引:避免行锁升级为表锁
- 显式加锁时指定 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_id | m_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-transaction | InnoDB 热备份,设置 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 Log 和 Undo 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 | 自动主从故障切换,从库提升为主库 | 传统主从复制架构 |
| MGR | MySQL Group Replication,基于 Paxos 的多主复制 | MySQL 原生高可用 |
| InnoDB Cluster | MGR + 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 并发控制要点
- InnoDB 行锁加在索引上,没有索引则升级为表锁
- MVCC 实现了读写不互斥,RC 每次读创建 Read View,RR 复用第一次的
- 避免死锁:按固定顺序访问、保持事务简短、合理使用索引
- MySQL 8.0+ 优先用
FOR SHARE/FOR UPDATE/NOWAIT/SKIP LOCKED
5.2 分布式事务要点
- 2PC 是强一致但同步阻塞的方案,适合传统中间件
- TCC 性能好但编码复杂,适合资金类高并发场景
- Saga 适合长流程业务,补偿操作必须幂等
- 本地消息表 实现简单、可靠性高,推荐作为默认方案
- CAP 决定了分布式事务无法同时满足强一致和高可用
5.3 备份与恢复要点
- 全量 + 增量 + Binlog 是最完整的备份策略
- WAL 原则:先写日志再写数据,保证崩溃恢复的正确性
- Redo Log 保证持久性,Undo Log 保证原子性
- PITR 依赖 Binlog,可恢复到任意时间点
- 备份要定期演练恢复,未验证的备份等于没有备份
5.4 参考文档
- MySQL 8.0 官方文档 - InnoDB Locking
- MySQL 8.0 官方文档 - InnoDB Transaction Model
- MySQL 8.0 官方文档 - Backup and Recovery
- Percona XtraBackup Documentation
- SEATA 官方文档