在并发编程和数据库操作场景中,当多个线程或事务同时操作同一份共享数据时,很容易出现数据不一致的问题,悲观锁和乐观锁就是用来解决这类并发冲突的两种核心思路,两者的定义和实现逻辑存在明显差异。

悲观锁的定义
悲观锁的核心思想是假设并发冲突一定会发生,因此在操作数据之前就会先对数据进行加锁,整个操作过程中其他线程或事务无法对该数据进行修改,直到当前操作完成释放锁之后,其他线程才能获取锁进行操作。
悲观锁的典型实现包括数据库的行锁、表锁,以及Java中的synchronized关键字、ReentrantLock等。以数据库为例,使用SELECT ... FOR UPDATE语句就可以对查询到的行加悲观锁:
-- 开启事务 START TRANSACTION; -- 对id为1的用户行加悲观锁,其他事务无法修改该行直到当前事务提交 SELECT balance FROM user WHERE id = 1 FOR UPDATE; -- 执行余额修改操作 UPDATE user SET balance = balance - 100 WHERE id = 1; -- 提交事务,释放锁 COMMIT;
悲观锁的优势是能完全避免并发冲突导致的数据不一致问题,但是因为加锁会阻塞其他操作,所以并发性能相对较低,适合写操作频繁、冲突概率高的场景。
乐观锁的定义
乐观锁的核心思想是假设并发冲突不会发生,因此操作数据时不会直接加锁,而是在更新数据的时候判断数据是否被其他线程或事务修改过,如果没有被修改则更新成功,否则更新失败,需要重试或者提示用户。
乐观锁通常通过在数据表中增加版本号字段或者时间戳字段来实现,更新时携带版本号进行比对。以下是基于版本号的乐观锁实现示例:
-- 查询数据时获取当前版本号 SELECT id, balance, version FROM user WHERE id = 1; -- 假设查询到的version是5,更新时判断版本号是否匹配 UPDATE user SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 5;
如果更新语句返回的影响行数为0,说明版本号不匹配,数据已经被其他事务修改过,当前更新失败。在Java代码中也可以结合版本号实现乐观锁逻辑:
public boolean updateBalanceWithOptimisticLock(int userId, int amount, int oldVersion) {
// 执行更新操作,比对版本号
String sql = "UPDATE user SET balance = balance - ?, version = version + 1 WHERE id = ? AND version = ?";
int affectedRows = jdbcTemplate.update(sql, amount, userId, oldVersion);
// 影响行数大于0说明更新成功,否则失败
return affectedRows > 0;
}
乐观锁不需要加锁阻塞其他操作,并发性能更高,但是如果冲突概率高,会导致大量更新失败重试,反而降低效率,适合读操作频繁、冲突概率低的场景。
两者的核心区别对比
为了更清晰地理解两种锁的差异,我们可以从以下几个维度进行对比:
| 对比维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 加锁时机 | 操作数据前加锁 | 更新数据时判断冲突 |
| 实现方式 | 数据库锁、显式锁机制 | 版本号、时间戳比对 |
| 并发性能 | 较低,会阻塞其他操作 | 较高,无阻塞 |
| 适用场景 | 写多读少、冲突概率高 | 读多写少、冲突概率低 |
使用注意事项
在实际开发中选择锁机制时,需要结合业务场景判断:如果业务中对数据一致性要求极高,且写操作占比大,优先选择悲观锁;如果读操作远多于写操作,且冲突发生概率低,优先选择乐观锁。另外使用乐观锁时需要注意重试机制的设计,避免无限重试导致系统资源耗尽。