干翻Mybatis源码系列之第十二篇:基于Mybatis Plugins做一个乐观锁

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 干翻Mybatis源码系列之第十二篇:基于Mybatis Plugins做一个乐观锁


给自己的每日一句

不从恶人的计谋,不站罪人的道路,不坐亵慢人的座位,惟喜爱耶和华的律法,昼夜思想,这人便为有福!他要像一棵树栽在溪水旁,按时候结果子,叶子也不枯干。凡他所做的尽都顺利。

如何找到孙帅本人

本文内容整理自《孙哥说Mybatis系列视频课程》,老师实力十分雄厚,B站搜孙帅可以找到本人,视频中有老师的微信号。

一:前言

1:什么是乐观锁,解决开发中什么问题?

锁:保证多用户并发访问数据库数据安全的一种机制。这里与Java的锁是不一样的,程序中的锁主要是保证多线程访问过程中程序中数据的安全。

乐观锁是一种数据库中的锁,数据库中的锁一种是乐观锁,另外一种是悲观锁。

二:悲观锁

悲观锁是数据库底层为我们提供的锁,引入悲观锁保证数据并发访问的安全。

什么是并发请求:

并发请求指的是,同一时间,多个请求(事务)访问操作了相同的数据。

什么是同一时间:

站在计算机角度,一定有一个微小的前后关系,这个有可能是毫秒或者是纳秒级别。

一个事务过来之后,对一行数据进行操作,如果还没操作完,另外一个事务也进来了,恰好也拿到了这条数据,那么这样就是拿到了脏数据,是不允许的,这里就需要锁了。基于锁,把并行化的机制强行改外串行化。基于悲观锁强行把这条锁住(行数据被称为行锁)。

insert update delete 执行这样的操作,就会加入悲观锁。基于事务的隔离机制也能加上行锁。

MySQL中对于insert update delete 这样的操作会默认加上事务(事务一定会的),执行一个语句前就会开启一个事务,执行完毕后提交事务。这是MySQL默认的事务机制。

1:悲观锁测试

MySQL的事务具有自动处理机制,想要测试这个效果,需要把自动提交改为false set autocommit = false

左侧终端事务没有提交,锁没有释放,右侧终端更新同条数据,只能进行等待,以至于锁超时,需要重新启动事务。

那什么时候可以释放锁呢?事务提交之后。

上述场景补充:

左右两终端默认开启事务后执行同一个update语句,左侧执行后获取到行锁,事务不提交,右侧只能进行等待,所测提交之后,右侧获取到行锁并且执行成功,如果不提交,行锁也不会被释放,提交之后,行锁释放。

2:悲观行锁

查询当中也可以加入悲观锁,可以在select * from t_user where id = 7 for update;这样就可以给这条数据加入悲观行锁。

需要在select * from t_user where id = 7 for update; 后边执行commit之后,就可以释放锁了。

查询方法也是可以或者需要使用事务的

1:使用悲观锁查询锁住数据:for update 必须得有事务 这里是MySQL自己start transaction

2:Mybatis需要使用二级缓存,也是需要有事务。 这里是必须代码里边去start transaction

MySQL默认每一条DML语句都是一个单独的事务,每条语句会自动开启一个事务,语句执行完毕自动提交事务,MySQL默认是自动提交事务。

悲观锁这个事不是我们来操心的,是数据库底层为我们添加的一种锁,他有一种问题,影响并发访问的效率。如果查询的话我们想使用悲观锁,可以加for update 只不过这种没有Id的情况下会锁整张表,这样的话任何一个事务中但凡想要DML语句中到此表中,事务到此处都会停止,因为获取不到锁。

单纯的查询语句select MySQL底层是不会开启事务的。

三:乐观锁

乐观锁和数据库底层没有半毛钱关系,是我们通过程序添加的一种锁。他本质上是应用锁,并发效率很高,但是安全性比较低。我们要知道锁,永远是物料层的才是最保险、最安全的

1:乐观锁的实现原理

本质上就是版本的比对。

两个事务并发访问了某一个行数据,一定会产生并发的问题。乐观锁就是基于版本的比对,我们在一张表当中添加一个新的列。新的列叫做version。这一列当中会有version的初始值,初始值都是0,每更新成功一次这个值都会在这一个字段上+1。

这种机制如何解决并发问题呢?每一次更新的时候都要进行版本的比对,如果版本一致说明没有其他事务对这行数据进行操作,如果版本不一致,说明有其他事务操作了这行数据。

