ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数据库事务、隔离级别与索引优化:从原理到分布式实战

数据库事务、隔离级别与索引优化:从原理到分布式实战 关系型数据库的核心原理听着像是学院派才会关心的事但实际排查过线上问题的人都知道这些基础知识往往是最后救命的工具。最近我又被同事问了一圈事务注解为什么没回滚主键索引和唯一索引不都是“唯一”吗一个慢查询把库拖垮了用 explain 看半天也不知道索引到底哪里失效。这些问题的根全都落在事务、隔离级别和存储引擎这三块基石上。这篇文章会直接拆开它们顺带把分布式事务和 SQL 优化这条关联链路一起讲清楚。不管是日常开发、性能调优还是面试冲刺都能从这里拿到可复用的判断方法而不是零散的“八股文”背诵。1. 事务ACID 不是口号是每秒都在发生的博弈1.1 为什么需要事务很多人对事务的理解停留在“要么全部成功要么全部失败”这句话没错但远远不够。事务存在的真实原因是数据库在高并发和故障面前仍然需要保证业务数据的正确性。拿最经典的转账场景来说A 给 B 转 100 元第一步扣 A 的余额第二步加 B 的余额。如果第二步执行到一半数据库突然宕机没有事务的约束A 的钱已经少了B 的钱却没到账这 100 元就凭空消失了。事务的四个特性 ACID就是为了解决这一类问题。原子性保证转账的扣款和入账是一个不可分割的整体一致性保证转账前后总金额不变所有约束都成立隔离性保证两个并发事务互相干扰的程度是可控的持久性保证事务一旦提交即使数据库崩溃数据也不会丢。这四个特性不是并列关系原子性、隔离性和持久性本质上是手段最终目标是一致性。这个理解角度面试时讲出来会明显比死背定义高级。从工程上看事务还会直接影响到你的代码设计。比如一个接口里同时写了订单表和库存表的更新你选择在 DAO 层开启事务还是 Service 层开启事务锁的持有时间完全不同。Service 层事务会把整个业务逻辑包进去锁的粒度大、并发能力弱DAO 层事务则可能把多条 SQL 拆成多个事务中间一旦异常就出现部分成功。这些都是实际踩过的坑。1.2 redo log、undo log 与崩溃恢复理解了 ACID接着要问数据库到底靠什么实现事务答案不是靠一句“事务开启”那么简单的魔法而是靠 redo log、undo log 和缓冲池组成的完整链路。redo log重做日志负责持久性。InnoDB 的数据默认放在磁盘上但每次写都直接落盘性能太差所以先写进内存缓冲池Buffer Pool然后异步刷到磁盘。崩溃发生时内存里的脏页可能还没来得及刷盘数据就丢了。redo log 的 WALWrite-Ahead Logging机制解决了这个问题在修改数据页之前先把“我准备把某页某位置改成什么值”这个操作记录到 redo log并把 redo log 落盘。这样崩溃恢复时只要重新执行 redo log 就能把数据找回来。这也是为什么磁盘上会出现多个 ib_logfile 文件。undo log回滚日志负责原子性和 MVCC。事务里每一条修改操作都会被记录成反向操作比如 update 之前先把旧值写到 undo log。事务回滚时按照 undo log 反向执行就能恢复原状。更妙的是undo log 还保留了多版本数据链读事务可以根据版本链读到历史快照这是后面要讲的隔离级别的核心支撑。一个容易被忽略的实践点是参数 innodb_flush_log_at_trx_commit。值为 1 代表每次事务提交都强制刷盘最安全但性能压力大值为 2 代表每次提交只把 redo log 写到操作系统缓存每秒再刷盘性能好但数据库主机断电可能丢最多一秒的事务值为 0 是每秒刷一次盘最不保险。金融类业务必须用 1普通业务如果对性能敏感且能接受极小概率丢失可以用 2。这几个配置在面试里也经常被追问知道为什么比记住数字更重要。1.3 事务注解的失效现场Java 开发几乎每天都会碰到 Transactional。这个注解用起来简单但失效的坑特别多很多线上事故其实就是小花样。第一Spring 默认只对 RuntimeException 和 Error 回滚对受检异常Checked Exception不回滚。你写了一个 try-catch 把业务异常吞掉或者抛出自定义的业务异常但没有继承 RuntimeException事务就不会回滚。这是最常见的一坑。解决办法是 rollbackFor Exception.class或者更精准地指定业务异常类。第二同类内部的 this 调用会使事务失效。Spring 事务靠 AOP 代理实现如果你在同一个类里调用另一个标注了 Transactional 的方法那是 this 直接调用没经过代理对象增强逻辑完全失效。这个很难排查因为代码看起来明明标了注解。绕开方法要么注入自身对象要么把事务方法拆到另一个 Service 里。第三方法不是 public 的或者类没被 Spring 管理事务同样失效。数据库层面的 autocommit 如果被改成了开启状态也会让事务边界变得模糊。稍不注意明明开了事务每条 SQL 却各自提交了。我自己的习惯是事务注解只做粗粒度事务控制并且尽量用 REQUIRED 传播行为覆盖整个 Service 方法细粒度锁控制用编程式事务 TransactionTemplate这样边界清楚、异常路径透明。另外一定要在 Service 层开事务而不是在 Controller 或 DAO 层开否则事务作用域过窄或过宽都会带来维护成本。2. 隔离级别并发越高数据越容易“看花眼”2.1 四种隔离级别对照表事务隔离级别解决的是“多个事务同时执行时到底能看到什么”的问题。标准 SQL 定义了四种级别从宽松到严格依次是读未提交Read Uncommitted、读已提交Read Committed、可重复读Repeatable Read、串行化Serializable。隔离级别脏读不可重复读幻读锁特征读未提交可能可能可能基本不加读锁读已提交不会可能可能每次读一致快照可重复读不会不会可能InnoDB 下可避免事务开始时统一快照串行化不会不会不会完全串行锁冲突严重这张表只是标准定义。实际上 MySQL 的 InnoDB 在可重复读级别下通过 MVCC 和间隙锁把幻读也基本解决了所以很多人误以为 RR 就是安全的。但如果真的把隔离级别从 RC 调整到 RR业务并发性能会明显下降因为间隙锁会增多。2.2 脏读、不可重复读与幻读的真实业务场景脏读最容易理解事务 A 修改了一条数据但还没提交事务 B 读到了这个未提交的数据。之后 A 回滚了B 却拿着一个不存在的数据出去了。这在余额查询、报表统计里是致命的。不可重复读是同一事务内两次相同的查询得到不同的结果因为另一个事务在这期间提交了 update。比如一个事务先查订单金额做一些校验再查一次发现金额变了整个业务逻辑就乱套了。这也是把隔离级别从 RU 升到 RC 能解决的事。幻读更隐蔽事务 A 按条件查询得到 2 行数据事务 B 插入了满足同样条件的第 3 行并提交事务 A 再次查询发现多了一行。注意update 和 delete 在这类场景下也可能出现“当前读”的幻行问题。InnoDB 的 RR 隔离级别下普通的 SELECT 走快照读看不到新插入的行因此幻读被避免了但如果走 SELECT FOR UPDATE、UPDATE、DELETE 这种当前读仍然需要 next-key lock行锁 间隙锁来锁住范围防止其他事务插入。所以面试谈到 MySQL 的 RR 时一定要把“快照读避免了幻读当前读靠 next-key lock 防幻读”这个细节讲清楚。这是拉开差距的关键点。2.3 MySQL 默认 RR 到底怎么工作的MVCCMySQL 的默认隔离级别是 RR很多人不理解为什么不是 RC。核心原因是历史兼容性但背后真正支撑它的是 MVCC多版本并发控制。MVCC 简单说就是“读写不冲突”。每行数据在 update 时不会直接覆盖旧值而是新建一个版本旧版本保留在 undo log 的版本链里。每个事务在第一次读的时候会生成一个 ReadView记录当前活跃事务列表。之后读数据时通过版本链判断哪个版本对当前事务可见——只有创建版本的事务已经提交、并且版本比 ReadView 早的数据才可见。这样 RR 下同一个事务内无论 SELECT 执行多少次看到的都是第一次读时生成的快照天然避免了不可重复读。而 RC 每次 SELECT 都会生成新的 ReadView所以第二次读能看到别的已提交事务的修改。一个常见的困惑是既然 RR 都保持一致性快照了为什么还会有死锁因为 UPDATE 和 SELECT FOR UPDATE 是当前读它们直接读最新数据并加锁。RR 下为了防幻读会在范围查询加间隙锁两个事务各自锁住范围的不同间隙再互相申请对方范围内的记录锁就死锁了。这也是很多数据库团队把隔离级别从 RR 降到 RC 的原因因为 RC 基本只加行锁间隙锁少死锁概率大幅下降。2.4 隔离级别怎么选选择隔离级别不是越严格越好关键看业务容忍度。读未提交基本不会用在正式环境除非是某些极端的“写多读少且读结果永远允许过期”的统计任务我一般不建议使用。读已提交适合大多数互联网业务能保证读到已提交数据并发好死锁少。可重复读适合需要事务内多次读取结果必须一致的场景比如对账、统计、复式计算。串行化几乎只用在资金类、极强一致性以及数据量小、并发极低的场景。实践中如果要改 MySQL 默认级别先要确认主从复制模式。在老版本基于 statement 的 binlog 复制下RC 会因为无法像 RR 那样保证日志执行顺序一致而导致主从数据不一致这可能是 MySQL 一直保留 RR 的官方原因之一。到了基于 row 的 binlog现在默认就是 rowRC 基本没问题了。所以不要裸改要做完整测试。3. 存储引擎InnoDB 凭什么能“通吃”3.1 先搞清楚存储引擎是什么存储引擎是 MySQL 中负责“数据怎么存、怎么取、怎么保证可靠性”的组件。表级别选择存储引擎也是 MySQL 和很多商业数据库最大的差异之一。你把不同的引擎理解为不同的“文件柜”InnoDB 是一个带抽屉锁、带日志、自带崩溃恢复功能的保险柜MyISAM 是一个轻便但容易损坏的文件架MEMORY 则是一个断电就清空的临时板子。查看当前默认引擎和表引擎的命令很简单SHOW VARIABLES LIKE default_storage_engine; SHOW TABLE STATUS LIKE your_table\G;日常开发基本只关心 InnoDB但面试题如果没有明确限定“MySQL 的存储引擎有哪些”这种问题就需要把 InnoDB、MyISAM、MEMORY以及 NDB、Archive 这些也提一句才算完整。3.2 InnoDB 的底层设计InnoDB 能成为默认引擎核心理由有三个支持事务、支持崩溃恢复、支持行级锁。但它的底层设计还有更多值得深挖的东西。最关键的是聚簇索引。InnoDB 的表数据本身就是按主键索引组织的主键索引的叶子节点直接存整行数据所以查询走主键索引时一次索引查找就能拿回全部列。二级索引普通索引的叶子节点保存的是主键值所以走二级索引查数据需要先找到主键再回主键索引取整行这个过程叫回表。这也是为什么主键设计得越小越好因为所有二级索引都要冗余主键值主键太长会让每个索引叶子节点变大占用更多磁盘和内存。缓冲池和变更缓冲也很重要。写操作不会立刻改磁盘而是先改 Buffer Pool 里的页再异步刷盘。对于二级索引上的随机插入操作InnoDB 不会每次更新索引页都去写一次磁盘而是先记录在 Change Buffer 中等页被读到或后台线程聚合后合并。这个机制的收益非常大能显著减少随机写放大特别是在“插入但长期不查”的二级索引场景下。奔溃恢复依赖 redo log事务回滚和多版本依赖 undo log。锁机制则包括记录锁、间隙锁、next-key lock、意向锁。这些机制共同保证了在高并发写入下InnoDB 依然能保持“不出错、可恢复、可并发”。3.3 MyISAM、MEMORY 等引擎一览MyISAM 在旧时代很流行典型特征是不支持事务、不支持外键锁粒度是表锁写入并发差但读性能曾经被认为快。实际上它也有一个优点支持 FULLTEXT 全文索引InnoDB 直到 MySQL 5.6 才开始支持全文索引。MyISAM 的崩溃恢复能力很差因为写操作只更新内存数据并定期刷盘没有 redo log 机制一旦机器断电表很容易损坏。这也是我在生产环境从不建议保留 MyISAM 表的原因就算读多写少也完全可以用 InnoDB 配合只读实例来扛。MEMORY 引擎把表存在内存里查询速度极快但服务器重启数据全没通常用来做临时中间表不适合长期数据。Archive 引擎只支持插入和查询压缩率高适合日志归档。NDB 是集群引擎配置复杂一般不在常规业务里使用。一个实用技巧验证一个表是不是 InnoDB直接 show table status。如果 engine 不是 InnoDB优先考虑迁移。迁移可以用一条语句ALTER TABLE old_table ENGINEInnoDB;但注意大表会锁表过程较长建议在低峰期执行或者通过新建表 拷贝的方式平滑切换。3.4 选型与迁移建议特性InnoDBMyISAMMEMORY事务支持支持不支持不支持锁粒度行锁 间隙锁表锁表锁崩溃恢复redo log 可靠恢复容易损坏重启即丢失外键支持不支持不支持MVCC支持不支持不支持全文索引5.6 起支持支持不支持适合场景绝大多数 OLTP只读、表小、无并发写临时表、缓存选型时我基本只用 InnoDB唯一的例外是如果线上还有全文检索需求且不想引入 Elasticsearch可以临时用 MyISAM 建一个只读副本或者改用 InnoDB 的 FULLTEXT 特性。从运维角度看引擎统一还能减少备份恢复工具链的差异省不少事。4. 索引原理与 SQL 优化那些面试必问的“送命题”4.1 主键索引和唯一索引的区别“主键索引和唯一索引不都是唯一吗”这是面试高频题也是很多开发容易混淆的点。首先主键索引是一种特殊的唯一索引但它在 InnoDB 里的地位完全不同。主键是聚簇索引的锚点整张表的数据就是按主键排序存储的。没有显式主键时InnoDB 会选一个非空的唯一索引作为隐式主键如果都没有会生成一个隐藏的 rowid。所以从存储结构上看主键索引叶子节点存的是整行数据唯一索引叶子节点存的是主键值。其次唯一索引允许存在多个 NULL 值因为 NULL 在 MySQL 里代表“未知”多个未知并不违反唯一性。主键索引不允许 NULL因为它必须能标识每一行。还有一点需要理解唯一索引还承担着约束职责。插入数据时唯一索引需要检查冲突因此会有额外的唯一性校验开销。主键索引的这种检查同样有但更多是存储层面的必选项。实践建议是主键尽量用自增 ID 或有序的 UUID。自增主键写入时是纯顺序插入不会频繁触发页分裂随机 UUID 作为主键会导致页分裂和碎片性能和空间都很差。如果一定要用 UUID可以改成类似 UUID 转有序字符串或者使用雪花算法生成的分布式 ID。4.2 哪些场景会导致索引失效索引失效是慢 SQL 的重灾区。我这里列出的每一个场景都是我实际在慢查询日志里看到过的。一、对索引列使用函数或计算。比如 where YEAR(create_time)2024会让 create_time 索引失效因为要计算完才能比较。改成 create_time 2024-01-01 AND create_time 2025-01-01就能走索引。二、隐式类型转换。字符串字段 where mobile138xxxxxxx如果 mobile 是 varchar但参数是数字MySQL 会先把字段转换成数字再比较索引就失效。解决办法是参数也写成字符串。三、LIKE 前置模糊。where name like %张 没法走索引因为前缀未知。如果业务必须用这类模糊搜索优先考虑全文索引或外部搜索引擎。四、联合索引不满足最左前缀。比如联合索引 (a,b,c)直接查 b 或 c 就无法使用该索引。这是通用规则必须遵守。这里也常考如果查询条件是 where a between ... and ... and b?那 b 上索引到底能不能用答案是 a 范围右侧的列无法完全利用需要结合执行计划具体判断。五、OR 连接时如果其中一个条件没有索引整个表达式可能全表扫描。优先使用 UNION 或为 OR 涉及的每个列分别建索引。全域优化器也可能选择扫描所以尽量写成 UNION 或增加选择性。六、对索引列做负数条件。!、NOT IN、NOT LIKE 往往会让优化器放弃索引。但这不绝对覆盖索引时可以走索引扫描。关键是看 selectivity 和 cost。还有两个容易被误解的点is null 不一定失效在 MySQL 8.0 里is null 可以走索引但允许 NULL 的列上看重复率索引列参与运算比如 id15也会失效因为计算后索引键不再匹配原始索引。修复思路是打开 EXPLAIN看 key 列是否为 NULL看 type 是否从 ref 变成了 ALL。一旦发现索引失效优先重写 SQL而不是盲目加索引。4.3 EXPLAIN 优化实操执行计划是 SQL 优化的第一工具。面试官问“说一些 SQL 上面的优化”不是要让背几条口诀而是想听你怎么定位问题、怎么分析。最好的回答方式就是讲 EXPLAIN。EXPLAIN SELECT order_id, amount FROM orders WHERE user_id 123 AND status 1 ORDER BY create_time DESC LIMIT 10;重点看四列type常见从好到坏是 system const eq_ref ref range index ALL。出现 ALL 就要警惕全表扫描。key实际使用的索引名为 NULL 表示没走索引。rows预估扫描行数越小越好。ExtraUsing index 表示覆盖索引Using filesort 表示排序没有用到索引Using temporary 表示用了临时表需要重点关注。一个经典慢 SQL 是深分页offset 很大时MySQL 要扫描并丢弃前面的行。比如 limit 100000,20优化方式是把 offset 改成基于主键定位SELECT * FROM orders WHERE order_id last_order_id ORDER BY order_id LIMIT 20;这样每次只扫 20 条。还有尽量不要 SELECT *只取需要的列这样更可能使用覆盖索引减少回表。批量插入数据时用一条 INSERT 语句插入多行或者用 LOAD DATA比循环单条 INSERT 快得多。排序时如果 ORDER BY 的字段不在索引里就会 filesort避免办法是让排序字段成为联合索引的一部分或者降低返回数据量。SQL 优化永远要结合具体数据分布。数据量千万级和十万级的优化路径完全不同不同索引选择性差异也很大。所以一定要先 EXPLAIN再手工验证这才是面试官想听到的“理性框架”。5. 分布式事务单库事务解决不了的问题怎么解5.1 跨库事务为什么这么难当业务拆分为微服务后订单服务维护订单表库存服务维护库存表两个服务通常使用不同的数据库。单库事务的 ACID 只能保证在一个数据库实例内跨库之后不再有统一的 redo log 和全局事务管理器所以经典事务直接失效。拿下单流程举例先扣库存再创建订单。如果扣库存成功创建订单失败但是两个操作各自发生在自己的本地事务里数据库层面并不感知对方是否成功。这时需要分布式事务来保证最终一致性。但根据 CAP在网络分区时分布式系统必须在一致性和可用性之间权衡。强一致的分布式事务代价非常高互联网业务普遍采用“最终一致性 补偿”的思路。这里面还有一个容易混淆的概念“分布式事务一致性”和“单库事务隔离级别”不是一回事。分布式事务更关心的是跨服务、跨库状态怎么对齐通常讨论最终一致性而隔离级别讨论的是在一个数据库内并发事务的可见性。5.2 常见方案2PC、TCC、消息事务、SAGA分布式的第一反应是 2PC两阶段提交。它有一个协调者第一阶段问所有参与者“能提交吗”第二阶段广播“提交”或“回滚”。问题是协调者单点、同步阻塞、参与者资源锁住时间长性能很差不适合高并发场景。真正在公司里自研 2PC 的很少更多是用 Seata 这种 AT 模式本质也是 2PC 的改进版但侵入小。TCC 是业务层面的补偿方案Try 阶段预留资源Confirm 阶段确认执行Cancel 阶段补偿回滚。比如扣库存前先检查预扣然后锁定库存确认时扣减取消时释放。TCC 的优点是性能好、不长期占用资源缺点是需要为每个业务写三套逻辑开发成本高。可靠消息事务是目前最常用的“最终一致性”方案比如 RocketMQ 事务消息。它的思路是把“业务操作”和“发送消息”放到同一个本地事务里。具体流程是发送 half message半消息执行本地事务本地事务成功后向 Broker 发送 commitBroker 才会真正让消费者看到消息本地事务失败则 rollback消息删除。如果半消息一直没确认Broker 会回查本地事务状态做到最终一致。这样订单服务只要保证本地订单创建和消息发送同成功、同失败库存服务消费消息扣减库存即可。缺点是有消息延迟不适合要求实时一致的系统。SAGA 是一个长事务拆成一串本地事务每个步骤记录正反操作按顺序执行失败则反向补偿。适合流程很多、节点多样的场景比如旅游预订。它没有全局锁性能好但补偿设计复杂业务语义散落在代码里。5.3 订单与库存场景拆解我用一个最典型的“订单与库存分布式事务”给你捋一遍假设下单接口先调订单服务创建订单再调库存服务扣库存。最直接的做法是订单本地事务执行 insert order 和 send MQ库存服务消费 MQ 执行扣库存。这里如果扣库存失败订单已经创建了怎么处理方案一是订单服务发一条补偿消息系统对订单做“已取消”处理同时库存回补方案二是库存服务先做预扣如果库存不足直接回查订单服务取消订单。实际生产上我会优先选择 RocketMQ 事务消息因为对业务入侵最小最终一致性目标清晰。流程是订单服务发送事务消息半消息。执行本地事务创建订单状态置为待支付。向 Broker 提交 commit。库存服务消费消息执行扣库存。如果库存扣减失败返回消费失败消息会被重试或进入死信队列需要报警和人工处理。这套方案里最关键的不是消息中间件而是“消息状态”和“业务状态”的对齐。比如本地事务成功但 commit 消息失败Broker 会通过事务回查接口确认订单是否存在以此决定消息是否放行。回查接口必须幂等否则会造成重复扣减。还有一个容易忽略的问题消费端一定要做幂等。消息可能被重复投递库存扣减必须能重入比如用订单号加唯一约束或者用 Redis 记录已完成消息 ID。不做幂等最终一致性的链条迟早会在某一次重试时崩掉。6. 面试题速查表先背下来再讲出原理6.1 高频问题清单与回答骨架很多后台开发准备 Java 技术面试时都会遇到事务、存储引擎、索引堆一起的连环问。我整理一张速查表每个问题给出回答骨架方便快速过。问题回答骨架MySQL 的存储引擎有哪些InnoDB 支持事务、行锁、崩溃恢复默认MyISAM 不支持事务、表锁、无恢复MEMORY 内存表根据业务选型。事务隔离级别有哪些RU、RC、RR、Serializable解释脏读、不可重复读、幻读MySQL InnoDB 默认 RRMVCC 实现 RR 快照读。事务注解失效的场景默认只回滚 RuntimeException类内部 this 调用非 public 方法异常被吞掉传播行为配置错误。主键索引和唯一索引的区别主键非空、聚簇索引锚点、叶子存整行唯一索引允许 NULL、普通二级索引、叶子存主键。哪些场景会导致索引失效隐式类型转换函数计算LIKE 前置模糊联合索引不满足最左前缀OR 含非索引列不等值条件。分布式事务一致性怎么答单库事务跨库失效CAP 取舍2PC 强一致但性能差TCC 侵入事务消息最终一致性SAGA 长流程补偿。说一些 SQL 上面的优化EXPLAIN 定位慢 SQL避免全表扫描覆盖索引、避免深分页、减少回表合理使用联合索引批量插入代替循环插入。这张表是骨架面试时不能只背一句结论。每个问题都要带一个真实场景做佐证比如“我上次在 XX 表发现主键用随机 UUID 导致页分裂后来改成有序 ID插入性能提了 X 倍”这种细节瞬间拉开与背书党的差距。6.2 怎么讲才不像背书我发现很多候选人不是不懂而是讲得太“平”全是定义没有推导。面试官问隔离级别你说“可重复读是事务开始后多次读取相同结果”不会加分。但如果你从 MVCC 的 ReadView、undo log 版本链、当前读和快照读的区分讲起再举例说明 RR 下为什么 SELECT 不会幻读但 UPDATE 可能锁范围面试官马上就知道你是真懂。回答面试题的正确顺序是结论优先 → 原理展开 → 场景举例 → 坑点提醒。比如“事务注解为什么会失效”先说“Spring 通过 AOP 代理实现只有外部调用才能被拦截”再说“受检异常默认不回滚”给一个 try-catch 吞异常的例子最后提一下同类调用问题。每一点都落到实际代码上就很自然。如果被追问“分布式事务怎么实现”也不要在一开始硬凹 2PC。先判断系统是强一致还是最终一致再给出方案对比最后落到订单与库存这个具体场景讲消息事务的半消息和回查机制。这样整个回答逻辑是活的不是在背模板。分布式事务面试中的最后一个提醒关于分布式事务我一直有一个体会能用消息事务就别上 TCC能用 TCC 就别上自研 2PC。越重的方案开发和运维成本越大。一个秒杀场景要求库存实时性高那就不能用消息延迟最终一致反而应该在库存服务本地做预扣然后通过 TCC 或 Seata 管理跨服务事务一个普通下单场景能容忍几秒延迟消息事务就够了。关键是先把业务容忍度搞清楚再去选技术。而在单机数据库这一侧我的经验是把所有表统一用 InnoDB把事务隔离级别理解清楚把索引设计前置到表结构评审阶段绝大多数慢 SQL 和一致性事故都可以提前规避。那些看起来很高深的技术名词剥开之后其实都是围绕“怎么不让数据错、怎么不让系统崩、怎么让并发尽量高”这三个问题展开。方向对了细节学起来就不会散。最后再分享一个从实际故障里练出来的小技巧每次写事务相关代码强制自己写清楚事务边界、异常路径和幂等策略。线上问题往往不是某个原理没学过而是边界定义模糊、异常处理随意。把这些基础动作做到位比收藏任何一份“数据库面试题总结”都管用。
返回列表