数据库锁
数据库锁通过限制并发事务对共享资源的访问顺序,保证隔离性并避免写冲突与部分并发异常。
#type / concept
#status / growing
#tech / dev / backend
#resource / database
[!info] related notes
数据库锁
一句话定义
锁是数据库为了控制并发事务对共享资源的访问顺序而施加的限制,用来保护修改、协调读取并维持隔离性。
它要解决什么问题
如果没有锁,多个事务并发访问同一份数据时很容易出问题:
- 两个事务同时更新同一行,后写覆盖前写
- 一个事务正在修改,另一个事务在不合适的时机读到中间状态
- 范围查询和范围更新彼此穿插,导致更难控制的并发异常
锁的本质不是“让数据库变慢”,而是:
- 规定谁先访问
- 哪些访问必须等待
- 冲突发生时谁让路
核心机制 / 工作原理
1. 锁总是附着在某个资源上
数据库里被锁住的对象不一定只是“一行数据”,还可能是:
- 某一行
- 某一页
- 某个索引范围
- 某张表
- 某类元数据
所以讨论锁,不能只问“有没有锁”,还要问:
- 锁住的是哪种资源
- 锁的粒度有多大
- 锁会持有多久
2. 锁模式决定别人还能不能继续操作
常见维度包括:
- 共享锁:通常允许多个读者并发读取,但会限制写
- 排他锁:持有者要写入,其他事务通常不能再读写该资源
- 行锁:只锁部分行,粒度细,并发能力更高
- 表锁:锁整张表,简单但影响范围更大
不同数据库还会有更多细分模式,例如意向锁、范围锁、gap lock、next-key lock 等,它们本质上都是在表达:
- 我想对这片资源做什么
- 别人现在还能不能一起做别的事
3. 锁和隔离级别是协作关系
更强的隔离级别,通常意味着数据库更愿意:
- 提前加锁
- 延长锁持有时间
- 把锁范围扩大到“可能影响结果集变化”的区域
所以锁不是独立机制,而是落实 事务隔离级别 的重要工具之一。
4. 锁和 MVCC 是互补,不是二选一
MVCC 能减少很多读写阻塞,但它解决不了所有问题。
数据库仍然需要锁来处理:
- 写写冲突
- 某些当前读或显式加锁读
- 更高隔离级别下的范围保护
- DDL 和元数据变更
所以“数据库用了 MVCC,所以基本没有锁”是常见误解。
最小例子 / 最小场景
两个事务同时更新同一条库存记录:
- 如果都不受协调,可能出现覆盖写
- 数据库通常会让其中一个事务先拿到写锁
- 另一个事务等待,直到前者提交或回滚
这个等待不是多余开销,而是数据库在保护“最终只能有一个正确写入顺序”。
从理解顺序上,最该先抓住什么
1. 锁的核心不是“有没有”,而是“范围和时长”
影响并发体验的关键通常是:
- 锁锁住了多少资源
- 锁持有了多久
- 是否覆盖了索引范围而不只是单行
这也是为什么短事务和合适索引对锁体验影响很大。
2. 很多性能问题本质上是锁等待问题
例如:
- 长事务迟迟不提交
- 大范围更新把太多行或索引区间锁住
- 热点行被频繁抢写
这些问题表面看像“数据库慢”,底层往往是锁冲突在放大延迟。
3. 死锁是锁系统的正常副作用之一
当事务 A 等 B,事务 B 又等 A,或者更复杂的等待环出现时,就会形成 死锁。
数据库通常会:
- 检测死锁
- 回滚其中一个事务
所以工程上不能假设“事务只要重试就永远不会冲突”,而应该接受死锁和重试是并发系统的一部分。
边界与易混淆点
- 有锁不代表性能一定差;没有必要的锁,数据可能更不安全。
- MVCC 能减少很多读写冲突,但写写冲突仍然绕不开锁。
- 死锁不是数据库特有概念,但数据库是最常见、最容易遇到死锁的工程场景之一。
- 锁多不一定意味着数据库设计差,有时是业务竞争确实激烈;真正需要判断的是锁是否过宽、过久、过频繁。
- “查询语句不会加锁”是错误说法,很多数据库里是否加锁要看读取方式、隔离级别和语句形态。