悲观锁与乐观锁
在数据库并发控制中,乐观锁和悲观锁是两种核心机制,用于解决多事务同时操作同一数据时的冲突问题(如脏写、不可重复读等)。两者的核心区别在于对 “并发冲突概率” 的假设不同,进而导致实现方式和适用场景的差异。
1. 悲观锁(Pessimistic Lock)
1. 核心思想
假设并发冲突会频繁发生,因此在操作数据前,会先 “锁定” 数据,阻止其他事务对该数据的修改,直到当前事务完成。
2. 实现方式
依赖数据库底层的锁机制(如行锁、表锁),通过显式或隐式的方式锁定数据:
- 显式锁定:通过 SQL 语句主动加锁,例如
SELECT ... FOR UPDATE(排他锁),事务执行时会锁定查询到的行,其他事务尝试修改或加锁时会阻塞等待,直到当前事务提交 / 回滚释放锁。 - 隐式锁定:数据库引擎自动加锁,例如 InnoDB 在执行
UPDATE/DELETE时,会对匹配的行自动加行锁,事务结束后释放。
3. 优缺点
- 优点:能有效防止并发冲突,安全性高,无需额外的重试逻辑。
- 缺点:
- 锁定期间会阻塞其他事务,降低并发性能(尤其是长事务);
- 可能导致死锁(例如两个事务互相等待对方释放锁);
- 若锁定范围过大(如无索引导致行锁升级为表锁),会严重影响性能。
4. 适用场景
- 写操作频繁,并发冲突概率高的场景(如库存扣减、金融交易);
- 事务执行时间长,需要确保数据一致性的场景。
2. 乐观锁(Optimistic Lock)
1. 核心思想
假设并发冲突很少发生,因此操作数据时不会预先锁定,而是在 “更新数据时” 检查数据是否被其他事务修改过,若未被修改则成功,否则失败(需重试)。
2. 实现方式
由应用层实现,不依赖数据库锁机制,常见方式:
- 版本号机制:在表中新增一个
version字段(整数),逻辑如下: - 查询数据时,同时获取当前版本号:
SELECT id, name, version FROM table WHERE id = 1;(假设版本号为 2); - 更新数据时,检查版本号是否与查询时一致:
UPDATE table SET name = 'new', version = version + 1 WHERE id = 1 AND version = 2;; - 若更新行数为 1,说明成功(无冲突);若为 0,说明数据已被其他事务修改(版本号变化),需重试。
- 时间戳机制:原理同版本号,用
update_time字段(时间戳)代替版本号,更新时检查时间戳是否一致。
3. 优缺点
- 优点:
- 无锁竞争,并发性能高(尤其读操作多的场景);
- 不会导致死锁,实现简单(应用层逻辑)。
- 缺点:
- 存在 “重试成本”:冲突发生时需重新执行事务(可能多次重试);
- 无法解决 “幻读”(因为不锁定数据,其他事务可能插入新数据)。
4. 适用场景
- 读操作频繁,写操作少,并发冲突概率低的场景(如商品详情页、用户信息查询);
- 短事务场景(减少重试成本)。
3. 对比总结
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 冲突假设 | 冲突频繁发生 | 冲突很少发生 |
| 实现层面 | 数据库底层锁机制 | 应用层(版本号 / 时间戳) |
| 并发性能 | 低(阻塞等待) | 高(无锁竞争) |
| 死锁风险 | 有(锁竞争时) | 无 |
| 适用场景 | 写多读少,冲突频繁 | 读多写少,冲突稀少 |
选择原则:根据并发冲突的概率决定 —— 冲突多则用悲观锁保证安全性,冲突少则用乐观锁提升性能。
4. 具体实现
以MySQL + InnoDB为例,结合库存扣减场景(经典的并发控制场景),分别展示悲观锁和乐观锁的具体实现。
场景说明:多个用户同时购买同一商品,需要扣减商品库存,确保库存不会出现负数(避免超卖)。
CREATE TABLE product_stock (
id INT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL COMMENT '商品ID',
stock INT NOT NULL COMMENT '库存数量',
version INT NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁用)',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_product_id (product_id)
) ENGINE=InnoDB COMMENT '商品库存表';
-- 初始化数据:商品ID=1001,初始库存100
INSERT INTO product_stock (product_id, stock) VALUES (1001, 100);
1. 悲观锁实现
核心逻辑:在查询库存时显式加排他锁(FOR UPDATE),阻止其他事务同时修改,直到当前事务提交 / 回滚释放锁。
def deduct_stock_pessimistic(product_id, quantity=1):
conn = None
try:
conn = get_conn()
cursor = conn.cursor(DictCursor) # 用字典游标,方便获取字段
# 1. 查询库存并加排他锁(FOR UPDATE)
# 注意:WHERE条件必须命中索引(这里product_id是唯一索引),否则会锁表
cursor.execute(
"SELECT stock FROM product_stock WHERE product_id = %s FOR UPDATE",
(product_id,)
)
result = cursor.fetchone()
if not result:
print(f"商品{product_id}不存在")
return False
current_stock = result["stock"]
# 2. 检查库存是否充足
if current_stock < quantity:
print(f"库存不足,当前库存:{current_stock},需扣减:{quantity}")
conn.rollback() # 无操作,回滚事务
return False
# 3. 扣减库存
cursor.execute(
"UPDATE product_stock SET stock = stock - %s WHERE product_id = %s",
(quantity, product_id)
)
print(f"扣减成功,剩余库存:{current_stock - quantity}")
conn.commit() # 提交事务,释放锁
return True
except Exception as e:
if conn:
conn.rollback() # 异常时回滚
print(f"悲观锁扣减失败:{str(e)}")
return False
finally:
if conn:
conn.close() # 关闭连接
# 测试调用
deduct_stock_pessimistic(product_id=1001, quantity=1)
2. 乐观锁实现
核心逻辑:查询时获取版本号,更新时检查版本号是否匹配(未被修改),若匹配则更新并递增版本号;否则重试。
def deduct_stock_optimistic(product_id, quantity=1, max_retry=3):
conn = None
retry_count = 0
while retry_count < max_retry:
try:
conn = get_conn()
cursor = conn.cursor(DictCursor)
# 1. 查询库存和当前版本号
cursor.execute(
"SELECT stock, version FROM product_stock WHERE product_id = %s",
(product_id,)
)
result = cursor.fetchone()
if not result:
print(f"商品{product_id}不存在")
return False
current_stock = result["stock"]
current_version = result["version"]
# 2. 检查库存是否充足
if current_stock < quantity:
print(f"库存不足,当前库存:{current_stock},需扣减:{quantity}")
conn.rollback()
return False
# 3. 尝试更新:WHERE条件同时检查版本号(确保期间未被修改)
affect_rows = cursor.execute(
"""UPDATE product_stock
SET stock = stock - %s, version = version + 1
WHERE product_id = %s AND version = %s""",
(quantity, product_id, current_version)
)
if affect_rows == 1:
# 更新成功(版本号匹配,无冲突)
print(f"扣减成功,剩余库存:{current_stock - quantity},新版本号:{current_version + 1}")
conn.commit()
return True
else:
# 更新失败(版本号不匹配,被其他事务修改),重试
print(f"检测到并发冲突,重试第{retry_count + 1}次...")
conn.rollback() # 回滚本次尝试
retry_count += 1
except Exception as e:
if conn:
conn.rollback()
print(f"乐观锁扣减失败:{str(e)}")
return False
finally:
if conn:
conn.close()
# 超过最大重试次数
print(f"超过最大重试次数({max_retry}次),扣减失败")
return False
# 测试调用
deduct_stock_optimistic(product_id=1001, quantity=1)
发表评论