乐观锁和悲观锁
乐观锁和悲观锁是并发控制中常用的两种机制,用于解决多线程或多进程环境下的数据一致性问题。以下是它们的具体概念、实现方式及适用场景:
1. 乐观锁
乐观锁假设数据在大多数情况下不会发生冲突,因此在操作数据时不加锁,而是在更新数据时通过版本号或时间戳来检测是否有其他线程修改过数据。如果检测到冲突,则重试操作。
实现方式
- 版本号机制:在数据库表中增加一个
version字段,每次更新数据时,检查version是否与读取时一致,若一致则更新,同时将version加 1。 - 时间戳机制:类似版本号,使用时间戳字段来判断数据是否被修改。
示例代码(基于 SQL)
-- 查询数据
SELECT stock, version FROM products WHERE id = 1;
-- 更新数据时检查版本号
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 10;
优点
- 无需加锁,性能较高,适用于读多写少的场景。
- 避免了死锁问题。
缺点
- 在高并发写入场景下,可能会频繁重试,影响性能。
适用场景
- 读多写少的场景,如商品库存扣减、订单状态更新等。
2. 悲观锁
悲观锁假设数据在大多数情况下会发生冲突,因此在操作数据时会加锁,确保其他线程无法同时操作该数据,直到锁被释放。
实现方式
- 数据库锁:通过数据库的
SELECT ... FOR UPDATE语句对数据加锁,其他线程在锁释放前无法读取或修改数据。 - 分布式锁:在分布式环境中,使用 Redis 或 Zookeeper 实现锁机制。
示例代码(基于 SQL)
-- 查询数据并加锁
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 更新数据
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
优点
- 数据一致性强,适用于写多的场景。
- 不需要重试机制,逻辑简单。
缺点
- 性能较低,可能导致锁等待或死锁。
- 在高并发场景下,容易成为系统瓶颈。
适用场景
- 写多的场景,如银行转账、订单支付等需要强一致性的操作。
3. 乐观锁与悲观锁的对比
| 特性 | 乐观锁 | 悲观锁 |
|---|---|---|
| 加锁方式 | 不加锁,通过版本号或时间戳检测冲突 | 加锁,阻止其他线程访问数据 |
| 性能 | 性能较高,适合读多写少的场景 | 性能较低,适合写多的场景 |
| 冲突处理 | 通过重试机制解决冲突 | 通过锁机制避免冲突 |
| 死锁风险 | 无死锁风险 | 存在死锁风险 |
| 适用场景 | 商品库存扣减、订单状态更新 | 银行转账、订单支付等强一致性场景 |
面试中的常见问题
-
乐观锁和悲观锁的区别?
- 乐观锁假设冲突少,不加锁,通过版本号检测冲突;悲观锁假设冲突多,加锁避免冲突。
-
如何避免悲观锁的死锁问题?
- 确保加锁顺序一致。
- 设置锁的超时时间。
-
乐观锁的重试机制如何设计?
- 在冲突时,重新读取数据并尝试更新,设置最大重试次数以避免无限重试。
通过理解乐观锁和悲观锁的原理及适用场景,可以根据实际业务需求选择合适的并发控制机制。