SQL 数据一致性保证实践

1次阅读

事务隔离级别、外键约束、update/delete条件校验及缓存一致性四层机制共同保障数据一致性,任一缺失都可能导致线上故障。

SQL 数据一致性保证实践

事务隔离级别选错,select 看不到刚 INSERT 的数据

不是代码写错了,是默认隔离级别在捣鬼。mysql 默认 REPEATABLE READpostgresql 默认 READ COMMITTED,而 sqlite 默认不开启 WAL 模式时连真正的 MVCC 都没有。同一事务内 SELECT 看不到自己刚 INSERT 的行?大概率是开了自动提交(autocommit=True)但没意识到——那每次 INSERT 都是独立事务,还没等你 SELECT,它就已经提交又“消失”在快照里了。

实操建议:

  • 确认驱动是否默认开启自动提交:Python 的 sqlite3 默认关,pymysql 默认开;Go 的 database/sql 默认关,但连接池可能重用已提交的连接状态
  • 显式控制事务边界:用 BEGIN / COMMIT 包住一组操作,别依赖隐式行为
  • 读已提交场景下,避免在 READ UNCOMMITTED 下做业务判断——脏读不是“快”,是不可靠

UPDATE ... WHERE 条件漏掉主键或唯一约束,导致意外更新多行

这是线上事故高频原因。比如想改某用户余额:UPDATE accounts SET balance = 100 WHERE user_id = 123,结果 user_id 字段没建索引,又或者传参时把 user_id 写成 id 导致条件恒真,整张表全被设成 100。

实操建议:

  • 所有 UPDATEDELETE 必须带 WHERE,且该条件字段必须有索引 + 唯一性保障(如主键、UNIQUE 约束)
  • 开发期加一层防护:在 ORM 层或 DAO 方法里强制校验 WHERE 子句是否包含主键字段,或用 LIMIT 1(MySQL 支持,PostgreSQL 需用 CTE 或子查询模拟)
  • 上线前用 EXPLAIN 看执行计划,确认 typeconsteq_ref,而不是 ALL

外键没开,级联更新/删除失效,应用层硬编码补逻辑

外键不是性能包袱,而是数据一致性的最低防线。关掉外键(比如 MySQL 的 FOREIGN_KEY_CHECKS=0),再用脚本批量导入数据,之后忘记恢复——后面所有 ON DELETE CASCADE 都形同虚设。更糟的是,应用层自己写“先删子表再删主表”的逻辑,结果并发时子表记录被其他请求插入,删主表就报错。

实操建议:

  • 建表时明确声明外键并指定动作:FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,别等出问题再补
  • 不要为“怕锁表”禁用外键——真正影响性能的是缺失索引,不是外键本身;确保外键列都有索引(MySQL 要求,PostgreSQL 推荐)
  • 迁移工具如 gh-ostpt-online-schema-change 默认保留外键,但需手动检查生成的 DDL 是否含 ADD CONSTRAint

应用层缓存和数据库不一致,cache-aside 模式下删缓存失败就彻底失联

查缓存 → 缓存未命中 → 查 DB → 写缓存,这流程看着稳,但只要中间一步失败(比如 DB 查到了,缓存写入超时),下次读到的就是旧值。更危险的是“先删缓存 → 再更新 DB”:删缓存成功,DB 更新失败,缓存空了,DB 还是旧数据,下次读直接穿透到旧值。

实操建议:

  • 删缓存操作必须和 DB 更新在同一事务中完成——做不到就用可靠消息队列兜底,比如更新 DB 后发一条 invalidate_user_123 消息,消费者重试直到缓存删除成功
  • 给缓存加短过期时间(如 60s),不追求强一致时,这是最简单有效的安全阀
  • 避免用哈希 key 拼接业务字段(如 "user:" + str(user_id)),一旦字段类型变(int → String)、格式变(加前缀)、分库分表规则变,key 就对不上,缓存永远不命中也不报错

数据一致性不是靠单点机制保证的,是隔离级别、约束、事务边界、缓存策略四层叠加的结果。少一层,漏一个配置,就可能在某个并发路径上崩掉。细节藏在 WHERE 条件有没有索引、FOREIGN_KEY_CHECKS 是开是关、缓存 DEL 命令有没有重试逻辑里——不是不知道,是上线前没盯住那一行。

text=ZqhQzanResources