悲观锁和乐观锁
在多线程编程中,乐观锁和悲观锁是两种解决并发安全问题的核心策略,它们的核心策略在于对“数据竞争风险”的假设和应对方式。
核心思想与定义
1. 悲观锁
- 核心思想:“悲观”地假设冲突很可能发生。在操作数据(尤其是修改)之前,预先锁定该数据,确保在持有锁的期间,其他事务无法修改(甚至有时无法读取)该数据,从而避免冲突。
- 行为模式:“先取锁,再操作”。事务开始时或操作数据前就获取锁(如行锁、表锁),直到事务提交或回滚才释放。
2. 乐观锁
- 核心思想:“乐观”地假设冲突很少发生。允许事务在不加锁的情况下直接读取和修改数据。但是在提交修改之前,会检查此期间的数据是否被其他事务修改过。如果检测到冲突,则放弃本次修改(通常通过回滚或重试机制处理)。
- 行为模式:“先操作,提交时再检查冲突”。核心在于冲突检测而非冲突避免。
实现机制
1. 悲观锁实现(通常由数据库直接提供)
- 数据库锁机制:直接利用数据库的锁功能。
- 行级锁:
SELECT ... FOR UPDATE
(加排他锁/X锁),SELECT ... LOCK IN SHARE MODE
(加共享锁/S锁 - 较少用于悲观更新)。这是 InnoDB 等支持行锁引擎的标准做法。FOR UPDATE
会阻塞其他事务的FOR UPDATE
和普通写操作。 - 表级锁:
LOCK TABLES ... WRITE/READ
。粒度大,并发性低,在支持行锁的引擎中不推荐用于细粒度并发控制。
- 行级锁:
- 特点:
- 强一致性:锁定期间保证数据的绝对独占性。
- 阻塞:获取不到锁的事务会阻塞等待,直到锁释放或超时。
- 开销:加锁、解锁、维护锁、处理死锁都需要开销。
- 死锁风险:多个事务循环等待对方持有的锁会导致死锁,需要数据库死锁检测和回滚。
2. 乐观锁实现(通常需要应用层逻辑配合数据库特性)
- 核心:版本控制
- 数据表增加版本字段:如
version
(整数) 或timestamp
(时间戳)。 - 读取:读取数据时,同时记录当前版本号
V_old
。 - 修改:更新数据时,在
WHERE
条件中包含主键和读取时的版本号V_old
,并将版本号+1
(或更新时间戳)。
- 数据表增加版本字段:如
- 冲突检测:
- 如果
UPDATE
语句返回的“影响行数”为 0,说明在读取之后、提交之前,该行数据已经被其他事务修改(V_old
与当前数据库中的版本号不匹配),即发生了冲突。 - 应用层检测到影响行数为 0 后,需要根据业务逻辑处理冲突(典型处理:回滚当前事务、重试整个业务操作、提示用户等)。
- 如果
- 特点:
- 非阻塞:读操作不加锁,写操作只在提交瞬间进行版本检查(本质是一个原子操作),不会导致其他事务阻塞等待(但冲突的事务需要自己处理失败)。
- 高并发:在冲突率低的场景下,性能通常优于悲观锁(省去了加锁开销和等待时间)。