乐观锁,悲观锁
乐观锁和悲观锁是两种常见的并发控制机制,主要用于解决并发操作中的数据一致性问题。它们的应用场景和实现方式各有特点:
1. 乐观锁
定义:
- 乐观锁基于乐观的并发控制思想,假设事务间的冲突概率较低,因此不对资源加锁。
- 在更新数据时,通过某种机制(如版本号或时间戳)来检测是否发生了冲突,如果检测到冲突,事务会回滚或重试。
实现方式:
- 在表中增加一个版本号字段(
version
)或时间戳字段。 - 读取数据时一并读取版本号。
- 更新时将版本号作为条件,如果版本号没有变化,则允许更新;否则,更新失败。
SQL 示例:
-- 假设有一个版本号字段 version
-- 事务 A 读取数据
SELECT balance, version FROM accounts WHERE id = 1;
-- 事务 A 更新数据,条件是版本号未被其他事务修改
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 1;
-- 如果条件未满足(版本号不匹配),更新失败,需要重试。
特性:
- 优点:
- 无需加锁,对读取操作没有阻塞,性能较高。
- 适合读多写少的场景。
- 缺点:
- 如果发生冲突,可能需要多次重试,增加了应用程序的复杂度。
2. 悲观锁
定义:
- 悲观锁基于悲观的并发控制思想,假设事务间的冲突概率较高,因此对资源加锁以避免冲突。
- 在读取数据时直接加锁,确保其他事务无法修改或读取该数据。
实现方式:
- 通过数据库提供的锁机制(如共享锁(S 锁)或排他锁(X 锁))来实现。
- 常用语法:
- 共享锁:
LOCK IN SHARE MODE
- 排他锁:
FOR UPDATE
- 共享锁:
SQL 示例:
-- 悲观锁使用排他锁,防止其他事务修改该行数据
START TRANSACTION;
-- 读取数据并加排他锁
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 更新数据
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
特性:
- 优点:
- 通过锁机制保证数据一致性,不需要额外的版本检测。
- 适合写多读少或高并发更新的场景。
- 缺点:
- 会阻塞其他事务的操作,可能导致性能降低。
- 容易发生死锁,需要特别注意加锁顺序。
3. 乐观锁与悲观锁的对比
特性 | 乐观锁 | 悲观锁 |
---|---|---|
加锁机制 | 不加锁,通过版本号或时间戳控制并发 | 通过数据库锁机制加锁 |
适用场景 | 读多写少,冲突概率低 | 写多读少,冲突概率高 |
并发性能 | 性能较高,无锁操作 | 性能较低,可能阻塞其他事务 |
冲突处理 | 冲突时需要回滚或重试 | 无冲突,直接完成事务 |
实现复杂度 | 需要应用逻辑支持(版本号或时间戳) | 数据库原生支持 |
事务阻塞 | 不会阻塞其他事务 | 可能阻塞其他事务 |
4. 如何选择
-
乐观锁适合以下场景:
- 读操作频繁、写操作较少。
- 数据冲突概率较低。
- 对系统性能要求较高。
-
悲观锁适合以下场景:
- 写操作频繁。
- 冲突概率较高,需要严格的数据一致性。
- 数据库支持较好,可以接受一定的阻塞。
5. 综合示例
假设一个账户余额更新系统,多个事务可能同时读取和更新账户余额:
乐观锁:
事务读取时不加锁,更新时检测版本号是否变化。
-- 读取数据
SELECT balance, version FROM accounts WHERE id = 1;
-- 更新数据
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 1;
悲观锁:
事务读取时加锁,防止其他事务同时操作。
START TRANSACTION;
-- 读取并加锁
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 更新
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
通过选择合适的锁机制,可以在性能和一致性之间找到平衡。
注意:该内容由由AIGC提供。