一、乐观锁核心原理
乐观锁的核心是“假设不会发生并发冲突,只在提交更新时检查数据是否被修改过”,而非像悲观锁(如SELECT ... FOR UPDATE)那样提前锁定数据。
- 核心逻辑:更新数据时,先验证数据的“版本/时间戳”是否和自己读取时一致——一致则更新,不一致则说明数据已被其他线程修改,放弃更新(或重试)。
- 适用场景:并发冲突概率低、读多写少的业务(比如商品库存查询、用户信息修改),避免悲观锁带来的性能损耗。
二、主流实现方案(2种核心方式)
1. 版本号法(最常用)
- 原理:在数据表中新增一个
version字段(整数,初始值0),每次更新数据时:- 读取数据时,同时获取
version值; - 更新时,将
version作为条件(WHERE version = 读取值),并把version自增1; - 若更新返回行数为0,说明版本不匹配(数据已被修改),触发冲突处理。
- 读取数据时,同时获取
2. 时间戳法
- 原理:类似版本号,新增
update_time字段(时间戳/DateTime),更新时校验“当前读取的时间戳”和“数据库中的时间戳”是否一致。 - 缺点:时间戳精度问题(如毫秒级)可能导致并发判断失效,不如版本号稳定,实际使用较少。
三、实操示例(MySQL + Java + MyBatis)
以“商品库存扣减”为例(典型的并发场景),完整实现乐观锁:
1. 建表语句(添加version字段)
CREATE TABLE product (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '商品ID',
name VARCHAR(255) NOT NULL COMMENT '商品名称',
stock INT NOT NULL DEFAULT 0 COMMENT '库存数量',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'
);
-- 插入测试数据
INSERT INTO product (name, stock, version) VALUES ('手机', 100, 0);
2. 实体类(对应数据表)
public class Product {
private Long id;
private String name;
private Integer stock;
private Integer version; // 乐观锁版本号
// 省略getter/setter/toString
}
3. Mapper接口(MyBatis)
public interface ProductMapper {
/**
* 根据ID查询商品(获取version)
*/
Product selectById(Long id);
/**
* 扣减库存(乐观锁核心:WHERE version = #{version})
* @param product 商品对象(含id、扣减后的库存、读取时的version)
* @return 影响行数:1=更新成功,0=版本冲突
*/
int deductStock(Product product);
}
4. Mapper XML(核心更新逻辑)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ProductMapper">
<!-- 查询商品 -->
<select id="selectById" resultType="com.example.entity.Product">
SELECT id, name, stock, version FROM product WHERE id = #{id}
</select>
<!-- 扣减库存(乐观锁) -->
<update id="deductStock">
UPDATE product
SET stock = #{stock}, version = version + 1
WHERE id = #{id} AND version = #{version}
</update>
</mapper>
5. 业务层(处理冲突+重试机制)
乐观锁冲突后,通常有两种处理方式:① 返回失败(告知用户“操作失败,请重试”);② 自动重试(有限次数)。以下是带重试的实现:
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
// 最大重试次数
private static final int MAX_RETRY = 3;
/**
* 扣减商品库存(带乐观锁重试)
* @param productId 商品ID
* @param num 扣减数量
* @return true=成功,false=失败
*/
public boolean deductStock(Long productId, int num) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
// 1. 查询商品(获取当前version和库存)
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
if (product.getStock() < num) {
return false; // 库存不足,直接失败
}
// 2. 准备更新参数(扣减库存,携带读取的version)
Product updateParam = new Product();
updateParam.setId(productId);
updateParam.setStock(product.getStock() - num);
updateParam.setVersion(product.getVersion());
// 3. 执行更新(乐观锁校验)
int affectedRows = productMapper.deductStock(updateParam);
if (affectedRows == 1) {
return true; // 更新成功
}
// 4. 版本冲突,重试(休眠50ms避免高频重试)
retryCount++;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 重试次数耗尽,返回失败
return false;
}
}
6. 测试并发场景
用多线程测试乐观锁效果:
@Test
public void testConcurrentDeduct() throws InterruptedException {
Long productId = 1L;
int threadCount = 5; // 5个线程同时扣减
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
boolean success = productService.deductStock(productId, 1);
System.out.println(Thread.currentThread().getName() + ":" + (success ? "扣减成功" : "扣减失败"));
} finally {
latch.countDown();
}
}).start();
}
latch.await();
// 最终库存应为 100 - 5 = 95(无超卖)
Product product = productMapper.selectById(productId);
System.out.println("最终库存:" + product.getStock());
}
四、进阶:MyBatis-Plus自动实现乐观锁
如果使用MyBatis-Plus,可以通过注解简化乐观锁实现,无需手动写UPDATE语句:
1. 实体类添加注解
public class Product {
private Long id;
private String name;
private Integer stock;
@Version // MyBatis-Plus乐观锁注解
private Integer version;
}
2. 配置乐观锁插件
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
3. 业务层简化
// MyBatis-Plus会自动拼接version条件,无需手动写SQL
public boolean deductStock(Long productId, int num) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
Product product = productMapper.selectById(productId);
if (product.getStock() < num) return false;
product.setStock(product.getStock() - num);
// 更新时,MyBatis-Plus自动校验version并自增
int affectedRows = productMapper.updateById(product);
if (affectedRows == 1) return true;
retryCount++;
Thread.sleep(50);
}
return false;
}
五、避坑要点
- 乐观锁不解决“脏读”:乐观锁仅保证“更新时的版本一致性”,若业务需要读取最新数据,需结合事务隔离级别(如READ COMMITTED)。
- 重试次数要限制:避免无限重试导致死循环,通常设置3-5次即可。
- 不要在批量更新中使用:乐观锁适合单条数据更新,批量更新(如
UPDATE ... WHERE 条件)无法精准校验版本,容易失效。 - 版本号必须自增:不能手动修改version,否则会破坏校验逻辑;MyBatis-Plus会自动处理,手动写SQL需确保
version = version + 1。 - 高并发场景需兜底:若并发冲突概率极高(如秒杀),乐观锁重试可能频繁失败,建议结合分布式锁(如Redisson)使用。
总结
- 乐观锁核心是版本校验,通过
version字段在更新时判断数据是否被修改,避免提前加锁; - 实操关键是“查询时获取版本 + 更新时校验版本 + 冲突后重试/返回失败”;
- MyBatis-Plus可通过
@Version注解简化实现,无需手动编写版本校验SQL,提升开发效率。
乐观锁的核心价值是“无锁化提升并发性能”,但需结合业务场景选择——低冲突用乐观锁,高冲突用悲观锁/分布式锁。
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接
文章评论
2025年底来打打卡,学习学习,学不会也看看~
Edge 114.0.1823.58中国-湖南
@皮皮社长 欢迎打卡
Chrome 143.0.0.0中国