目录
👀大家好呀!我是爷爷的茶七里香,最近在复习mp框架,感觉好久没搞这个框架了,最近也遇到了乐观锁相关的内容,顺便记录一下!
为什么需要锁?
我们假设有两条线程要去修改数据,比如要去修改年龄(age)的字段,第一条线程去修改年龄为18岁之后即将要走查询了,在第一条线程走查询之前恰好第二条线程将年龄修改成了3岁,第一条线程查出来记录之后就出现了很离谱的事:我修改的不是18岁吗?怎么就变成3岁了?;所以我们为了避免这种情况,就需要锁!如图:

什么是乐观锁?
除了乐观锁以外还有一种是悲观锁,而乐观锁呢是指每次去查询记录的时候都会认为别人不会修改数据,不进行上锁操作,但是悲观锁却恰恰相反,每次去查询记录都会认为别人会修改数据,所以进行上锁操作,这么一来就会导致线程阻塞的问题,本文就不涉及悲观锁啦,基本上都是使用乐观锁,很少使用悲观锁;我们回归乐观锁的问题,乐观锁查询记录时不会上锁,但是会在更新记录的时候去判断下有没有人去更新了这条记录!在保证了线程安全的同时也能提高吞吐量;常见的实现是使用版本号进行控制,还有一种是CAS算法,该算法最终是由CPU实现的;本文只讲解通过版本号实现乐观锁的方式!!!
通过版本号实现乐观锁的思路:
查询数据 -> 插入需要更新的数据 -> 更新前判断查出来的版本号和数据库是否一直(可能中途有其他线程已经更新过数据并更新了版本)-> 版本一致则更新否则更新失败

