给自己的每日一句
不从恶人的计谋,不站罪人的道路,不坐亵慢人的座位,惟喜爱耶和华的律法,昼夜思想,这人便为有福!他要像一棵树栽在溪水旁,按时候结果子,叶子也不枯干。凡他所做的尽都顺利。
如何找到孙帅本人
本文内容整理自《孙哥说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来实现。