你天天写SELECT、INSERT、UPDATE,一顺手就COMMIT。但大多数时候,你并不会去想:一条看起来已经执行成功的 SQL,凭什么到了磁盘上还是对的?要是事务执行到一半数据库崩溃了呢?要是两个客户端同时改同一行呢?要是主库刚返回成功、从库还没收到日志呢?如果这些环节里有任何一个“掉链子”,业务数据都可能悄无声息地错掉。
关系型数据库能几十年如一日地扛住这些风险,靠的不是运气,而是一整套“故意设计的冗余”:ACID 事务、WAL 日志、undo/redo log、锁和 MVCC、页校验、双写缓冲、主从复制、半同步机制,以及我们最常忽略的备份恢复。这篇文章把这些机制拆开讲,让你从“会写 SQL”到“知道数据库是怎么保证数据不出错的”,并且能把这些知识用到线上问题的排查和架构设计里。
先看整体框架,再用具体机制说明,最后落到真实场景和排错手段。如果你是后端开发、DBA 或正在做系统设计的工程师,这篇内容值得收藏。
1. 数据不出错的整体框架:事务、日志、多版本和副本
一句话先回答标题:关系型数据库保证数据不出错,靠的是把“出错”这件事变成可检测、可回退、可重放、可恢复。
- 进程崩了,事务能回滚:靠 undo log。
- 机器突然断电,已提交数据不丢:靠 redo log 和 WAL。
- 并发写同一行,结果不互相覆盖:靠锁和 MVCC。
- 磁盘静默损坏,能发现还能兜底:靠页校验和双写缓冲。
- 主库挂了,切换后数据尽量不丢:靠半同步复制和主从数据校验。
- 半夜手滑 DELETE 全表,还能救回来:靠全量备份 + binlog 回放。
1.1 ACID 不只是“印象”,它是数据库的底座
数据库事务被抽象成 ACID 四个特性,很多人背过概念,却在写代码时没有真正用起来。原子性管的是“要么全成功,要么全失败”;一致性管的是“事务执行前后,数据状态要满足约束”;隔离性管的是“并发事务之间不能互相干扰”;持久性管的是“一旦提交,结果就永久生效”。
ACID 不是应用层想出来的规范,而是数据库在底层用日志、锁、版本链和崩溃恢复机制实现的。单独看任何一条都可能觉得抽象,组合起来就是“数据库自主纠错”的完整闭环。
1.2 核心机制速览
先给一张速览表,后面逐项展开。
| 保证维度 | 核心机制 | 解决什么问题 |
|---|---|---|
| 原子性 | undo log / 回滚段 | 事务中途失败时撤销已执行的修改 |
| 持久性 | redo log、WAL、fsync | 断电或崩溃后已提交事务不丢失 |
| 隔离性 | 锁、MVCC、ReadView | 并发读写互相干扰,出现脏数据 |
| 一致性 | 约束、外键、应用事务边界 | 从入口拦截非法数据,保证业务规则成立 |
| 存储可靠性 | 页校验、双写缓冲、纠错 | 磁盘部分写、坏块、静默损坏能被发现 |
| 崩溃恢复 | checkpoint、LSN、回滚重放 | 启动时把数据恢复到一致状态 |
| 主从一致性 | binlog、半同步复制、校验工具 | 复制链路异构、主从数据偏离可发现 |
| 灾难恢复 | 全量备份、增量备份、binlog | 误删、整库损坏时找回业务数据 |
2. 事务与 ACID:写到一半断了怎么办
先从一个最简单的转账场景入手。
START TRANSACTION; -- 扣款方余额充足才扣 UPDATE account SET balance = balance - 100 WHERE id = 1 AND balance >= 100; -- 收款方加钱 UPDATE account SET balance = balance + 100 WHERE id = 2; COMMIT;第一条更新刚执行完、第二条更新还没执行时,如果数据库进程被kill -9,或者宿主机突然断电,数据库重启后应该处于什么状态?绝对不能出现“钱扣了但没到账”这种只执行一半的情况。
2.1 原子性:没有 undo log,事务无法“后悔”
为了支持回滚,数据库在修改每一行时,会先把修改前的旧值记录到 undo log 里。事务没有提交时,如果发生异常回滚,数据库通过 undo log 里的反向操作把数据恢复原状。
更关键的是崩溃恢复场景。事务 A 执行了 10 条更新,其中第 5 条之后数据库崩溃。重启时,数据库扫描到事务 A 是“未提交”状态,就会利用 undo log 把这 10 条更新全部撤销。这里的核心点在于:不是只清理崩溃前最后一条,而是把整个事务的修改全部抹掉,这才叫原子性。
所以在代码里不要把几十条重要 SQL 拆成多条独立事务去“慢慢提交”。一旦中间失败,前面已经提交的部分就无法自动回滚,数据库能力再强也帮不了你。
2.2 持久性:先有 WAL,再有数据文件
如果每次事务提交时都要把修改后的数据页立刻刷回磁盘,性能会非常差,因为磁盘随机写比顺序写慢太多。数据库普遍采用WAL(Write-Ahead Logging):先把事务产生的日志顺序写入日志文件,并且保证日志文件已经刷盘,再返回“提交成功”。
MySQL InnoDB 里对应的是 redo log。redo log 记录的是“页上做了什么修改”,它是物理逻辑日志。崩溃恢复时,数据库根据日志的 LSN 判断哪些数据页还没刷盘,再重新把这些修改应用一遍。这样就不怕“日志写了但数据页还没来得及写”的场景。
PostgreSQL 的 WAL 机制也是类似原理:commit 是否成功,取决于 WAL 是否刷盘,而不是数据文件是否写完。
这里必须注意一个经典参数配置:
[mysqld] # MySQL 8.0.30 之后用该参数控制 redo log 容量 innodb_redo_log_capacity = 1G # 每次事务提交都把 redo log 刷盘,保证持久性 innodb_flush_log_at_trx_commit = 1 # 每次事务提交都同步 binlog 到磁盘,保证复制安全 sync_binlog = 1 # 行级 binlog,降低误操作和主从不一致风险 binlog_format = ROWinnodb_flush_log_at_trx_commit如果改成 2,数据库性能会提升,但主机断电时可能丢失最近 1 秒左右的事务。很多互联网团队会做性能和持久性折中,但金融类核心系统一般会用最保守的配置。这个取舍必须由业务方想清楚,不能默认“数据库反正不会丢”。
2.3 一致的“事务边界”比数据库更关键
你可能会疑惑:ACID 里的“一致性”难道不是数据库自动保证的吗?并不是。数据库只能保证约束条件不被破坏,但无法理解“业务上账户余额不能为负”这种规则。
例如转账 SQL 里如果漏写balance >= 100这个条件,数据库不会主动拦下来。约束、外键、唯一索引、非空约束,能帮你在很大程度上挡掉非法数据,但大量业务一致性需要靠事务边界和应用代码共同维护。
所以从工程角度看,“数据库保证数据不出错”有个隐含前提:使用者的 SQL 本身要写得正确、事务边界要清晰。数据库能保证的是在并发、故障面前不放大错误,而不是替你修复错误逻辑。
3. 日志机制拆解:redo、undo、binlog 各管一摊
很多初学者混淆 redo log、undo log 和 binlog。这里用一个表格区分。
| 日志类型 | 所属层级 | 记录内容 | 主要作用 |
|---|---|---|---|
| redo log | InnoDB 存储引擎层 | 物理页修改 | 崩溃恢复、持久性 |
| undo log | InnoDB 存储引擎层 | 事务回滚所需旧值 | 事务回滚、MVCC 版本链 |
| binlog | MySQL 服务层 | SQL 语句或行变更逻辑 | 主从复制、基于时间的恢复 |
redo log 的数据是循环写的,容量有限,写满后需要推进 checkpoint,把脏页刷盘,才能复用空间。所以如果你有一个超长事务,或者瞬时写入量过大,redo log 可能会在短时间内占用大量磁盘 IO,甚至影响其他写请求。
undo log 会保存在回滚段里。事务活跃时间越长、修改行数越多,undo log 就可能膨胀。长事务还会导致 MVCC 历史版本无法清理,表面上是事务漏提交,实际上会拖垮数据库的读写性能。这也能解释为什么生产环境总要强调“避免长事务”。
binlog 是逻辑日志,负责 MySQL 主从复制和恢复。在 MySQL 8.0 环境下,binlog_format=ROW是更稳妥的默认选择。如果使用 STATEMENT 格式,UPDATE ... WHERE ...里的函数或不确定条件可能导致主库和从库数据不一致。ROW 格式会记录每一行变更的最终值,虽然日志量大一些,但数据回放更准确。
崩溃恢复并不是简单地“重放 redo log”,它分为两个阶段:先利用 redo log 将已提交且未来得及刷盘的修改重新应用;再利用 undo log 回滚崩溃时尚未提交的事务。这个过程也叫前滚和回滚。所以“先日志,后数据”的顺序非常重要,这也是 WAL 被几乎所有主流关系型数据库采用的原因。
4. 隔离性:并发不踩踏,靠锁和 MVCC
数据库“不出错”的另一半难题是并发。假设两个事务同时执行:
- 事务 A:把商品库存从 100 改成 90。
- 事务 B:把商品库存从 90 改成 80。
如果两个事务完全并发且不加控制,结果可能是:A 和 B 都读到 100,A 改成 90,B 也基于 100 改成 90,最终库存是 90,丢了 B 的扣减。这就是典型的更新丢失。
4.1 四个隔离级别
ANSI 定义了四种事务隔离级别,用来权衡一致性与并发性能。
| 隔离级别 | 可能发生的异常 | 解决程度 |
|---|---|---|
| READ UNCOMMITTED | 脏读、不可重复读、幻读 | 基本不推荐 |
| READ COMMITTED | 不可重复读、幻读 | 多数数据库默认 |
| REPEATABLE READ | 幻读(InnoDB 靠间隙锁可解决) | MySQL 默认 |
| SERIALIZABLE | 基本没有以上异常,但并发度低 | 读写都加锁 |
MySQL 默认是 REPEATABLE READ,通过 MVCC + 间隙锁基本消除了幻读。PostgreSQL 默认是 READ COMMITTED,用户也可以选择 REPEATABLE READ。这里不评价谁更好,更值得做的是在实际项目里确认自己用的是哪个级别。
查看当前隔离级别:
-- MySQL SELECT @@transaction_isolation; -- PostgreSQL SHOW transaction_isolation;如果业务核心是资金、库存、订单状态,最简单有效的方法不是把隔离级别调到 SERIALIZABLE,而是在代码里加锁或使用乐观锁。
4.2 当前读加锁:SELECT ... FOR UPDATE
在高并发扣库存场景里,正确写法通常是这样:
START TRANSACTION; -- 加行锁,并带上余额条件 SELECT balance FROM account WHERE id = 1 FOR UPDATE; -- 应用逻辑判断余额是否充足 UPDATE account SET balance = balance - 100 WHERE id = 1; COMMIT;SELECT ... FOR UPDATE是当前读,它会读取最新已提交数据,并对命中的行加锁,让其他写事务必须等待。没有这个锁,两个并发请求可能同时读到同一个余额,然后各自扣款,数据最后就错了。
死锁在高并发事务里也是常见现象。事务 A 锁了行 1 再等行 2,事务 B 锁了行 2 再等行 1,两者互不相让。数据库会通过死锁检测让其中一个事务回滚。应用层遇到死锁需要捕获异常并重试,而不是把死锁当成“数据库出错”。
4.3 快照读与 MVCC:读不阻塞写
如果所有 SELECT 都加锁,数据库的并发读性能会很难看。于是 InnoDB 使用 MVCC 实现快照读:普通 SELECT 不需要加锁,直接读取某个版本的数据,写事务照常进行,读写互不阻塞。
MVCC 的核心是 undo log 版本链和 ReadView。每一行记录里会记录事务 ID,行上可能存在多个历史版本。读事务开启时生成 ReadView,根据规则判断哪些版本对它可见。这就是“可重复读”在 MySQL 中不依赖锁也能生效的原因。
用 MVCC 时要特别注意一个认知:它解决的是快照读的并发问题,并不能解决“事务 A 读到了旧数据,然后基于旧数据去更新”的丢失更新问题。更新操作如果要绝对准确,仍然要走当前读或锁。
5. 磁盘层面的“防呆设计”:页校验与双写缓冲
事务日志能应对崩溃,但是磁盘本身也可能骗人。数据库传统机械盘或 SSD 都可能出现“写后校验不一致”的情况,即应用程序认为写成功了,实际数据位被翻转。这种错误叫静默数据损坏,是数据库最难察觉的问题之一。
5.1 页校验与坏页检查
InnoDB 数据页在写入时会计算校验和,读取时也会校验。如果发现页的校验和与内容不匹配,数据库会报告“page corrupt”并拒绝使用这个页。这只是有存储引擎自动完成的机制。
想验证数据库有没有检出坏页,可以使用查询:
SHOW GLOBAL STATUS LIKE 'Innodb_page_errors';不同版本统计口径略有差异,但思路一致:如果该值不断增长,说明存储系统存在异常,需要优先检查磁盘硬件。
5.2 doublewrite buffer:防止半页写
数据库写一个 16KB 的数据页时,操作系统可能只会刷出一部分内容。如果此时断电,磁盘上就会出现一个“半个新页 + 半个旧页”的残缺状态,而且 redo log 也不一定能恢复它,因为 redo log 基于 LSN 重放,而残缺页的 LSN 可能已经部分写入。
InnoDB 通过 doublewrite buffer 机制解决:先向共享表空间中的 doublewrite 区域整页写入,再写实际数据文件。崩溃恢复时,如果检测到数据页损坏,可以用 doublewrite 区域的副本覆盖回来。很多云数据库已经默认开启该功能,出现磁盘异常时它能明显降低修复成本。
5.3 崩溃恢复流程
当我们重启一个异常关闭的 MySQL 实例时,InnoDB 会进入恢复流程:
- 根据 redo log 的最新 checkpoint,扫描需要恢复的 LSN 范围。
- 从 redo log 重放数据页的修改,让内存和磁盘数据追上崩溃点。
- 找出崩溃时尚未提交的事务,利用 undo log 回滚。
- 完成后打开数据库供外部访问。
这个过程中任何日志文件或数据文件缺失,都可能中断恢复。所以日常运维一定要开启监控:redo log 文件是否异常膨胀、磁盘空间是否充足、异常重启日志里有没有 “recovery” 告警。数据文件损坏不是 DBA 应不应该碰上,而是“当碰上了,流程能不能自动接住”。
6. 主从复制:数据不出错的“分布式版本”
单机数据库再强,也扛不住整机故障。为了高可用,我们经常做主从复制。但主从复制如果只做异步,很可能出现主库已经提交、从库还没收到日志的情况。这时候主库宕机,强制切换从库,业务就会丢数据。
6.1 异步复制、半同步复制和组复制
异步复制是 MySQL 默认且最常见的模式,主库执行完事务,并不等待从库确认。从库网络延迟高或实例切换时,主备数据窗口就拉大。普通业务可以接受秒级延迟,但不能接受切换后丢关键订单。
半同步复制至少会等待一个从库收到并写入 relay log,主库事务才返回提交成功。这样能在主库突然宕机时,保证至少一个从库有该事务日志。注意,如果半同步超时或从库异常,MySQL 通常会降级为异步,此时仍然存在丢数风险。
更强一致性的方案是 MySQL Group Replication 或 Galera,通过组成员协商保证每个节点提交顺序一致。但分布式系统没有“完美的性能又完美的一致”,同步越严格,提交延迟越高,网络抖动影响越大。
6.2 复制链路检查
从库数据偏离是很多线上事故的隐藏根源。复制进程可能因为字段长度、字符集、唯一键冲突而中断,也可能因为主库 SQL 中存在非确定函数,导致从库应用结果不同。即便 binlog 用 ROW 格式,也无法完全避免误操作在主从同步后的结果。
日常巡检至少要关注:
-- 从库执行,查看复制状态(MySQL 8.0 术语为 Replica) SHOW REPLICA STATUS; -- 如果看到 Replica_IO_Running: Yes、Replica_SQL_Running: Yes -- 说明复制链路本身在工作;但这不代表数据内容完全一致。SHOW REPLICA STATUS显示的 “Seconds_Behind_Source” 只是估算延迟,并不代表主从数据一定一致。复制链路没有报错,也不意味着两张表里的数据完全相同。
6.3 主从数据校验:不上工具,问题发现不了
建议定期使用 Percona Toolkit 的pt-table-checksum对主从关键表做数据校验。
pt-table-checksum --host=127.0.0.1 --user=checksum --password=yourpass \ --databases=app_db --tables=orders --replicate=app_db.checksums它会统计主从各行数据的校验值,并在从库执行同样的校验,最后输出差异。如果差异不为 0,先确认是否有延迟,再用pt-table-sync人工核对后再修复。
用这类工具时要谨慎:它会对表加检查和锁定,最好在业务低峰期先小范围执行,并明确数据库账号的权限边界。生产环境不需要追求“所有表所有时刻都绝对一致”,核心表定期校验优于永远不校验。
7. 备份与恢复:最后一次“兜底”
前面讲的所有机制,都是尽量让数据库在故障中“自己恢复”。但有一种情况数据库帮不了你:人写错了 SQL。
DELETE FROM orders WHERE create_time < '2025-01-01';如果写这个 SQL 时以为自己在测试库,实际连接在生产库,并且没有 WHERE 限制,那一条命令就会批量删除数据。这时候事务日志只能保证这条 DELETE 作为一个事务回滚,但不能自动判断你是不是误操作。能救你的只有一套完整、可恢复、定时演练过的备份。
7.1 物理备份与逻辑备份
- 逻辑备份:使用
mysqldump导出 SQL 或 CSV,适合小库、整库逻辑导出,恢复速度较慢。 - 物理备份:Percona XtraBackup 或 MariaDB Backup,直接复制物理文件,恢复速度快,适合中大型数据库。
一个比较稳妥的框架是:
- 每日做全量物理备份;
- 实时或定时传输 binlog 到备份机;
- 保留最近 N 天全量 + 连续 binlog;
- 至少每月进行一次恢复演练。
执行 mysqldump 示例:
mysqldump \ --single-transaction \ --set-gtid-purged=OFF \ --databases app_db \ --host=127.0.0.1 \ --user=backup_user \ --password=xxx \ --result-file=/backup/app_db_$(date +%F).sql--single-transaction是为了在不锁表的情况下拿到一致快照,适用于 InnoDB。恢复时:
mysql --host=127.0.0.1 --user=xxx --password=xxx app_db < app_db_2025-01-01.sql7.2 利用 binlog 恢复到误删前一秒
只有全量备份还不够,因为备份通常在凌晨执行,如果下午三点发生误删,备份文件只能恢复到凌晨状态。此时需要把凌晨备份之后的 binlog 重放到误删前一秒。
思路如下:
- 从备份开始位置对应的 binlog 坐标开始。
- 用
mysqlbinlog把目标时间段的 binlog 解析为 SQL。 - 人工确认误删语句的位置,使用
--stop-position跳过误删之后的日志。 - 回放到临时实例或原实例之前的一刻。
这是一个标准流程,但实际执行非常依赖日志连续性、备份一致性和字符集设置。建议在系统上线前就准备好脚本,并记录好 binlog 的位置,不要在事故当天临时查文档。
8. 你以为没出错,其实已经错了:真实场景复盘
关系型数据库的“正确性”并不是总能写进教科书。在实际开发中,我遇到过不少看似数据没问题、实际已经错过的场景,挑几个有代表性的复盘。
8.1 并发扣减没有锁,库存变负数
最先踩到的坑就是并发 UPDATE 没有条件限制。很多人先查库存,业务代码里判断库存大于 0,再执行UPDATE stock SET stock = stock - 1 WHERE product_id = 1。两个请求同时查库存,都判断库存充足,都执行了扣减,最后库存变成了负数。
正确做法是把库存条件放进 UPDATE,或者在查询时加锁:
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1 AND stock > 0;这个语句能利用行锁保证并发下只有一个事务能扣到最后一单位库存。注意这里仍然要考虑事务必须在同一连接内提交,不能先查再自由延迟更新。
8.2 检查约束没有建,脏数据进了核心表
很多业务表的“状态”“金额”字段没有 CHECK 约束,只靠应用代码检查。一旦老系统、脚本、数据同步工具漏掉校验,脏数据就会进入表。关系型数据库的完整性机制给了我们自动兜底的能力,就应该用起来。
例如在 MySQL 8.0 中:
ALTER TABLE orders ADD CONSTRAINT chk_order_status CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'));这种约束能在数据库层挡住明显非法状态,比只在应用层判断更可靠。
8.3 事务执行时间过长,从库延迟、锁等待集中爆发
开发同学为了“保证一致性”,开启一个事务后,在事务里调用外部 HTTP 接口,整整几秒钟才提交。结果事务锁定的行一直被占用,业务高峰时大量请求排队,主库锁等待暴涨。因为长事务持有读视图,undo log 无法清理,从库也可能长时间落后。
不要为了理论上的“一致性”而把远程调用放进数据库事务。事务应该尽量短小,远程调用、文件读写、用户等待这些 IO 操作放到事务外面。
8.4 主从切换后丢失了“已提交”数据
主库压力大或磁盘耗尽时,线上经常选择把从库提升为主库。如果原主库上有事务已经提交,但 binlog 还没传到从库,这时将旧主库下线、从库强制上线,那部分数据就永久丢失。业务面板里看到的“下单成功”,实际上并没有同步到新的主库。
解决方式不是事后修改,而是架构选型。要绝对避免丢核心事务,就不能只用异步复制。至少要在订单、支付这类核心场景接半同步或组复制,或者在提交反馈链路中先行额外确认重试。没有一套真正一致性的架构规划,任何数据库都无法替你兜底。
9. 常见问题与排查方法
下面按经验整理一份排查清单,比较适合实际故障定位。
| 问题现象 | 可能原因 | 排查方式 | 解决思路 |
|---|---|---|---|
| 事务提交后重启,数据丢了 | 未设置innodb_flush_log_at_trx_commit=1,或主机异常掉电但日志未刷盘 | 查看参数配置,检查崩溃日志 | 关键业务使用最严格刷盘配置,性能从硬件层面优化 |
| 并发扣库存出现负数 | 缺少条件更新或行锁 | 分析 lock 监控,复现并发压测 | 把stock > 0放进 UPDATE;必要时用FOR UPDATE |
| 大量锁等待,业务超时 | 长事务、锁粒度太大、热点行竞争 | SHOW ENGINE INNODB STATUS查看锁信息,查information_schema.innodb_trx | 缩短事务、小事务拆分、降低热点冲突、低峰批量处理 |
| 主从延迟越来越大 | 大事务一次性修改太多行,从库单线程回放能力不足 | SHOW REPLICA STATUS观察延迟,查看 binlog 排行 | 拆分大事务,使用并行复制,优化从库磁盘性能 |
| 从库复制 SQL 线程停止 | 主从数据不一致,唯一键冲突或表结构不一致 | 查看错误日志,定位复制停止位置 | 在备份环境重建从库,或使用校验工具人工修补一致后重新开始 |
| 误删全表后无法恢复 | 没有全量备份,binlog 过期或未开启 | 检查备份策略和 binlog 保留时间 | 开启 binlog,做全量备份,并定期做恢复演练 |
| 启动时提示数据页损坏 | 磁盘坏块、内存故障、异常关机导致页损坏 | SHOW GLOBAL STATUS LIKE 'Innodb_page_errors' | 优先尝试从备份恢复,或使用doublewrite区域恢复,检查硬件健康度 |
| 更新操作影响行数比预期多得多 | SQL WHERE 条件写错,或未用唯一键定位 | 开启sql_safe_updates=1,先 SELECT 评估结果集 | 强制要求 UPDATE/DELETE 带条件,杜绝全表误改 |
10. 最佳实践:让数据库的可靠性真正为你服务
在掌握理论后,真正有价值的是把它落实到工程中。这里给一份可以放到团队内部规范里的最佳实践清单。
第一,在设计表结构时就用上约束。主键、唯一键、外键、CHECK、NOT NULL 这些功能不是摆设,而是数据库给你的第一层“不出错”保障。能用数据库约束解决的就不要完全依赖应用层判断。
第二,事务保持短、快、明确。事务只包裹必要的 SQL,不在事务中做远程调用和长时间计算。提交前先确认影响行数和预期是否一致,避免无谓等待。
第三,并发更新永远要记得条件与版本。凡是执行 UPDATE 都要想清楚:这条 SQL 在并发下是否安全。要么使用UPDATE ... WHERE stock > 0这种条件更新,要么使用version字段做乐观锁,要么使用SELECT ... FOR UPDATE做悲观锁定。
第四,不要把主从复制当成实时一致的工具。异步复制有延迟,“从库读到旧数据”不一定说明主库写失败。做读写分离时,对一致性要求高的场景应该强制走主库,或者等待安全返回。
第五,备份和恢复方案要包含“恢复演练”这个环节。备份文件永远放着但没恢复测试过,等到事故发生时才发现备份文件不完整或者恢复命令出错,代价远大于平时多演练一次。
第六,关注数据库监控指标。除了连接数、QPS、CPU,还要关注慢查询、复制延迟、临时表、redo log 刷盘频率、锁等待时长、页错误数。数据不出错通常不是某一个神奇功能带来的,而是监控能提前发现问题。
关系型数据库要真正保证数据不出错,从来都不是写对一条 SQL 这么简单。它靠的是从存储引擎到主从复制、从日志到备份的一个长链条。理解链条上的每一环,你才敢在高并发、多副本和复杂故障场景下,对线上数据更有把握。下一次写完事务,不妨先问自己:如果这条 SQL 执行到一半断电了,这个数据库有没有办法让我不会丢数据?答案越明确,你的系统就越可靠。