一、 MVCC 与行锁的场景区分
- MVCC 相当于行锁的升级版。
- 普通的
SELECT(快照读) :后面没有UPDATE等修改操作的情况,底层是用 MVCC 来做隔离控制。 - 当前读(
UPDATE/DELETE/SELECT ... FOR UPDATE) :这种情况底层就不用 MVCC 了,用的是锁来处理。 - 它们分别用来保证事务的隔离级别(如:读已提交 RC、可重复读 RR)。
二、 快照读与当前读在 RC / RR 下的具体实现
1. 普通 SELECT(快照读,底层用 MVCC)
- 读已提交(RC) :相当于拍两次(多次)快照。刚开始事务发起查询时拍一张快照;当另外一个事务插入/修改数据后,再查询时会再拍一张快照,所以能读到已提交的数据。
- 可重复读(RR):事务第一次查询时拍一张快照,后续查询一直复用这张快照,保证同一事务内读到的数据一致。
2. SELECT ... FOR UPDATE(当前读,底层用锁)
- 可重复读(RR) :需要加间隙锁 和临键锁 。
- 间隙锁:开区间(不包含两头)。
- 临键锁:一个点。
- 间隙锁加临键锁组合起来,相当于一个左开右闭的区间,保证这一段间隙里插入不进去值,从而保证当前读下的可重复读(防止幻读)。
- 读已提交(RC):不加间隙锁,实现读已提交这个隔离级别。
三、 脏读、不可重复读与幻读的区别
- 脏读:读取到了另一个事务未提交的数据(如果对方回滚,读到的就是脏数据)。
- 不可重复读 :同一个事务内,多次读取同一条数据,数据的内容被修改或删除了,导致前后读取的不一样。
- 幻读:读出的记录**条数(行数)**发生了变化,增加或减少了,这叫幻读。
总结口诀(按口述直觉速记)
- 脏读 ➡️ 读到未提交
- 不可重复读 ➡️ 记录的内容变了
- 幻读 ➡️ 记录的**条数(行数)**变了
四、 RC(读已提交)下的典型业务坑点与解决方案
在生产环境中,很多数据库默认采用 RC 隔离级别(并发高、锁开销小)。但在以下场景中,容易产生业务 Bug:
案例 1:财务对账/资产汇总"跨表统计不一致(凭空多钱)"
- 场景 :计算
总资产 = 银行卡余额 + 余额宝余额。 - 问题:在 RC 下,事务 A 查询银行卡后,用户发生了转账并提交;事务 A 随后查询余额宝读到了最新转入的钱,导致总资产统计重复、凭空多出钱。
- 解决方案 :针对该特定对账事务,显式将隔离级别设置为 RR(可重复读),确保整笔对账在同一个时间点快照下完成。
案例 2:电商抢购/余额扣减"Check-Then-Act 超卖"
- 场景 :先
SELECT检查库存是否>= 1,然后UPDATE扣减。 - 问题 :在 RC 下普通
SELECT不加锁,检查通过后、扣减前,其他事务可能抢先扣减并提交,导致库存被扣成负数(超卖)。 - 解决方案 :
- 悲观锁 :查询时改用
SELECT ... FOR UPDATE物理加锁,强行串行化。 - 乐观锁 :利用
WHERE version = oldVersion或状态条件WHERE stock >= 1。
- 悲观锁 :查询时改用
五、 锁机制:悲观锁 vs 乐观锁原理对比
1. 悲观锁(Pessimistic Locking)
- 核心假定:假设并发冲突概率很高,别人一定会来抢。
- 做法 :在查询时直接强行上物理锁(如
SELECT ... FOR UPDATE),阻塞其他试图修改该行的事务,直到当前事务提交。 - 优缺点:绝对安全,但并发性能较低。
2. 乐观锁(Optimistic Locking & CAS 思想)
- 核心假定:假设并发冲突概率较低,查询时不加任何物理锁。
- 做法(Version 标志位 / CAS 比较并交换) :
- 数据表中增加
version字段。 - 更新时执行:
UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = oldVersion;
- 数据表中增加
- 逻辑判定 :
version没变:说明在此期间无人动过数据 ➡️ 更新成功。version变了:说明被他人抢先修改 ➡️ SQL 影响行数为 0,更新失败(由应用层重试或报错)。
- 优缺点:不上锁、并发性能极高,适合读多写少场景。
六、 事务隔离级别的 3 种控制粒度
隔离级别可以根据业务需要,在不同粒度下进行灵活控制:
- 全局粒度 (Global) :影响整个数据库实例(
SET GLOBAL ...)。 - 会话粒度 (Session) :仅影响当前数据库连接(
SET SESSION ...)。 - 特定事务粒度 (Transaction) :仅对即将开启的这一个事务 生效,提交后恢复默认。
- SQL 语法 :在
START TRANSACTION前执行SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; - Spring 框架语法 :直接在特定方法上使用注解
@Transactional(isolation = Isolation.REPEATABLE_READ)。
- SQL 语法 :在