ARTICLE DETAIL

资讯详情

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

事务与锁机制全解:从InnoDB隔离级别到分布式锁实战

事务与锁机制全解:从InnoDB隔离级别到分布式锁实战 从事数据库开发或者后端开发的同学几乎都会在某个阶段被事务和锁折磨得头皮发麻。你可能遇到过这种情况一个批量更新接口偶尔报死锁一个高并发下单场景出现超卖一个事务里嵌套了远程调用导致连接池被打满。这些问题表面上千奇百怪但根子都指向同一个东西——事务的隔离级别、锁的粒度和释放时机。我自己早年排查一个线上死锁问题查了两天才发现是两条 SQL 的加锁顺序跟索引选择不一致导致的从那以后我就把事务和锁的底层机制彻底啃了一遍。这篇内容就是基于我这几年的实战梳理把事务、隔离级别、锁、MVCC 以及分布式场景下的事务与锁方案一次性讲透适合刚入门想建立体系的后端开发也适合被线上并发问题折磨到想摔键盘的兄弟们。很多人对事务和锁的理解停留在“事务保证原子性锁保证并发安全”这种口号层面真到用的时候连SELECT ... FOR UPDATE在什么情况下会锁全表都说不清楚。我写这篇文章的目标很简单把每个概念都落到能看懂、能上手、能排查问题的程度而不是给你堆一堆教科书定义。1. 事务到底在解决什么问题1.1 从一个转账场景说起聊事务之前建议先想清楚一个问题事务解决的本质矛盾是什么是“多个操作要么全部成功、要么全部失败”的一致性诉求。拿最经典的转账场景来说A 账户扣 100 块B 账户加 100 块这两个操作必须绑在一起。如果扣款成功但加款失败钱凭空消失了这无论是业务上还是账务上都是不能接受的。但这里有个经常被忽略的细节事务不只是“能回滚”这么简单。它还要处理并发场景下的相互干扰。比如 A 扣款的同时另一个事务正在读 A 的余额它看到的是扣款前的余额还是扣款后的余额再比如两个事务同时给同一个账户加钱最终结果会不会丢更新这些问题的答案取决于事务的隔离级别和锁机制怎么配合。我用一个更贴近实际的例子来说明。电商下单场景里一个订单涉及创建订单记录、扣减库存、锁定优惠券、更新用户积分等多个操作。如果这些操作不在同一个事务里扣库存成功了但创建订单失败库存就凭空少了如果都在一个事务里但事务隔离级别设置不当两个用户同时买最后一件商品就可能出现都看到库存为 1、都扣减成功的情况。所以事务的价值不是某个单一特性而是四个特性共同作用的结果原子性保证操作不可分割一致性保证数据状态合法隔离性保证并发事务互不干扰持久性保证提交后数据不丢失。这就是常说的 ACID。1.2 ACID 到底是怎么落地的四兄弟里面大家最容易搞混的是“一致性”和另外三者的关系。我个人的理解是一致性是目的原子性、隔离性、持久性是手段。也就是说数据库通过 undo log 实现原子性回滚到操作前的状态通过锁和 MVCC 实现隔离性让并发事务看起来像是串行执行的通过 redo log 实现持久性崩溃后能恢复最终共同服务于“数据状态始终合法”这个一致性的目标。这里有一个重要的认知一致性不完全是数据库的事业务逻辑也要参与。数据库能保证的是约束、外键、触发器等层面的完整性但像“转账金额不能为负”“下单数量必须大于 0”这类业务规则需要开发者在事务里自己判断。我见过不少团队把一致性完全寄托在数据库上结果业务代码里先扣库存再判断库存不足回滚倒是回滚了但接口响应慢得离谱因为整个事务持有锁的时间太长了。持久性这块InnoDB 采用的是 WALWrite-Ahead Logging机制先写 redo log 再刷数据页。这样即使数据库崩溃重启后也能通过 redo log 恢复已经提交的事务。但这里有个参数值得关注innodb_flush_log_at_trx_commit。默认值是 1表示每次事务提交都把 redo log 刷到磁盘安全性最高但性能最差改成 2 表示每秒刷一次性能好很多但极端情况下可能丢 1 秒的数据改成 0 则是交给操作系统调度性能最好但风险最大。怎么选取决于业务对数据丢失的容忍度不是无脑用默认值就万事大吉。1.3 事务的边界什么时候该用什么时候不该用这是一个实操中容易走极端的问题。有的团队信奉“万物皆事务”一个接口里从查用户到发消息全塞进一个事务有的团队则被事务坑怕了能不用就不用。我的经验是事务的边界应该等于“业务上不可分割的操作集合”而不是“接口的代码范围”。判断标准很简单——如果在事务执行过程中某个操作失败了前面的操作是否需要全部撤销需要就放进同一个事务不需要就拆开。比如订单创建和发送通知短信通知短信发失败不应该让订单创建回滚所以不应该放在同一个事务里。但如果你把这两个操作硬塞进一个事务短信接口超时会导致数据库事务长时间不提交连接被占用锁被持有最终拖垮整个数据库连接池。这个问题在微服务架构下尤其常见我后面在分布式事务的部分还会详细展开。2. 隔离级别事务之间的相处规则2.1 四种隔离级别怎么选SQL 标准定义了四种隔离级别读未提交Read Uncommitted、读已提交Read Committed、可重复读Repeatable Read、串行化Serializable。它们解决的是三个并发问题脏读、不可重复读、幻读。脏读读到另一个事务未提交的数据。这个数据可能被回滚所以你读到的是“不存在过”的数据。不可重复读同一个事务内两次读取同一行数据结果不一样。原因是另一个事务在这期间提交了更新。幻读同一个事务内两次执行相同的范围查询返回的行数不一样。原因是另一个事务在这期间插入或删除了符合条件的数据。MySQL 的 InnoDB 默认隔离级别是 Repeatable Read但它在 RR 级别下通过间隙锁和 MVCC 很大程度上规避了幻读问题。Oracle 和 PostgreSQL 的默认级别则是 Read Committed。这几个默认值的差异导致很多从 Oracle 转 MySQL 的同学一开始会踩坑。很多人问到底该用哪个隔离级别我的看法是绝大多数业务场景Read Committed 和 Repeatable Read 都够用关键看你对“同一事务内多次读取结果一致”有没有硬性要求。比如统计报表类的事务可能会多次读取同一张汇总表做累加计算这时候 RR 更合适能保证快照读的一致性而像订单状态流转这种单行更新场景RC 就够了锁冲突还更少。2.2 隔离级别背后的实现原理这四种级别不是凭空设计的它们本质上是“加锁强度”和“读取版本策略”的不同组合。隔离级别越严格并发能力越差数据一致性越强。串行化级别下事务完全串行执行不会出现任何并发问题但性能最差实际生产环境很少用。InnoDB 里隔离级别的差异主要体现在读操作上。普通的SELECT是快照读MVCC不加锁读的是历史版本数据SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE、DELETE是当前读必须加锁读的是最新已提交数据。在 RC 级别下每条语句执行时都会生成一个新的 ReadView所以两次查询可能看到不同版本的数据在 RR 级别下事务内第一次执行快照读时生成 ReadView之后整个事务都复用这个 ReadView所以快照读的结果始终一致。这就是为什么 RR 能解决不可重复读的原因。但要注意快照读的一致性不等于当前读的一致性。在 RR 级别下快照读避免了幻读但如果事务里先做了快照读再执行当前读比如UPDATE仍然可能被其他事务插入的新数据影响。处理这种问题通常需要手动加锁比如用SELECT ... FOR UPDATE把范围锁住。2.3 一个容易翻车的操作事务内的长事务聊隔离级别的时候必须顺带说一个实操中经常出问题的情况——长事务。一个事务开启后长时间不提交会带来三个后果持有锁的时间变长阻塞其他事务undo log 无法清理导致回滚段膨胀binlog 和 redo log 的写入压力增大。我遇到过最离谱的一个案例业务代码里在事务中间调用了第三方支付接口支付接口超时重试了 3 次每次 30 秒等于这个事务持锁 90 秒以上。期间所有操作同一张表的请求全部卡住数据库线程数被打满最终引起雪崩。排查的时候发现information_schema.innodb_trx表里有一个事务的trx_started时间已经超过了两分钟。所以排查长事务一定要学会用这张表SELECT * FROM information_schema.innodb_trx;重点看trx_started、trx_rows_locked、trx_query这几个字段能快速定位是哪个事务持锁时间过长、锁了多少行、最后执行的 SQL 是什么。生产环境建议加个定时任务把超过阈值的长事务告警出来而不是等出了事故再查。3. 锁数据库并发控制的核心武器3.1 锁的分类与兼容矩阵InnoDB 的锁可以从两个维度分类一个是锁的粒度一个是锁的模式。粒度上分为表级锁和行级锁模式上分为共享锁S 锁和排他锁X 锁。共享锁之间兼容共享锁与排他锁互斥排他锁之间互斥。理解这个兼容矩阵是分析死锁的基础。用一个简单的表格来对比锁类型共享锁S排他锁X共享锁S兼容互斥排他锁X互斥互斥共享锁允许其他事务继续加共享锁读但不允许其他事务修改数据。对应 SQLSELECT ... LOCK IN SHARE MODEMySQL 8.0 之后推荐用SELECT ... FOR SHARE。排他锁只允许当前事务读写该行其他事务既不能加共享锁也不能加排他锁。对应 SQLSELECT ... FOR UPDATE、UPDATE、DELETE。实际操作中UPDATE和DELETE会自动对涉及的行加排他锁INSERT则通过隐式锁机制处理这里先不展开。需要注意的是SELECT默认不加锁走的是 MVCC 快照读只有显式加锁或者处于串行化隔离级别时才会走当前读。表级锁在 InnoDB 里也有但主要用于 DDL 操作或者LOCK TABLES这类显式命令。日常开发里最需要关注的是意向锁Intention Lock它是在事务对某行加锁之前自动在表级别打上的标记用来快速判断表上是否有锁冲突避免逐个检查行锁。意向锁分为意向共享锁IS和意向排他锁IX它们之间是兼容的但跟表级别的 S 锁和 X 锁互斥。3.2 行锁、间隙锁与临键锁的细节这是整个锁机制里最容易出问题、也最值得细说的地方。InnoDB 的行锁在 RR 隔离级别下有三种形态记录锁Record Lock、间隙锁Gap Lock、临键锁Next-Key Lock。记录锁锁住索引记录本身是最基础的行锁。间隙锁锁住两个索引记录之间的间隙防止其他事务在这个间隙里插入新记录。它是解决幻读的关键。临键锁记录锁 间隙锁的组合锁住一个索引记录及其前面的间隙区间。举个例子假设某张表的id列有主键索引现有数据 id 为 1、5、10。如果事务 A 执行SELECT * FROM t WHERE id BETWEEN 5 AND 10 FOR UPDATE在 RR 级别下它不仅会锁住 id5 和 id10 的记录还会锁住 (5,10) 这个间隙防止其他事务插入 id 为 6、7、8、9 的新记录。如果没有这把间隙锁其他事务插入一条 id7 的记录并提交事务 A 再次执行同样的查询就会发现多了一条数据——这就是幻读。间隙锁有几个容易踩的坑第一间隙锁只在 RR 隔离级别下生效。RC 级别下只会加记录锁不会加间隙锁这也是为什么 RC 级别出现幻读的原因。第二间隙锁锁的是间隙不是记录本身所以不同事务可以同时持有互相重叠的间隙锁吗不可以间隙锁之间是互斥的。这就导致了一个常见的死锁场景两个事务都执行了范围查询并在相同间隙上加锁然后各自尝试插入数据互相等待对方释放间隙锁。第三间隙锁跟索引选择有直接关系。如果查询条件没有命中索引InnoDB 会对整个表的所有间隙加锁相当于把全表都锁住了。这是“一条 SQL 锁全表”的根本原因之一。我见过有人执行一个不带索引条件的UPDATE在 RR 级别下直接导致全表数据无法插入和更新从问题发生到恢复用了将近十分钟。3.3 死锁的产生与排查死锁的本质是循环等待。两个事务各自持有一个锁同时又在等对方手里的锁谁都释放不了就形成了死锁。InnoDB 会通过死锁检测机制自动回滚其中一个事务来打破死锁被回滚的事务会收到类似Deadlock found when trying to get lock; try restarting transaction的错误。最常见的死锁场景有两种。场景一两个事务以不同的顺序加锁。事务 A 先锁 id1 再锁 id2事务 B 先锁 id2 再锁 id1。如果两个事务同时执行A 持有 id1 的锁等 id2B 持有 id2 的锁等 id1死锁产生。解决方案是约定所有事务按相同的顺序访问资源比如总是先锁 id 小的记录。场景二范围查询导致间隙锁交叉。事务 A 执行DELETE FROM t WHERE id BETWEEN 1 AND 10事务 B 执行INSERT INTO t (id) VALUES (5)A 持有间隙锁等 B 释放插入意向锁B 等 A 释放间隙锁死锁产生。排查死锁我通常用两步。第一步打开死锁日志SHOW ENGINE INNODB STATUS;看LATEST DETECTED DEADLOCK部分里面会明确写出两个事务各自持有的锁、等待的锁以及对应的 SQL。第二步根据日志里的 SQL 分析加锁顺序和锁范围然后调整代码或者索引。这里给一个实操建议死锁日志里的 SQL 往往只显示语句不显示事务上下文所以一定要在日志里找到trx id和事务开始时间再结合应用日志去还原整个事务的操作序列。死锁不是 bug它是数据库并发控制下的正常现象。处理死锁的正确思路不是“消灭死锁”而是“降低死锁概率 代码层面重试”。我一般会在事务冲突比较频繁的接口里加一个简单的重试机制捕获死锁错误后随机延迟 10~50 毫秒再重试整个事务重试 2~3 次成功率非常高。4. MVCC读写不阻塞的并发利器4.1 undo log 与版本链MVCC 是 InnoDB 实现高并发读的核心机制全称是 Multi-Version Concurrency Control多版本并发控制。它的核心思路是写操作加锁读操作不加锁读写之间不互相阻塞。这是怎么做到的靠的就是“多版本”。每一行数据在 InnoDB 里都有两个隐藏列trx_id最近修改该行的事务 ID和roll_pointer指向上一个版本的指针。每次 UPDATE 操作不会直接覆盖旧数据而是生成一个新版本的行记录旧版本通过roll_pointer串成一条版本链链的头部是最新版本越往链尾走版本越旧。这些历史版本存在 undo log 里所以 undo log 除了支持事务回滚还承担了 MVCC 版本链的存储职责。举个具体的例子假设 id1 的这行数据初始余额是 100事务 A 把它改成 80但还没提交。此时这条记录的版本链是版本 2余额 80trx_idA→ 版本 1余额 100trx_id之前的某个事务。如果事务 B 此时执行SELECT它可以根据自己的 ReadView 判断自己能不能看到版本 2。如果看不到就顺着版本链往下找找到自己能看到的那个版本。这就是快照读。4.2 ReadView 的生成规则ReadView 是 MVCC 判断“某个版本对当前事务是否可见”的依据里面记录了事务系统里活跃事务的 ID 列表trx_list、最小活跃事务 IDup_limit_id和下一个待分配的事务 IDlow_limit_id。判断规则可以简化为如果版本的trx_id小于up_limit_id说明该事务已经提交当前事务可以看到这个版本。如果版本的trx_id大于等于low_limit_id说明该事务在 ReadView 生成之后才开启当前事务看不到这个版本。如果版本的trx_id在up_limit_id和low_limit_id之间则看它是否在活跃事务列表里如果在说明还未提交不可见如果不在说明已提交可见。这里就是 RC 和 RR 最大的区别所在。RC 级别下每条 SQL 执行时都生成新的 ReadView所以事务内的两条相同查询可能基于不同的 ReadView 读到不同的数据版本产生不可重复读。RR 级别下事务内第一条 SQL 生成 ReadView 后整个事务都复用所以后续查询只能看到事务开始之前已提交的数据版本实现了可重复读。MVCC 在带来高并发读性能的同时也有一个容易被忽略的坑长事务会让 undo log 不断膨胀。因为事务没结束它依赖的旧版本数据就不能被清理版本链越拉越长undo log 越来越大最终可能导致磁盘空间暴涨甚至拖慢所有查询。这也是前面强调“避免长事务”的原因之一现在你可以理解为什么它影响面那么大了。5. 从单机到分布式事务与锁的延伸5.1 分布式事务的常见方案当数据分布在多个数据库实例或者多个微服务里单机事务的 ACID 就撑不住了。你不能指望一个全局事务管理器去统一控制所有数据库的提交和回滚所以分布式事务应运而生。常见的方案有2PC两阶段提交、TCCTry-Confirm-Cancel、SAGA、本地消息表、基于 MQ 的最终一致性。2PC 是最经典的方案分准备和提交两个阶段。协调者先让所有参与者完成准备工作并锁定资源全部准备好之后再统一提交。优点是强一致性缺点是协调者单点故障会影响整体准备阶段加锁时间过长会影响性能。TCC 把每个业务操作拆成 Try、Confirm、Cancel 三个阶段。Try 阶段做资源检查和预留Confirm 阶段做真正的业务操作Cancel 阶段做回滚补偿。TCC 的侵入性比较强需要业务方自己实现三个方法但它能做到比较灵活的资源控制。我参与过的一个订单系统就是用 TCC 处理库存扣减Try 阶段冻结库存Confirm 阶段实际扣减Cancel 阶段解冻固化下来之后效果不错。本地消息表和基于 MQ 的最终一致性则是把事务的原子性拆成多个本地事务加上异步消息保证“最终一致”。这种方案的优点是性能好、不阻塞缺点是无法做到强一致数据在一段时间内可能处于中间状态。选型的时候我的建议是先问业务能不能接受最终一致。如果必须强一致比如金融交易优先考虑 2PC 或者 TCC如果最终一致可以接受比如订单通知、积分发放用本地消息表就能解决 80% 的问题。另外还有一个很现实的思路能通过业务设计规避的分布式事务就不要硬上分布式事务方案。比如把订单和库存放到同一个库用本地事务解决很多问题就不存在了。5.2 分布式锁的实现与权衡分布式并发控制光是数据库层面的行锁还不够。比如一个定时任务在多个实例上同时执行数据库里有一条任务记录要被多个服务实例竞争这时候需要的是分布式锁。分布式锁的常见实现有三种基于数据库、基于 Redis、基于 ZooKeeper/etcd。基于数据库实现分布式锁最简单的方式就是利用数据库的唯一索引。插入一条记录成功就拿到锁删除记录释放锁。但这有几个问题依赖数据库可用性数据库挂了我们照样有问题锁没有自动过期机制进程挂了锁永远不会释放性能也比较差。所以这种方式只适合极低并发的场景生产环境我基本不推荐。基于 Redis 实现分布式锁是目前最主流的方案。核心命令是SET key value NX EX seconds利用 NX不存在才设置保证互斥EX 设置过期时间防止持有锁的进程崩溃导致死锁。释放锁的时候一定要用 Lua 脚本保证“先校验 value 再删除”的原子性避免误删别人的锁if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endvalue 要用一个全局唯一的标识比如 UUID 或者业务请求 ID这样才能保证只有锁的持有者才能释放锁。如果不用这个校验A 的锁过期被 B 拿到A 操作完再去删锁就会把 B 的锁删掉引发严重问题。这里要特别提醒一个热词里经常出现的场景redistemplate 分布式锁定时任务重复执行。很多人用 RedisTemplate 实现分布式锁会遇到两个典型问题一个是 value 设置成了当前日期这类没有唯一性的值导致误删锁另一个是忘记设置过期时间进程崩溃后锁永远不释放。这些都是可以在代码 review 阶段就发现的低级错误但我在实际项目里见到的频率相当高。更进阶的考虑是 Redisson 的看门狗机制它会在锁过期前自动续期避免业务执行时间超过锁过期时间导致锁提前释放。如果你的业务操作是不确定时长的建议直接用 Redisson 而不是自己封装 Redis 分布式锁。至于 RedLock 这种多节点方案业界争议比较大普通业务场景不需要过度设计。5.3 分布式锁的面试高频题热词里出现了“分布式锁面试题”我顺便把最常被问的几个点整理一下给准备面试的同学参考。第一个问题分布式锁和数据库本地锁有什么区别核心答法是锁的作用域不同本地锁只作用于单个进程内的线程分布式锁作用于多个进程/实例需要通过外部存储协调互斥。第二个问题Redis 分布式锁怎么保证释放锁的原子性答 Lua 脚本先校验 value 再删除整个过程是原子的。第三个问题锁过期时间设置多长合适一般应该设置一个预估最大执行时间的 3~5 倍同时配合看门狗自动续期。如果业务执行时间波动很大续期机制比单纯调大过期时间更靠谱。第四个问题分布式锁和信号量有什么区别锁是互斥的同一时刻只能一个持有者信号量允许多个持有者并发访问控制的是并发数量上限。Redis 里可以用INCRBY配合计数实现信号量或者用 Redisson 的RSemaphore。6. 常见问题与排查技巧实录6.1 线上问题定位的思路这一节我直接把平时排查问题的流程拿出来分享都是实战中沉淀下来的。第一步看监控。先确认是单点问题还是大面积故障。比如数据库 CPU 飙升、慢查询增多、锁等待超时这些都是不同的方向。第二步查锁等待。通过information_schema.innodb_trx和sys.innodb_lock_waits定位当前有没有事务在等待锁谁在等谁。SELECT * FROM sys.innodb_lock_waits;这张视图会显示被阻塞的事务、阻塞别人的事务、等待时间、以及涉及的锁信息。拿到之后基本能确定是哪条 SQL 引起的。第三步查死锁日志。用SHOW ENGINE INNODB STATUS查看最近一次死锁的详细信息分析两个事务的锁请求顺序。第四步回看应用日志。数据库层的信息只有 SQL但业务上下文在应用日志里。把trx_id或者 SQL 的开始时间跟应用日志的时间线对齐还原整个事务的操作序列才能真正定位到代码层面的问题。6.2 平时最容易踩的坑清单我把自己和身边同事踩过的坑汇总成了一张速查表强烈建议对照检查你自己的项目。坑点表现原因解决方案事务里调远程接口连接池耗尽事务持锁时间过长远程调用移出事务不带索引的 UPDATE全表被锁写入堵塞RR 级别下间隙锁覆盖全表确保条件命中索引锁的 value 无唯一性误删他人锁释放锁时未校验持有者用 UUID Lua 脚本释放不设置锁过期时间进程崩溃后锁永久不释放无自动续期和过期机制必须设置过期时间或用 Redisson多个事务加锁顺序不一致死锁频繁循环等待统一资源加锁顺序SELECT自动加锁的误解以为普通查询会锁行InnoDB 默认 MVCC 快照读需要加锁时显式FOR UPDATE这张表里的每一条我都见过真实的生产事故。特别是“不带索引的 UPDATE 锁全表”这条发生频率远超想象。很多同学以为 InnoDB 是行锁数据库就不会锁全表但忽略了行锁是基于索引实现的没有索引能定位到具体行数据库就只能把整个表的所有间隙加锁。这跟你用的隔离级别没关系RC 级别虽然不加间隙锁但没有索引的情况下InnoDB 也会锁住所有扫描过的记录效果跟锁全表没区别。还有一个跟索引相关的细节如果一条 UPDATE 的 WHERE 条件同时命中了多个索引InnoDB 会先锁住匹配的主键记录再通过索引回表更新这里的加锁顺序可能跟你的直觉不一样。排查死锁时如果发现两个事务明明操作的“看起来不相关”的数据却死锁了大概率就是索引选择和回表加锁顺序的问题。6.3 实测过的一些调优参数最后分享几个我实际调过、效果比较明显的 InnoDB 参数注意这些参数要结合你的业务场景和机器配置来调整不建议照搬。innodb_lock_wait_timeout默认 50 秒是事务等待锁的超时时间。如果业务对响应时间要求高可以调小到 5~10 秒避免请求长时间挂起。但调太小会导致正常的锁等待也被中断需要根据你的锁冲突情况来定。innodb_deadlock_detect默认开启。死锁检测会消耗 CPU如果并发特别高、死锁概率低可以考虑关闭检测靠innodb_lock_wait_timeout兜底。但要慎重实际上大多数业务场景还是建议开着。transaction-isolation如果业务对可重复读没有硬性要求可以考虑把默认的 RR 改成 RC能减少间隙锁带来的锁冲突提升并发度。这个改动影响面比较大改之前一定要评估所有涉及范围查询和批量更新的 SQL。参数调优这件事我的个人体会是数据库的默认参数是经过大规模场景验证的多数情况下不要轻易动它。真正需要调的往往不是你看到的那个显眼参数而是一些跟业务模型强相关的约束比如索引设计是否合理、事务边界是否过大、锁持有时间是否过长。先把代码层的问题解决掉再谈参数调优顺序不能反。说实话事务和锁这块内容属于那种“看起来文档满天飞真正出问题还是懵”的知识。我这些年踩过的坑、熬夜排查的事故回头看基本都是对底层机制理解不够透彻导致的。这篇文章把事务的隔离级别、InnoDB 的锁模型、MVCC 的版本链、分布式事务和分布式锁的关键点都梳理了一遍希望你在看完之后至少能在下次遇到死锁或锁等待时第一时间知道从哪里查起。最后再分享一个我觉得最值得养成的习惯每次写完一个涉及事务的接口都自问一句“这个事务持锁多久如果并发上来会不会有人卡在这里”想清楚这两个问题能帮你避开大部分跟锁相关的线上事故。
返回列表