以上通过版本号实现乐观锁是假设更新并发低的情况下,如果更新并发高的情况下这种方式是无法保证一个线程从取到数据到更新数据之间会不会有其他的commit,当然可以使用CAS算法来保证一个线程从取到数据到更新数据之间不出现其他的commit,CAS是由CPU也就是硬件层提供的无锁非阻塞的一种算法。看着挺完美,但是吧~~~
(自己私下了解吧,咱们回归正题)
相信通过上面的说明,假设你已经明白了~~哈哈哈哈~下面开始实战吧
- CREATE TABLE `sys_dept` (
- `dept_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '部门id',
- `parent_id` bigint(20) DEFAULT '0' COMMENT '父部门id',
- `dept_name` varchar(30) DEFAULT '' COMMENT '部门名称',
- `version` int(11) DEFAULT NULL COMMENT '版本号',
- `updated_time` datetime DEFAULT NULL COMMENT '更新时间',
- `created_time` datetime DEFAULT NULL COMMENT '创建时间',
- PRIMARY KEY (`dept_id`) USING BTREE
- ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='部门表';
- <!--mybatis-plus-->
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <version>3.4.3</version>
- </dependency>
- <!--MySQL驱动-->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <!--加上这个实体类就不需要提供get/set方法了-->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- </dependency>
- #连接MySQL数据库配置
- spring.datasource.driver-class-name=com.mysql.jdbc.Driver
- spring.datasource.url=jdbc:mysql://127.0.0.1:3306/yy
- spring.datasource.username=root
- spring.datasource.password=root
- package xyz.keydoisdls.mybatisplus.entity;
-
- import java.util.Date;
- import com.baomidou.mybatisplus.annotation.*;
- import lombok.Data;
-
- /**
- * 系统部门
- *
- * @author 爷爷的茶七里香
- * @date 2022/05/21
- */
- @Data
- public class SysDept {
- /**
- * 部门id TableId设置插入数据自动填充主键策略
- */
- @TableId(type = IdType.ASSIGN_ID)
- private Long deptId;
- /**
- * 父id
- */
- private Long parentId;
- /**
- * 部门名称
- */
- private String deptName;
- /**
- * 版本 实现乐观锁需要添加@Version注解
- * FieldFill.INSERT设置在插入时填充
- */
- @Version
- @TableField(fill = FieldFill.INSERT)
- private Integer version;
- /**
- * 更新时间
- * FieldFill.INSERT_UPDATE设置在插入和更新时填充
- */
- @TableField(fill = FieldFill.INSERT_UPDATE)
- private Date updatedTime;
- /**
- * 创建时间
- * FieldFill.INSERT设置在插入时填充
- */
- @TableField(fill = FieldFill.INSERT)
- private Date createdTime;
- }
- package xyz.keydoisdls.mybatisplus.mapper;
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import xyz.keydoisdls.mybatisplus.entity.SysDept;
-
- /**
- * 系统部门映射器
- *
- * @author 爷爷的茶七里香
- * @date 2022/05/21
- * 继承BaseMapper可使用mybatis-plus提供的单表通用CRUD
- */
- public interface SysDeptMapper extends BaseMapper<SysDept> {}
- package xyz.keydoisdls.mybatisplus.handler;
-
- import java.util.Date;
- import org.apache.ibatis.reflection.MetaObject;
- import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
- import org.springframework.stereotype.Component;
-
- /**
- * 元对象处理程序新
- *
- * @author 爷爷的茶七里香
- * @date 2022/05/21
- */
- @Component
- public class MetaObjectHandlerNew implements MetaObjectHandler {
- /**
- * 配置插入时需要填充的字段
- *
- * @param metaObject 元对象
- */
- @Override
- public void insertFill(MetaObject metaObject) {
- // 注意该写法是3.3.2版本之后的
- // 插入数据填充更新时间
- this.strictInsertFill(metaObject, "updatedTime", Date::new, Date.class);
- // 插入数据填充插入时间
- this.strictInsertFill(metaObject, "createdTime", Date::new, Date.class);
- // 插入数据填充默认版本号为1
- // 只支持 int,Integer,long,Long,Date,Timestamp,LocalDateTime这些数据类型
- this.strictInsertFill(metaObject, "version", () -> 1, Integer.class);
- }
-
- /**
- * 配置更新时需要填充的字段
- *
- * @param metaObject 元对象
- */
- @Override
- public void updateFill(MetaObject metaObject) {
- // 更新数据填充更新时间
- this.strictInsertFill(metaObject, "updatedTime", Date::new, Date.class);
- }
- }
- package xyz.keydoisdls.mybatisplus.config;
-
- import org.mybatis.spring.annotation.MapperScan;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
- import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
-
- /**
- * mybatis plus配置
- *
- * @author 爷爷的茶七里香
- * @date 2022/05/21
- * MapperScan是配置扫描mapper接口的路径,在这个位置写了就不需要在主入口上方写了
- */
- @Configuration
- @MapperScan("xyz.keydoisdls.mybatisplus.mapper")
- public class MybatisPlusConfig {
-
- /**
- * mybatis plus拦截器
- *
- * @return {@link MybatisPlusInterceptor}
- */
- @Bean
- public MybatisPlusInterceptor mybatisPlusInterceptor() {
- // 创建mybatis plus拦截器
- MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
- // 添加乐观锁插件
- mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
- return mybatisPlusInterceptor;
- }
- }
目录如下:

将以上东西创建完之后咱们就用测试类测试一下是否生效了
- package xyz.keydoisdls.mybatisplus;
-
- import javax.annotation.Resource;
- import org.junit.jupiter.api.Test;
- import org.springframework.boot.test.context.SpringBootTest;
- import xyz.keydoisdls.mybatisplus.entity.SysDept;
- import xyz.keydoisdls.mybatisplus.mapper.SysDeptMapper;
-
- /**
- * mybatis plus应用程序测试
- *
- * @author 爷爷的茶七里香
- * @date 2022/05/21
- */
- @SpringBootTest
- class MybatisPlusApplicationTests {
-
- /**
- * 注入mapper
- */
- @Resource
- private SysDeptMapper sysDeptMapper;
-
- /**
- * 插入系统部门
- */
- @Test
- public void insertSysDept() {
- SysDept sysDept = new SysDept();
- sysDept.setParentId(0L);
- sysDept.setDeptName("七杀殿");
- // 插入
- sysDeptMapper.insert(sysDept);
- }
-
- }
查看数据库:
能看到自动填充是生效的,接下来就测试下更新数据后乐观锁是否生效
- /**
- * 更新系统部门
- */
- @Test
- public void updateSysDept() {
- // 需要先查询然后再更新,这样才能使乐观锁生效
- SysDept sysDept = sysDeptMapper.selectById(1528005311556849666L);
- // 修改查出来的数据
- sysDept.setDeptName("老六门");
- // 更新数据
- sysDeptMapper.updateById(sysDept);
- }
查看数据库:

发现版本号加1了,说明是生效的,不过这不够严谨,私下自己可以开线程试试是否会有更新失败的情况,这里就不演示啦~
注: 一定要先查询再更新,不然乐观锁没法生效,切记切记~
在开始之前呢先给大家分享一下非常有用的配置:
- # mybatis-plus日志(可在日志查看生成的SQL)
- mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- #全局设置主键生成策略
- mybatis-plus.global-config.db-config.id-type=ASSIGN_ID
在添加了全局主键生成策略之后,只需要在实体类中的主键字段添加@TableId注解就行了,无需指定策略了!!!
- // 添加MySQL的分页插件
- mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
如图:

- /**
- * 分页查询
- */
- @Test
- public void selectSysDeptPage() {
- // 对参数的解释:第几页,一页多少条
- Page<SysDept> page = new Page<>(1, 2);
- // 调用分页查询的方法,第一个参数传的是分页的配置,第二个传的是查询条件,传null就是不设置条件,直接查全部
- Page<SysDept> sysDeptPage = sysDeptMapper.selectPage(page, null);
- // 使用getRecords获取分页后的数据
- List<SysDept> sysDeptS = sysDeptPage.getRecords();
- sysDeptS.forEach(System.out::println);
- // 总条数
- long total = sysDeptPage.getTotal();
- System.out.println("总条数 = " + total);
- // 是否有下一页
- boolean isHasNext = sysDeptPage.hasNext();
- System.out.println("是否有下一页 = " + isHasNext);
- // 是否有上一页
- boolean isHasPrevious = sysDeptPage.hasPrevious();
- System.out.println("是否有上一页 = " + isHasPrevious);
- }
查询结果:

除了以上演示的这些,还有很多好玩的,可以自己去玩玩~~
今天就到这里啦~对你有帮助的话不妨留个赞呗!
🥇原创不易,还希望各位大佬支持一下!
👍点赞,你的认可是我创作的动力 !
🌟收藏,你的青睐是我努力的方向!
✏️评论,你的意见是我进步的财富!