Mybatis Plus 乐观锁插件(手把手教学)

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

截止目前, 星球 内专栏累计输出 72w+ 字,讲解图 3103+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2400+ 小伙伴加入学习 ,欢迎点击围观

前言

大家好,我是小哈。

程序设计中,若遇到高并发场景,需要多线程修改共享资源的时候,为了保证数据的原子性,就需要频繁使用到锁。锁的类型也有很多,如互斥锁、自旋锁、读写锁、悲观锁、乐观锁等,因为本文主要内容是 Mybatis Plus 实现乐观锁功能,具体就不展开讲了。

什么是乐观锁?

乐观锁就好比一个人性格非常积极乐观。当多个线程需要修改同一个资源时,乐观锁认为发生冲突的概率很低,所以不会有加锁的动作,先改了再说,所以乐观锁也称为无锁编程

实现方式

乐观锁通常会使用版本号机制或者 CAS 算法来实现。 这里主要回顾一下版本号机制,通常是在建表时,新增一个 version 版本号字段,假设初始值为 1。当多线程修改某条记录时,会先进行查询操作(包括版本号 version),后续修改数据时,会判断查询出来的当前版本号是否与数据库中的一致,一致才会进行更新,并且对 version 版本号进行加 1 操作,否则更新失败。

乐观锁应用场景

乐观锁适用于读多写少(读数据多写数据少)的场景,因为没有加锁,避免了锁竞争带来的性能消耗,所以吞吐量非常高

Mybatis Plus 实现乐观锁

配置乐观锁插件

Mybatis Plus 有现成的乐观锁插件,就好比分页插件一样,集成一下即可实现相关功能。配置该插件有两种方式:

注意:考虑到项目中可能会添加多个插件,需要注意顺序关系,官方推荐顺序如下:

多租户插件 -> 动态表名插件 -> 分页插件 -> 乐观锁插件 -> sql 性能规范插件 -> 防止全表更新与删除插件。

Spring Boot 整合

新建 config 包,用于放置配置类,然后,新建 MybatisPlusConfig 配置类,代码如下:

/**
 * @Author: 犬小哈
 * @From: 公众号:小哈学Java, 网站:www.quanxiaoha.com
 * @Date: 2022-12-15 18:29
 * @Version: v1.0.0
 * @Description: TODO
 **/
@Configuration
@MapperScan("com.quanxiaoha.mybatisplusdemo.mapper")
public class MybatisPlusConfig {

    /**
     * 插件相关
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

}

Spring MVC 整合

xml 文件配置如下:

<bean class="com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor" id="optimisticLockerInnerInterceptor"/>

<bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
    <property name="interceptors">
        <list>
            <ref bean="optimisticLockerInnerInterceptor"/>
        </list>
    </property>
</bean

数据库表与数据

新建一张带有 version 字段的秒杀商品表,Schema 建表脚本如下,并添加一条:

DROP TABLE IF EXISTS t_seckill_goods;

CREATE TABLE `t_seckill_goods` (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `goods_name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
  `count` int(11) NULL DEFAULT NULL COMMENT '库存',
  `version` int(11) NOT NULL DEFAULT 1 NULL COMMENT '乐观锁版本号, 默认从 1 开始',
  PRIMARY KEY (`id`)
) COMMENT = '秒杀商品表';

INSERT INTO `test`.`t_seckill_goods` (`id`, `goods_name`, `count`, `version`) VALUES (1, '《三国演义》', 1000, 1);

实体类

新建秒杀商品实体类 SeckillGoods

/**
 * @author: 犬小哈
 * @from: 公众号:小哈学Java, 网站:www.quanxiaoha.com
 * @date: 2022-12-13 14:13
 * @version: v1.0.0
 * @description: 秒杀商品实体类
 **/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_seckill_goods")
public class SeckillGoods {
    /**
     * 主键 ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 商品名称
     */
    private String goodsName;
    /**
     * 库存
     */
    private Integer count;
    /**
     * 乐观锁版本号
     */
    @Version
    private Integer version;

}

TIP : @Data @Builder @AllArgsConstructor @NoArgsConstructor 都是 Lombok 注解,偷懒用的,加上它即编译自动添加 getXXX/setXXX类构造器 等相关方法,不了解的小伙伴可自行搜索一下如何使用。

注意:

  • @Version 注解仅支持数据类型为:int, Integer, long, Long, Date, Timestamp, LocalDateTime
  • 整数类型下会执行 newVersion = oldVersion + 1
  • newVersion 会回写到 entity 实体类中
  • 仅支持 updateById(id)update(entity, wrapper) 方法
  • update(entity, wrapper) 方法下, wrapper 不能复用!!!

新建 Mapper 接口

mapper 包下创建 SeckillGoodsMapper 接口,继承自 BaseMapper :

/**
 * @author: 犬小哈
 * @from: 公众号:小哈学Java, 网站:www.quanxiaoha.com
 * @date: 2022-12-13 14:13
 * @version: v1.0.0
 * @description: TODO
 **/
public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> {
}

测试一波看看乐观锁是否生效

在单元测试类中注入 SeckillGoodsMapper:

@Autowired
private SeckillGoodsMapper seckillGoodsMapper;

新建一个单元测试方法 testVersion() ,代码如下:

@Test
void testVersion() {
    UpdateWrapper<SeckillGoods> wrapper = new UpdateWrapper<>();
    // 库存减一
    wrapper.setSql("count = count - 1");
    // 设置当前版本号
    SeckillGoods seckillGoods = SeckillGoods.builder().version(1).build();
    // 乐观锁更新
    seckillGoodsMapper.update(seckillGoods, wrapper);

    System.out.println("回填版本号:" + seckillGoods.getVersion());
}

执行该单元测试,实际执行 SQL 如下:

我们对库存进行了减一操作,并且乐观锁插件自动将 version 进行了加 1 操作,同时进行了版本号判断。

结语

本小节中,我们学习了什么是乐观锁,以及其实现方式(版本号机制)与应用场景,最后通过 Mybatis Plus 整合了乐观锁插件,最后以单元测试实测是完全没有问题的。