mybatis实现数据库乐观锁解决并发问题实践

阅读: 评论:0

mybatis实现数据库乐观锁解决并发问题实践

mybatis实现数据库乐观锁解决并发问题实践

目录

何为乐观锁?

举个例子!!

具体实现(简单实现)

         一、在本地数据库设计一个测试表并添加一条测试数据(test_user)

二、创建实体类

三、使用Mybatis插件,实现在执行Sql前同时利用version实现乐观锁(版本的自动更新)

一、mybatis插件介绍

         二、拦截器编写依据

四、在Mybatis拦截器中配置bean使得编写的拦截器生效

五、编写代码测试


何为乐观锁?

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

举个例子!!

假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

1 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。

2 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。

3 操作员 A 完成了修改工作,将 version=1 的数据连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,同时数据库记录 version 更新为 2(set version=version+1 where version=1) 。

4 操作员 B 完成了数据录入操作,也将 version=1 的数据试图向数据库提交( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

具体实现(简单实现)

一、在本地数据库设计一个测试表并添加一条测试数据(test_user)

二、创建实体类

@Table(name = "test_user")
public class UserEntity {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Integer id;@Column(name = "user_name")private String userName;@Column(name = "user_telephone")private String userTelephone;@Column(name = "user_email")private String userEmail;@Column(name = "version")private Integer version;public Integer getVersion() {return version;}public void setVersion(Integer version) {this.version = version;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public String getUserTelephone() {return userTelephone;}public void setUserTelephone(String userTelephone) {this.userTelephone = userTelephone;}public String getUserEmail() {return userEmail;}public void setUserEmail(String userEmail) {this.userEmail = userEmail;}
}

 因为作者使用Mybatis的通用Mapper没有使用XML的形式,所以实体类与表关联使用了注解形式

三、使用Mybatis插件,实现在执行Sql前同时利用version实现乐观锁(版本的自动更新)

一、mybatis插件介绍

MyBatis 允许在己映射语句执行过程中的某一点进行拦截调用。默认情况下, MyBatis 允许使用插件来拦截的接口和方法包括以下几个:

  • Executor (update 、query 、flushStatements 、commit 、rollback 、getTransaction 、close 、isClosed)

  • ParameterHandler (getParameterObject 、setParameters)

  • ResultSetHandler (handleResul tSets 、handleCursorResultSets、handleOutputParameters)

  • StatementHandler (prepare 、parameterize 、batch update 、query) 

MyBatis 插件实现拦截器接口Interceptor,在实现类中对拦截对象和方法进行处理 。 

  • setProperties:传递插件的参数,可以通过参数来改变插件的行为。

  • plugin:参数 target 就是要拦截的对象,作用就是给被拦截对象生成一个代理对象,并返回。

  • intercept:会覆盖所拦截对象的原方法,Invocation参数可以反射调度原来对象的方法,可以获取到很多有用的东西。

除了需要实现拦截器接口外,还需要给实现类配置拦截器签名。 使用 @Intercepts 和 @Signature 这两个注解来配置拦截器要拦截的接口的方法,接口方法对应的签名基本都是固定的。

@Intercepts 注解的属性是一个 @Signature  数组,可以在同 一个拦截器中同时拦截不同的接口和方法。

@Signature 注解包含以下三个属性。

  • type:设置拦截的接口,可选值是前面提到的4个接口 。

  • method:设置拦截接口中的方法名, 可选值是前面4个接口对应的方法,需要和接口匹配 。

  • args:设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法 。

二、拦截器编写依据

要实现版本号自动更新,我们需要在SQL被执行前修改SQL,因此我们需要拦截的就是 StatementHandler  接口的 prepare 方法,该方法会在数据库执行前被调用,优先于当前接口的其它方法而被执行。

/*** 乐观锁:数据版本插件**/
@Intercepts(@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class, Integer.class})
)
public class VersionInterceptor implements Interceptor {private static final String VERSION_COLUMN_NAME = "version";private static final Logger logger = Logger(VersionInterceptor.class);@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取 StatementHandler,实际是 RoutingStatementHandlerStatementHandler handler = (StatementHandler) Target());// 包装原始对象,便于获取和设置属性MetaObject metaObject = SystemMetaObject.forObject(handler);// MappedStatement 是对SQL更高层次的一个封装,这个对象包含了执行SQL所需的各种配置信息MappedStatement ms = (MappedStatement) Value("delegate.mappedStatement");// SQL类型SqlCommandType sqlType = ms.getSqlCommandType();if(sqlType != SqlCommandType.UPDATE) {return invocation.proceed();}// 获取版本号Object originalVersion = Value("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME);if(originalVersion == null || Long.String()) <= 0){return invocation.proceed();}// 获取绑定的SQLBoundSql boundSql = (BoundSql) Value("delegate.boundSql");// 原始SQLString originalSql = Sql();// 加入version的SQLoriginalSql = addVersionToSql(originalSql, originalVersion);// 修改 BoundSqlmetaObject.setValue("delegate.boundSql.sql", originalSql);// proceed() 可以执行被拦截对象真正的方法,该方法实际上执行了method.invoke(target, args)方法return invocation.proceed();}/*** Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象.** @param target 被拦截的对象* @return 代理对象*/@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}/*** 设置参数*/@Overridepublic void setProperties(Properties properties) {}/*** 获取代理的原始对象** @param target* @return*/private static Object processTarget(Object target) {if(Proxy.Class())) {MetaObject mo = SystemMetaObject.forObject(target);return Value("h.target"));}return target;}/*** 为原SQL添加version** @param originalSql 原SQL* @param originalVersion 原版本号* @return 加入version的SQL*/private String addVersionToSql(String originalSql, Object originalVersion){try{Statement stmt = CCJSqlParserUtil.parse(originalSql);if(!(stmt instanceof Update)){return originalSql;}Update update = (Update)stmt;if(contains(update)){buildVersionExpression(update);}Expression where = Where();if(where != null){AndExpression and = new AndExpression(where, buildVersionEquals(originalVersion));update.setWhere(and);}else{update.setWhere(buildVersionEquals(originalVersion));}String();}catch(Exception e){(e.getMessage(), e);return originalSql;}}private boolean contains(Update update){List<Column> columns = Columns();for(Column column : columns){ColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){return true;}}return false;}private void buildVersionExpression(Update update){// 列 versionColumn versionColumn = new Column();versionColumn.setColumnName(VERSION_COLUMN_NAME);Columns().add(versionColumn);// 值 version+1Addition add = new Addition();add.setLeftExpression(versionColumn);add.setRightExpression(new LongValue(1));Expressions().add(add);}private Expression buildVersionEquals(Object originalVersion){Column column = new Column();column.setColumnName(VERSION_COLUMN_NAME);// 条件 version = originalVersionEqualsTo equal = new EqualsTo();equal.setLeftExpression(column);equal.setRightExpression(new String()));return equal;}}

在 interceptor 方法中对 UPDATE 类型的操作,修改原SQL,加入version,修改后的SQL类似下图,更新时就会自动将version+1。同时带上version条件,如果该版本号小于数据库记录版本号,则不会更新。

四、在Mybatis拦截器中配置bean使得编写的拦截器生效

    @Beanpublic Interceptor VersionInterceptor(){return new VersionInterceptor();}

五、编写代码测试

@RequestMapping(value = "/test/update",method = RequestMethod.POST, headers = "Accept=application/json")public Result update(@RequestBody UserEntity user){user = userService.update(user);return Results.successWithStatus(200,"更新成功");}

使用Swagger编写测试数据

当前版本与数据库的版本对应均为1

Debug拦截器中方法,Sql修改成功。

数据更改,并且实现了版本的自动增长。如果依旧使用版本1进行数据更改,数据是不会发生变化的。

这样,数据库的乐观锁的简单实现就完成了

本文发布于:2024-02-02 07:25:58,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170682995942263.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:乐观   数据库   mybatis
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23