这里边有两个细节。细节1:表中必须得有一个version列,这个version列有一个初始值0,这一行数据每被更新成功一次就会+1。细节2:在实际运行过程中,如果我这个事务,要对这行数据进行更新,那么接下来不是直接去更新数据,而是先去比对版本,如果是版本一致说明没有事务和我并发处理这条数据,然后接下来就去更新。如果对比的版本的不一致。就放弃当前操作,后续再次执行版本比对操作。毕竟,实际开发过程中进行过并发的可能性很低,乐观锁性能很高,我们优先使用乐观锁,乐观锁有问题的话, 我们在切换到悲观锁。

总结:

悲观锁:数据库底层提供的锁,最安全的保证数据库并发机制,手动控制事务,存在并发效率问题

乐观锁:应用锁不涉及数据库底层,并发效率很高,安全性较低。

JPA和Hibernate是天然支持乐观锁的,Mybatis并不支持乐观锁,所以Mybatis当中对这个概念提到的很少。那么我们怎么去实现这个东西呢?

2:乐观锁实现思路

Mybatis原生自己不支持,但是我们自己需要使用,需要自己封装,思路是什么呢?

程序员Mybatis开发过程:

1:entity 添加version字段,存储版本值。

2:别名

3:表 添加vers字段,存储版本值。

4:Dao接口

5:Mapper文件注册

6:API使用

程序员,实现这两部,就可以实用我们的乐观锁了,那么对于封装乐观锁的我们如何去处理乐观锁呢?

1:保证version字段当中初始值为0

用户插入过程中保证数据插入,保证vers = 0

2:每一次更新的过程中,都要把对象中的version属性和表中的vers做对比

3:如果值一致,做更新且vers+1

4:如果值不一致,抛出异常。

我们封装以上这些操作的话,在那里干?

insert 语句,需要获取SQL语句并且实现vers = 0

update语句,需要获取SQL并且实现vers+1

这些显而易见,非常适合在拦截器当中进行使用。

四:实现乐观锁

1:准备一个代码架子

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class LockInterceptor extends MyMybatisInterceptorAdapter {
    private static final Logger log = LoggerFactory.getLogger(LockInterceptor.class);
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }
    @Override
    public void setProperties(Properties properties) {
    }
}

2:最终的成果

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class LockInterceptor extends MyMybatisInterceptorAdapter {
    private static final Logger log = LoggerFactory.getLogger(LockInterceptor.class);
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (log.isInfoEnabled())
            log.info("----LockInterceptor------");
        MetaObject metaObject = SystemMetaObject.forObject(invocation);
        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("target.delegate.mappedStatement");
        String id = mappedStatement.getId();
        /*
            在用户进行插入操作时,需要由拦截器 设置vers值0
            🤔 用户书写的Sql语句:insert into t_user (name) values (#{name});
               封装需要干的事    insert into t_user (name,vers) values (#{name},0)
               问题:如何获得 用户书写SQL ?
               解答:String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
               问题:如何修改sql语句 为其添加vers 值0 ?
               解决:涉及到对原有sql语句操作,JsqlParser
         */
        if (id.indexOf("save") != -1) {
            CCJSqlParserManager parserManager = new CCJSqlParserManager();
            Insert insert = (Insert) parserManager.parse(new StringReader(sql));
            //插入的列 vers  匹配对应的值 0
            //列名字 Columns
            List<Column> columns = insert.getColumns();
            columns.add(new Column("vers"));
            //列的值
            ExpressionList itemsList = (ExpressionList) insert.getItemsList();
            List<Expression> expressions = itemsList.getExpressions();
            expressions.add(new LongValue(0));
            insert.setSetExpressionList(expressions);
            //修改完成sql语句后 新的sql语句 交给Mybatis ---> 继续进行?替换
            metaObject.setValue("target.delegate.boundSql.sql", insert.toString());
        }
         /*
             update t_user set name =?,vers = vers+1 where id = ?
             如果进行update操作:
                1. 在提交update操作时,需要对比此时 对象中的version里面存储的值与数据库中vers字段中的值是否相等
                 1.1 如果不等
                       说明已经有其他用户进行了更新 (存在并发) 抛出异常
                 1.2 如果相等
                       可以进行更新操作,并把对应的vers+1
          */
        if (id.indexOf("update") != -1) {
            CCJSqlParserManager parserManager = new CCJSqlParserManager();
            Update update = (Update) parserManager.parse(new StringReader(sql));
            Table table = update.getTable();
            String tableName = table.getName();
            //id值 一定是更新操作中 User id属性存储
            Integer objectId = (Integer) metaObject.getValue("target.delegate.parameterHandler.parameterObject.id");
            Integer version = (Integer) metaObject.getValue("target.delegate.parameterHandler.parameterObject.version");
            Connection conn = (Connection) invocation.getArgs()[0];
            String selectSql = "select vers from " + tableName + " where id = ?";
            PreparedStatement preparedStatement = conn.prepareStatement(selectSql);
            preparedStatement.setInt(1, objectId);
            ResultSet resultSet = preparedStatement.executeQuery();
            int vers = 0;
            if (resultSet.next()) {
                vers = resultSet.getInt(1);
            }
            System.out.println();
            if (version.intValue() != vers) {
                throw new RuntimeException("版本不一致");
            } else {
                //vers+1
                //正常进行数据库更新
                List<Column> columns = update.getColumns();
                columns.add(new Column("vers"));
                List<Expression> expressions = update.getExpressions();
                expressions.add(new LongValue(vers + 1));
                update.setExpressions(expressions);
                metaObject.setValue("target.delegate.boundSql.sql", update.toString());
            }
        }
        return invocation.proceed();
    }
    @Override
    public void setProperties(Properties properties) {
    }
}

