乐观锁

Administrator 8 阅读 java

​@Version​ 注解是 MyBatis-Plus 实现乐观锁的核心,它能帮你优雅地解决高并发下的数据更新冲突问题,比如商品超卖、余额扣减等,核心原理可以用一句话概括:​基于“版本号”的冲突检测​。

🤔 什么是乐观锁?

乐观锁是一种并发控制策略,它​假设并发操作发生冲突的概率较低​,因此操作数据时不会加锁,而是在数据提交更新时进行冲突检测。若发现数据在此期间已被修改,则当前更新操作会失败。

🧠 核心原理:一个版本号的较量

  1. ​读数据,记版本​:当你查询一条记录时,MP会顺手查出它的版本号(version​),比如版本号为 1​。

  2. ​提交时,验版本​:在更新这条数据时,MP生成的SQL会自动加上一个条件:WHERE ... AND version = 1​。

  3. ​无冲突,加一版​:如果这期间没人动过这条数据,数据库里的版本号仍然是 1​,条件成立,更新成功。同时,MP会把版本号自增为 2​。

  4. ​有冲突,即失败​:如果你查询后,别人抢先一步更新了数据,版本号就变成了 2​。这时你再执行更新,SQL中的 version = 1​ 条件就无法匹配,更新操作的效果为0行,你就能知道冲突发生了。

🛠️ 使用步骤:三步搞定

第一步:数据库准备

为需要并发控制的表添加一个版本号字段,比如 version​。

sql

ALTER TABLE your_table ADD COLUMN version INT DEFAULT 0;

第二步:实体类添加 @Version​ 注解

在实体类中,用 @Version​ 注解标记版本号字段。

java

import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;

@Data
public class Product {
    private Long id;
    private String name;
    private Integer price;
    
    @Version
    private Integer version; // 版本号字段
}

第三步:注册乐观锁插件

在你的 MyBatis-Plus 配置类中,添加 OptimisticLockerInnerInterceptor​ 插件。

java

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 注册乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

✨ 效果与实战

完成以上步骤后,当你执行 updateById​ 等方法时,MP 会自动进行版本控制。

​自动生成的SQL​:

sql

-- 假设查询到的 version = 1
UPDATE product 
SET price = 99, version = 2 
WHERE id = 1 AND version = 1

​业务代码示例​:

java

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
    @Override
    public boolean updatePrice(Long productId, Integer newPrice) {
        // 1. 查询商品,version 会自动被查询出来
        Product product = baseMapper.selectById(productId);
        if (product == null) {
            return false;
        }

        // 2. 修改字段
        product.setPrice(newPrice);
        
        // 3. 执行更新,MP 会自动处理 version 的校验和自增
        return updateById(product);
    }
}

🚦 业务层处理:更新冲突怎么办?

当并发发生时,updateById(product)​ 会返回 false​ 或影响行数为 0。你需要根据业务场景,在业务层主动处理冲突,通常有以下几种方式:

  • ​提示重试​:对于用户触发操作,可以返回“数据已被修改,请刷新后重试”。

  • ​自动重试​:对库存扣减等对用户无感的操作,可采用自动重试机制,并建议设置最大重试次数和指数退避策略以避免加剧冲突。

  • ​合并更新​:应用在用户个人资料修改等场景,可尝试合并两次更新,或提示用户确认覆盖。

  • ​人工介入​:在财务等敏感核心系统,可将冲突数据记录下来,交由人工审核处理。


💎 总结与注意

✅ 典型适用场景

  • ​高并发下的库存扣减​:秒杀、抢票系统。

  • ​账户余额更新​:同时发生充值、消费等。

  • ​多项配置同时修改​:避免最后提交覆盖此前内容。

  • ​多终端/多用户编辑同一数据​:编辑协同、后台管理。

⚠️ 几个重要提醒

  • ​不要手动修改 version​:由 MP 自增,手动修改将破坏并发控制逻辑。

  • ​确保版本号有初始值​:新增数据时,确保 version​ 字段有默认值(如 0 或 1)。

  • ​不支持伪更新​:更新时若字段都没变,MP 可能不追加 where version = ?​,导致乐观锁失效。可通过 UpdateWrapper​ 的 setSql​ 强制触发,如 setSql("version=version")​。

  • ​注意插件配置​:若同时使用分页等多种插件,确保已正确注册 OptimisticLockerInnerInterceptor​。

  • ​支持的数据类型​:版本号字段支持 int​、Integer​、long​、Long​、Date​、Timestamp​、LocalDateTime​。对于整数类型,版本号自增为 oldVersion + 1​。


🤔 什么时候不适合用?

  • ​高冲突场景​:数据激烈竞争会导致大量更新失败与重试开销。

  • ​长事务​:事务过长会显著增加冲突概率。

  • ​需强事务一致性​:依赖数据库事务隔离级别或对一致性有极高要求的场景。

