1. 为什么需要乐观锁?
在数据库并发控制领域,乐观锁是一种非常重要的技术手段。与悲观锁不同,乐观锁假设多用户并发操作时不会产生冲突,因此不会在读取数据时就加锁,而是在更新时检查数据是否被其他事务修改过。这种机制特别适合读多写少的场景,能够显著提高系统的并发性能。
我曾在电商平台的库存管理系统中遇到过典型的并发问题。当多个用户同时抢购同一件商品时,如果使用传统的SELECT...FOR UPDATE悲观锁方式,会导致大量请求串行化处理,系统吞吐量急剧下降。而改用乐观锁后,系统并发能力提升了近5倍。
乐观锁的核心思想是"先读后写":
- 读取数据时不加锁
- 修改数据前检查版本
- 如果版本匹配则提交,否则回滚
这种机制避免了长时间持有锁导致的性能问题,但也带来了新的挑战 - 如何处理冲突?这正是我们需要深入探讨的三种实现方式要解决的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基于版本号的乐观锁实现
2.1 表结构设计
最经典的乐观锁实现方式是在表中添加一个version字段。这个字段通常为整数类型,每次更新时自动递增。下面是一个典型的设计:
sql复制CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
stock INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
version INT DEFAULT 0,
-- 其他字段...
);
注意:version字段的初始值通常设为0,而不是NULL,这样更便于后续的原子性更新操作。
2.2 更新操作实现
实际更新时的SQL语句需要包含版本检查:
sql复制UPDATE products
SET stock = stock - 1,
version = version + 1
WHERE id = 123
AND version = 5; -- 假设之前读取到的版本是5
这个SQL的精妙之处在于:
- 它同时完成了库存扣减和版本号递增
- WHERE条件确保了只有版本号匹配时才执行更新
- 返回的affected_rows可以判断是否更新成功
2.3 业务层处理
在Java代码中,我们通常会这样处理:
java复制public boolean deductStock(Long productId, int quantity) {
// 1. 查询当前数据和版本号
Product product = productDao.selectById(productId);
// 2. 执行业务逻辑计算
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
// 3. 尝试更新
int affectedRows = productDao.updateStockAndVersion(
productId,
quantity,
product.getVersion()
);
// 4. 处理结果
if (affectedRows == 0) {
// 版本号不匹配,说明数据已被其他事务修改
throw new OptimisticLockException("并