Mysql当中int类型不指定长度的时候,默认是11。

为什么乐观锁不一致的时候,需要抛异常出去,这样做的目的是让用户决定是否重新进行提交。比如,提示用户刷新页面后在提交数据即可。

3:程序bug

当前我们这个代码有没有bug?

事务是交由Mybatis来管理的。即使有多个操作,Mybatis也会根据他自己的操作进行处理。

实际问题存在于A,B线程查询版本号的时候,如果两个线程都没来得及更新,但是都去查版本号去了,那么在做更新的时候又出现了并发,那么怎么解决这个问题呢?

解决方式1:update t_user set name = ? ,vers = vers + 1 where id = ? and vers = version

当然这并不能百分之百解决并发。

五:JSqlParser

我们一定特别注意对JSQLParser的使用。对操作String类型的SQL语句具有很强的作用。

<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.3</version>
</dependency>

六:日志引申

1:日志分类

开发日志:监控、平台 框架运行过程 Log4J logback log4j2

应用日志:业务相关的信息。

谁 在什么时间 做了什么事 成功 失败。

存储在数据库中,建立管理系统,帮助公司分析。

假定,让我们设计这个应用日志的话,应该书写在什么位置?代码应该书写在什么位置?

业务或者是应用日志应该写到Service当中。然后,我们写耦合或者是Aop都是可以的。

我们干预业务操作了几张表,操作了那些表的什么字段,字段值是啥。

最后落点一定是拦截器,基于JsqlParser来实现。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
1月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
45 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
1月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
129 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
3月前
|
XML Java 数据库连接
mybatis源码研究、搭建mybatis源码运行的环境
这篇文章详细介绍了如何搭建MyBatis源码运行的环境,包括创建Maven项目、导入源码、添加代码、Debug运行研究源码,并提供了解决常见问题的方法和链接到搭建好的环境。
mybatis源码研究、搭建mybatis源码运行的环境
|
3月前
|
SQL Java
9、Mybatis-Plus 乐观锁
这篇文章介绍了Mybatis-Plus中乐观锁的实现和使用流程,包括使用场景、在实体类中添加版本号字段、配置乐观锁插件以及通过测试验证乐观锁的效果,确保在并发环境下数据的一致性。
9、Mybatis-Plus 乐观锁
|
3月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
3月前
|
供应链 前端开发 Java
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
该博客文章介绍了一个使用Mybatis、Layui、MVC和JSP技术栈开发的服装库存管理系统,包括注册登录、权限管理、用户和货号管理、库存管理等功能,并提供了源码下载链接。
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
|
3月前
|
缓存 Java 数据库连接
我要手撕mybatis源码
该文章深入分析了MyBatis框架的初始化和数据读写阶段的源码,详细阐述了MyBatis如何通过配置文件解析、建立数据库连接、映射接口绑定、动态代理、查询缓存和结果集处理等步骤实现ORM功能,以及与传统JDBC编程相比的优势。
我要手撕mybatis源码
|
5月前
|
XML 前端开发 Java
Mybatis-Plus乐观锁配置
Mybatis-Plus乐观锁配置
|
6月前
|
SQL 数据库
MyBatisPlus之逻辑删除、MyBatisPlus解决并发问题的乐观锁机制
MyBatisPlus之逻辑删除、MyBatisPlus解决并发问题的乐观锁机制
83 2