悲观锁
概念
悲观锁的核心思想是 "悲观" 地认为在对共享资源进行操作时,一定会有其他线程或事务来修改该资源。所以在操作之前,会先对资源进行加锁,以防止其他线程或事务的干扰,确保操作的独占性。
实现方式
- 数据库层面 :在数据库中,常用的悲观锁实现方式是使用
FOR UPDATE
语句。例如,在 MySQL 中,执行SELECT ... FOR UPDATE
语句会对查询结果中的记录加上行级锁,其他事务在这些记录被锁定期间无法对其进行修改或删除操作,直到当前事务提交或回滚释放锁。
sql
-- 假设在一个事务中执行以下语句
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 对查询到的记录进行修改操作
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
- 编程语言层面 :在 Java 中,
synchronized
关键字和ReentrantLock
类都可以实现悲观锁。synchronized
关键字可以修饰方法或代码块,当一个线程进入被synchronized
修饰的方法或代码块时,会自动获取锁,其他线程需要等待该线程释放锁才能进入。
java
public class PessimisticLockExample {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
}
使用场景
悲观锁适用于写操作频繁、冲突可能性较高的场景。例如,在银行系统中,对用户账户余额的修改操作通常使用悲观锁,以确保在一个事务对账户进行修改时,不会有其他事务同时修改该账户,避免出现数据不一致的问题。
乐观锁
概念
乐观锁持有 "乐观" 的态度,它认为在大多数情况下,多个线程或事务对共享资源的操作不会发生冲突,所以在操作之前不会加锁。只有在更新资源时,才会检查该资源是否被其他线程或事务修改过,如果没有被修改,则进行更新操作;如果被修改了,则采取相应的处理措施,如重试或报错。
实现方式
- 版本号机制:在数据库表中增加一个版本号字段(通常为整数类型),每次对记录进行修改时,版本号会加 1。在更新记录时,会比较当前版本号与数据库中记录的版本号是否一致,如果一致则更新记录并将版本号加 1,如果不一致则表示记录已被其他事务修改,更新失败。
sql
-- 假设 users 表中有 id、balance 和 version 字段
-- 查询记录并获取当前版本号
SELECT id, balance, version FROM users WHERE id = 1;
-- 更新记录时检查版本号
UPDATE users SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 1;
- 时间戳机制:与版本号机制类似,只是使用时间戳来代替版本号。每次对记录进行修改时,会更新记录的时间戳。在更新记录时,会比较当前时间戳与数据库中记录的时间戳是否一致。
使用场景
乐观锁适用于读操作频繁、写操作较少、冲突可能性较低的场景。例如,在电商系统中,商品的库存信息通常会被大量用户读取,但只有在用户下单时才会进行写操作,这种情况下可以使用乐观锁来提高系统的并发性能。
对比
- 性能方面:悲观锁由于在操作之前就加锁,会导致其他线程或事务需要等待锁的释放,可能会降低系统的并发性能。而乐观锁在大多数情况下不需要加锁,只有在更新时才进行检查,因此在并发度较高的场景下,乐观锁的性能通常优于悲观锁。
- 冲突处理:悲观锁通过加锁来避免冲突,保证了数据的一致性,但会增加死锁的风险。乐观锁在冲突发生时,需要进行额外的处理,如重试或报错,可能会导致一定的性能开销。
- 使用场景:悲观锁适用于写操作频繁、冲突可能性较高的场景,而乐观锁适用于读操作频繁、写操作较少、冲突可能性较低的场景。