MyBatis-Plus 的 @Version​ 注解实现乐观锁,为高并发下数据一致性问题提供了高效、低侵入的解决方案。它通过版本号机制以无锁方式应对并发,并需要你在业务层配合做好冲突处理。​@Version​ 注解是 MyBatis-Plus 实现乐观锁的核心,它能帮你优雅地解决高并发下的数据更新冲突问题,比如商品超卖、余额扣减等,核心原理可以用一句话概括:​基于“版本号”的冲突检测​。

🤔 什么是乐观锁?

乐观锁是一种并发控制策略,它​假设并发操作发生冲突的概率较低​,因此操作数据时不会加锁,而是在数据提交更新时进行冲突检测。若发现数据在此期间已被修改,则当前更新操作会失败。

🧠 核心原理:一个版本号的较量

  1. ​读数据,记版本​:当你查询一条记录时,MP会顺手查出它的版本号(version​),比如版本号为 1​。

  2. ​提交时,验版本​:在更新这条数据时,MP生成的SQL会自动加上一个条件:WHERE ... AND version = 1​。

  3. ​无冲突,加一版​:如果这期间没人动过这条数据,数据库里的版本号仍然是 1​,条件成立,更新成功。同时,MP会把版本号自增为 2​。

  4. ​有冲突,即失败​:如果你查询后,别人抢先一步更新了数据,版本号就变成了 2​。这时你再执行更新,SQL中的 version = 1​ 条件就无法匹配,更新操作的效果为0行,你就能知道冲突发生了。

🛠️ 使用步骤:三步搞定

第一步:数据库准备

为需要并发控制的表添加一个版本号字段,比如 version​。

sql

ALTER TABLE your_table ADD COLUMN version INT DEFAULT 0;

第二步:实体类添加 @Version​ 注解

在实体类中,用 @Version​ 注解标记版本号字段。

java

import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;

@Data
public class Product {
    private Long id;
    private String name;
    private Integer price;
    
    @Version
    private Integer version; // 版本号字段
}

第三步:注册乐观锁插件

在你的 MyBatis-Plus 配置类中,添加 OptimisticLockerInnerInterceptor​ 插件。

java

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 注册乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

✨ 效果与实战

完成以上步骤后,当你执行 updateById​ 等方法时,MP 会自动进行版本控制。

​自动生成的SQL​:

sql

-- 假设查询到的 version = 1
UPDATE product 
SET price = 99, version = 2 
WHERE id = 1 AND version = 1

​业务代码示例​:

java

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
    @Override
    public boolean updatePrice(Long productId, Integer newPrice) {
        // 1. 查询商品,version 会自动被查询出来
        Product product = baseMapper.selectById(productId);
        if (product == null) {
            return false;
        }

        // 2. 修改字段
        product.setPrice(newPrice);
        
        // 3. 执行更新,MP 会自动处理 version 的校验和自增
        return updateById(product);
    }
}

🚦 业务层处理:更新冲突怎么办?

当并发发生时,updateById(product)​ 会返回 false​ 或影响行数为 0。你需要根据业务场景,在业务层主动处理冲突,通常有以下几种方式:

  • ​提示重试​:对于用户触发操作,可以返回“数据已被修改,请刷新后重试”。

  • ​自动重试​:对库存扣减等对用户无感的操作,可采用自动重试机制,并建议设置最大重试次数和指数退避策略以避免加剧冲突。

  • ​合并更新​:应用在用户个人资料修改等场景,可尝试合并两次更新,或提示用户确认覆盖。

  • ​人工介入​:在财务等敏感核心系统,可将冲突数据记录下来,交由人工审核处理。


💎 总结与注意

✅ 典型适用场景

  • ​高并发下的库存扣减​:秒杀、抢票系统。

  • ​账户余额更新​:同时发生充值、消费等。

  • ​多项配置同时修改​:避免最后提交覆盖此前内容。

  • ​多终端/多用户编辑同一数据​:编辑协同、后台管理。

⚠️ 几个重要提醒

  • ​不要手动修改 version​:由 MP 自增,手动修改将破坏并发控制逻辑。

  • ​确保版本号有初始值​:新增数据时,确保 version​ 字段有默认值(如 0 或 1)。

  • ​不支持伪更新​:更新时若字段都没变,MP 可能不追加 where version = ?​,导致乐观锁失效。可通过 UpdateWrapper​ 的 setSql​ 强制触发,如 setSql("version=version")​。

  • ​注意插件配置​:若同时使用分页等多种插件,确保已正确注册 OptimisticLockerInnerInterceptor​。

  • ​支持的数据类型​:版本号字段支持 int​、Integer​、long​、Long​、Date​、Timestamp​、LocalDateTime​。对于整数类型,版本号自增为 oldVersion + 1​。


🤔 什么时候不适合用?

  • ​高冲突场景​:数据激烈竞争会导致大量更新失败与重试开销。

  • ​长事务​:事务过长会显著增加冲突概率。

  • ​需强事务一致性​:依赖数据库事务隔离级别或对一致性有极高要求的场景。

MyBatis-Plus 的 @Version​ 注解实现乐观锁,为高并发下数据一致性问题提供了高效、低侵入的解决方案。它通过版本号机制以无锁方式应对并发,并需要你在业务层配合做好冲突处理。