什么是乐观锁、悲观锁?
乐观锁:乐观锁和悲观锁是并发控制的两种方式,用来确保在多线程或多用户访问共享资源时,数据的一致性和完整性。
悲观锁(Pessimistic Lock)
悲观锁假设并发操作会经常发生,因此在每次操作数据前,都会通过加锁的方式来避免其他线程或用户修改数据。它的名字来源于对数据被修改的“悲观”假设。悲观锁常用于数据库系统中,依靠数据库的锁机制来实现。
特点:
- 锁定资源:在读或写数据之前,先获取锁。获取锁后,其他操作无法对该数据进行修改,直到锁释放。
- 适用场景:适用于并发量高且数据争用严重的场景,确保每次操作数据时,不会被其他操作修改。
- 性能开销:由于悲观锁通常会导致资源等待和阻塞,会影响系统性能,尤其在高并发场景下。
优点:
- 能够保证数据在并发情况下的强一致性。
- 数据修改过程中不会出现并发修改冲突。
缺点:
- 由于锁定资源,可能会导致大量等待,尤其在高并发场景下,降低系统性能。
- 可能会出现死锁问题,尤其在多个线程或事务相互等待的情况下。
乐观锁(Optimistic Lock)
乐观锁假设并发冲突不会频繁发生,因此在操作数据时不加锁。它通常会在数据提交或更新时检查是否有冲突,使用版本号或时间戳来判断数据是否被其他线程修改过。如果检测到冲突,则回滚操作并重新尝试。
特点:
- 不加锁:数据在操作时不加锁,只是在提交或更新时检查数据是否被修改。
- 适用场景:适用于读多写少的场景,并发冲突较少,且对性能要求较高。
- 冲突检测:依靠检测机制来判断数据是否被修改,而不是通过锁住资源来避免冲突。
优点:
- 没有加锁开销,性能相对较高,尤其在并发冲突较少时。
- 避免了锁导致的等待和阻塞,适合读多写少的场景。
缺点:
- 不能完全保证冲突不会发生,当检测到冲突时,可能需要回滚并重试操作。
- 在高并发写操作场景中,可能会导致频繁的回滚和重试,影响性能。
区别
对比项 | 悲观锁 | 乐观锁 |
---|---|---|
假设 | 并发冲突频繁发生,必须锁定资源 | 并发冲突很少发生,无需锁定资源 |
实现方式 | 依赖数据库或系统的锁机制 | 依赖版本号或时间戳进行冲突检测 |
性能 | 在高并发场景下性能较差 | 在并发较低时性能更优 |
适用场景 | 数据争用严重、冲突频繁的场景 | 读多写少、冲突较少的场景 |
开销 | 可能引发大量锁等待,甚至死锁 | 可能出现回滚重试的开销 |
数据一致性保证 | 强一致性 | 最终一致性,冲突时需要回滚重试 |
总结来说,悲观锁适合并发冲突频繁的场景,而乐观锁则更适合并发冲突较少的场景。在选择时,需要根据实际应用的读写操作频率、并发量、冲突概率等因素综合考虑。
实现
0.mp实现乐观锁:
1. 悲观锁的实现
悲观锁通常依赖于数据库的锁机制来实现。数据库提供了多种锁类型(如共享锁和排他锁),开发者可以通过显式加锁来实现悲观锁。常见的数据库(如 MySQL、Oracle)都有相应的锁机制。
在数据库中实现悲观锁:
-
SQL语句:使用
SELECT ... FOR UPDATE
悲观锁可以通过在查询时使用FOR UPDATE
语句锁定行,防止其他事务修改该数据。MySQL 示例:
-- 事务开始 BEGIN; -- 查询并锁定数据,其他事务不能修改这条记录 SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 执行更新操作 UPDATE users SET balance = balance - 100 WHERE id = 1; -- 提交事务,释放锁 COMMIT;
在这个例子中,
SELECT ... FOR UPDATE
会锁定id=1
的记录,直到事务结束(通过COMMIT
或ROLLBACK
),其他事务在此期间无法修改这条记录。
在编程语言中实现悲观锁:
编程语言可以通过锁对象来实现悲观锁,例如使用 Java 的 synchronized
关键字或者 Lock
接口。
-
Java 示例(使用
synchronized
):public class PessimisticLockExample { private final Object lock = new Object(); public void updateResource() { synchronized (lock) { // 加锁后执行数据操作,确保只有一个线程能访问 System.out.println("Resource is locked."); // 执行资源修改操作 } } }
在这里,
synchronized
锁住了代码块,确保在同一时间只有一个线程能够执行updateResource()
方法中的操作。
2. 乐观锁的实现
乐观锁依赖于版本号或时间戳来实现冲突检测,通常不锁定资源,而是在更新数据时检查是否有其他事务修改过数据。如果检测到数据已被修改,则回滚并重试。
在数据库中实现乐观锁:
-
SQL 语句:基于版本号的实现
通过在数据库表中增加一个version
字段,每次更新时检查该字段是否与读取时相同。MySQL 示例:
-- 查询数据并获取版本号 SELECT id, balance, version FROM users WHERE id = 1; -- 尝试更新时检查版本号是否一致 UPDATE users SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 1; -- 如果更新成功,表示没有并发修改;如果失败,说明版本号不一致,需重新获取数据并重试
在这个例子中,
version
是用户表中的一个字段。事务在更新数据时检查version
是否与查询时一致。如果版本号已经被修改,则更新失败,意味着该记录已经被其他事务修改。
在编程语言中实现乐观锁:
可以通过类似的方式实现,使用版本号或状态标记进行并发检查。
-
Java 示例(基于版本号的实现):
public class OptimisticLockExample { private int version = 1; public boolean updateResource(int newVersion) { if (this.version == newVersion) { // 如果版本号匹配,更新资源并增加版本号 this.version++; System.out.println("Update successful."); return true; } else { // 版本号不匹配,说明数据已被其他线程修改 System.out.println("Version mismatch. Update failed."); return false; } } }
在这个示例中,
updateResource
方法会检查版本号是否与预期的一致。如果一致,更新成功;如果不一致,说明有并发冲突,需要重新获取数据。
优缺点总结
- 悲观锁依赖于数据库的锁机制或程序中的同步机制,能够确保强一致性,适合高冲突场景,但代价是性能可能会因为锁等待和阻塞而降低。
- 乐观锁则通过版本号或时间戳进行检测,适合冲突较少的场景,避免了锁带来的开销,但在高并发写操作时可能导致频繁重试。
根据实际业务需求的并发量和冲突频率选择合适的锁机制能够提高系统性能和可靠性。