数据库锁机制是控制并发访问数据的关键技术。
本文系统介绍了锁的概念、分类和应用场景:
1)锁通过限制并发访问确保数据一致性,类比厕所门锁机制;
2)按粒度分为表锁(适合批量操作)、行锁(适合高并发)和页锁;
3)按模式分为共享锁(读锁)、排他锁(写锁)和意向锁;
4)对比了悲观锁(先加锁)和乐观锁(后检查)的实现差异;
5)分析了死锁成因及解决方案;
6)特别说明Hive采用表锁的设计考量。
不同数据库根据应用场景选择适合的锁机制,如OLTP系统多用行锁,数据仓库倾向表锁。
数据库锁机制详解
一、什么是锁?
一句话理解 :锁就是控制多人同时访问同一数据的机制。
生活类比
text
公共厕所:
- 有人进去了 → 锁上门(加锁)
- 其他人 → 只能等待(阻塞)
- 里面的人出来 → 打开锁(释放锁)
- 下一个人 → 才能进去
如果不锁门:两个人同时进去...尴尬!
二、为什么需要锁?
问题场景
假设银行账户:余额 1000 元
| 时间 | 操作A(取钱500) | 操作B(取钱800) | 结果 |
|---|---|---|---|
| 10:00:00 | 读取余额=1000 | ||
| 10:00:01 | 读取余额=1000 | ||
| 10:00:02 | 计算 1000-500=500 | ||
| 10:00:03 | 计算 1000-800=200 | ||
| 10:00:04 | 写入余额=500 | ||
| 10:00:05 | 写入余额=200 | ||
| 10:00:06 | ❌ 最终余额=200(多取了300!) |
没有锁:两个操作互相干扰,数据错误!
有锁:
text
操作A:读取余额 → 加锁 → 处理 → 写回 → 释放锁
↓
操作B:等待...等待...等待... → 读取新余额 → 处理 → 写回
三、锁的分类
3.1 按粒度分类
| 锁粒度 | 锁的范围 | 并发度 | 开销 | 适用场景 |
|---|---|---|---|---|
| 表锁 | 整张表 | 低 | 小 | 全表操作(如 ALTER TABLE) |
| 行锁 | 某一行 | 高 | 大 | OLTP 系统(如银行、电商) |
| 页锁 | 某一页数据 | 中等 | 中等 | 某些数据库(如 SQL Server) |
3.2 按模式分类
| 锁模式 | 英文 | 符号 | 含义 |
|---|---|---|---|
| 共享锁 | Shared Lock | S | 读锁,多个事务可同时读,不能写 |
| 排他锁 | Exclusive Lock | X | 写锁,只能一个事务读写,其他不能 |
| 意向锁 | Intention Lock | IS/IX | 表示"准备加更细的锁",提高效率 |
3.3 兼容性矩阵
| 共享锁(S) | 排他锁(X) | |
|---|---|---|
| 共享锁(S) | ✅ 兼容(可同时读) | ❌ 不兼容 |
| 排他锁(X) | ❌ 不兼容 | ❌ 不兼容 |
四、行锁 vs 表锁 详解
4.1 表锁
sql
-- MySQL
LOCK TABLES emp READ; -- 加表锁(共享锁)
SELECT * FROM emp; -- 可以读
UPDATE emp SET sal=5000; -- ❌ 不能写(会阻塞)
UNLOCK TABLES; -- 解锁
特点:
-
锁定整张表,简单粗暴
-
适合批量操作(数据仓库、ETL)
-
Hive 主要使用表锁
4.2 行锁
sql
-- MySQL InnoDB
BEGIN;
SELECT * FROM emp WHERE empno=7369 FOR UPDATE; -- 只锁这一行
UPDATE emp SET sal=5000 WHERE empno=7369;
COMMIT; -- 释放锁
特点:
-
只锁影响的行,其他行不受影响
-
适合高并发 OLTP(银行、电商)
-
Oracle/MySQL InnoDB 默认使用行锁
五、乐观锁 vs 悲观锁
这是思想层面的分类,不是具体的锁实现。
| 类型 | 思想 | 做法 | 适用场景 |
|---|---|---|---|
| 悲观锁 | "肯定有人跟我抢" | 先加锁,再操作 | 并发高的场景 |
| 乐观锁 | "应该没人跟我抢" | 先操作,提交时检查 | 并发低的场景 |
5.1 悲观锁示例
sql
-- MySQL 悲观锁
BEGIN;
SELECT * FROM products WHERE id=1 FOR UPDATE; -- 加锁
UPDATE products SET stock=stock-1 WHERE id=1;
COMMIT; -- 释放锁
5.2 乐观锁示例
sql
-- 用版本号实现乐观锁
-- 1. 读取数据 + 版本号
SELECT stock, version FROM products WHERE id=1; -- stock=10, version=5
-- 2. 更新时检查版本号
UPDATE products
SET stock=9, version=6
WHERE id=1 AND version=5; -- 版本号匹配才更新
-- 3. 如果更新行数=0,说明被别人改过,重试
六、死锁
6.1 什么是死锁?
两个事务互相等待对方释放锁,谁也动不了。
text
事务A:锁住了行1,等待行2
事务B:锁住了行2,等待行1
结果:两个都卡死!
6.2 死锁示例
sql
-- 事务A
BEGIN;
UPDATE accounts SET balance=balance-100 WHERE id=1; -- 锁住行1
UPDATE accounts SET balance=balance+100 WHERE id=2; -- 等待行2
-- 事务B(同时执行)
BEGIN;
UPDATE accounts SET balance=balance-50 WHERE id=2; -- 锁住行2
UPDATE accounts SET balance=balance+50 WHERE id=1; -- 等待行1
-- 死锁!两个事务互相等待
6.3 如何避免死锁?
| 方法 | 说明 |
|---|---|
| 固定访问顺序 | 总是先更新 id 小的,再更新 id 大的 |
| 减少事务时间 | 尽快 COMMIT,不要有用户交互 |
| 使用超时 | 设置锁等待超时,超时后自动放弃 |
| 死锁检测 | 数据库自动检测,杀掉其中一个事务 |
七、Hive 中的锁
7.1 Hive 锁的特点
| 特点 | 说明 |
|---|---|
| 表级锁 | Hive 主要使用表锁,不支持行锁 |
| 共享锁(S) | 读取表时加,多个读操作可并发 |
| 排他锁(X) | 写入表时加,阻塞其他读写 |
| 自动加锁 | 执行 SQL 时自动加,不需要手动 |
7.2 Hive 锁示例
sql
-- 读操作 → 加共享锁
SELECT * FROM emp; -- 自动加共享锁,允许其他读,禁止写
-- 写操作 → 加排他锁
INSERT INTO emp VALUES(9999,'张三','CLERK',...); -- 自动加排他锁,阻塞其他操作
-- 查看锁
SHOW LOCKS;
-- 手动解锁(某些情况需要)
UNLOCK TABLE emp;
7.3 为什么 Hive 用表锁?
| 原因 | 说明 |
|---|---|
| 设计定位 | 数据仓库,不是高并发 OLTP |
| 批量操作 | 每次处理大量数据,行锁意义不大 |
| 简单可靠 | 表锁简单,死锁概率低 |
| HDFS 特性 | HDFS 不支持行级修改 |
八、课堂笔记相关理解
你的课堂笔记中提到:
小文件过多导致跑批任务慢
这与锁的关系:
| 阶段 | 锁情况 | 说明 |
|---|---|---|
| 读取阶段 | 共享锁 | 多个任务可以同时读 |
| 写入阶段 | 排他锁 | 写任务会阻塞其他操作 |
| 合并文件 | 排他锁 | 小文件合并时需要独占表,导致其他任务等待 |
九、各数据库锁机制对比
| 数据库 | 默认锁粒度 | 支持行锁 | 死锁检测 | 适用场景 |
|---|---|---|---|---|
| MySQL(InnoDB) | 行锁 | ✅ | ✅ | OLTP(高并发) |
| MySQL(MyISAM) | 表锁 | ❌ | ❌ | 只读/低并发 |
| Oracle | 行锁 | ✅ | ✅ | OLTP |
| PostgreSQL | 行锁 | ✅ | ✅ | OLTP |
| Hive | 表锁 | ❌ | ❌ | 数据仓库 |
| HBase | 行锁 | ✅ | ✅ | 实时读写 |
十、总结
| 问题 | 答案 |
|---|---|
| 锁是什么? | 控制多人同时访问同一数据的机制 |
| 为什么需要锁? | 防止数据不一致(如银行取钱) |
| 行锁 vs 表锁 | 行锁并发高,表锁简单 |
| 共享锁 vs 排他锁 | 共享锁可同时读,排他锁独占 |
| 乐观锁 vs 悲观锁 | 乐观锁假设没人抢,悲观锁假设有人抢 |
| 死锁 | 两个事务互相等待对方释放锁 |
| Hive 用什么锁? | 主要用表锁(共享锁/排他锁) |
一句话记忆
锁就像厕所门锁:进去就锁上,别人得等;出来再打开,下一个才能进。
目的就是防止两个人同时用同一个数据搞乱账。