MySQL乐观锁和悲观锁的实现
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
核心结论
核心思想 | 假设并发一定会发生,先加锁再操作 | 假设并发很少发生,先操作再校验 |
实现机制 | 依赖数据库的锁机制 (行锁/表锁) | 依赖版本号或时间戳 |
数据库层面 |
|
|
适用场景 | 写多读少,冲突概率高,怕重试 | 读多写少,冲突概率低,可高并发 |
性能开销 | 高 (涉及锁等待、阻塞) | 低 (无锁机制,依靠CAS操作) |
一、 悲观锁 (Pessimistic Lock) 实现
悲观锁的逻辑是:我就认为一定会有人来改我的数据,所以我在操作之前先把它锁死,别人只能等我操作完。
1. 实现原理
在 InnoDB 存储引擎下,悲观锁的实现依赖于数据库的锁机制。它是一种互斥锁(Exclusive Lock,X锁)。
- 行级锁 (Row Lock): 只锁住当前行。
- 表级锁 (Table Lock): 锁住整个表。
2. 核心语法:SELECT ... FOR UPDATE
这是最典型的悲观锁用法。它会在查询时立即获取行级锁,直到事务提交或回滚才释放。
Java代码示例 (伪代码):
// 假设这是一个转账服务
@Transactional
public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) {
// 1. 悲观锁定用户余额行 (关键步骤)
// 注意:这里必须带主键ID,否则会退化为表锁
String sql = "SELECT balance FROM user_account WHERE id = ? FOR UPDATE";
UserAccount fromAccount = jdbcTemplate.queryForObject(sql, new Object[]{fromUserId}, BigDecimal.class);
// 2. 执行业务逻辑
fromAccount = fromAccount.subtract(amount);
// ... 更新余额逻辑 ...
// 3. 事务提交时,自动释放锁
}
3. 实现细节与注意点
- 必须在事务中:FOR UPDATE 必须在 @Transactional 事务管理下生效,否则会立即释放锁。
- 避免死锁:如果多个事务互相等待对方的锁,会发生死锁。需要保证加锁顺序一致。
- 索引失效会导致锁升级:如果查询条件没有命中索引,InnoDB 会使用表锁,导致性能急剧下降。
二、 乐观锁 (Optimistic Lock) 实现
乐观锁的逻辑是:我认为大家都很守规矩,不会同时改我的数据。我不加锁,我只管在最后提交的时候,看一下中间有没有人动过它。如果动过,我就报错重试;没动过,我就提交成功。
1. 实现原理
乐观锁不依赖数据库的锁机制,而是通过版本控制机制 (Versioning) 来实现。
- CAS 原理 (Compare and Swap):Compare (比较):在更新时,比较当前数据库中的版本号是否和我取出时的一致。Swap (交换):如果一致,就更新为新版本号;如果不一致,就更新失败。
2. 数据库表结构设计
必须在表中增加一个版本字段,通常命名为 version。
CREATE TABLE `user_account` ( `id` bigint NOT NULL AUTO_INCREMENT, `balance` decimal(10,2) DEFAULT NULL, `version` int NOT NULL DEFAULT '0', -- 乐观锁版本号 PRIMARY KEY (`id`) ) ENGINE=InnoDB;
3. Java代码实现 (无锁编程思想)
@Transactional
public void updateBalance(Long userId, BigDecimal newBalance) {
// 1. 先查询出旧数据和版本号
String selectSql = "SELECT id, balance, version FROM user_account WHERE id = ?";
UserAccount account = jdbcTemplate.queryForObject(selectSql, new Object[]{userId},
(rs, rowNum) -> new UserAccount(rs.getLong("id"), rs.getBigDecimal("balance"), rs.getInt("version")));
// 2. 准备更新数据,这里使用 version 进行 CAS 校验
String updateSql = "UPDATE user_account SET balance = ?, version = version + 1 WHERE id = ? AND version = ?";
// 3. 执行更新,返回受影响的行数
int rows = jdbcTemplate.update(updateSql,
newBalance,
account.getId(),
account.getVersion()); // 这里的 version 是旧版本号
// 4. 判断结果
if (rows == 0) {
// 更新失败,说明在查询和更新之间,数据被其他线程修改了 (乐观锁冲突)
throw new RuntimeException("更新失败,数据已被并发修改,请重试");
}
}
4. 进阶:使用 @Version 注解
在实际项目中(如使用 JPA / MyBatis-Plus),通常使用框架提供的乐观锁插件,只需在实体类上加注解即可自动实现:
@Version private Integer version;
三、 总结与选型建议
- 选悲观锁:当你的业务是秒杀、库存扣减这类写竞争非常激烈的场景。虽然加锁会阻塞,但它能保证强一致性,避免大量更新失败带来的重试压力。
- 选乐观锁:绝大多数业务系统(如订单创建、用户信息修改)。因为读多写少,乐观锁性能最好,无锁阻塞。
- 避坑指南:悲观锁一定要走索引,否则锁表。乐观锁在高并发写冲突下(如1000人同时点赞),会导致大量更新失败(CAS失败),此时建议改为Redis分布式锁或分段锁方案。
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
本专栏聚焦MySQL并发控制核心:锁机制与MVCC多版本并发控制。拆解行锁、表锁、意向锁、间隙锁、临键锁,详解MVCC的undo log、read view、版本链实现。讲透事务隔离、幻读、死锁、锁等待等高频考点与实战问题。助力后端开发者、DBA快速掌握高并发下数据一致性与性能调优,夯实面试与工程实践核心能力。
