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

目录
何为乐观锁?
举个例子!!
具体实现(简单实现)
一、在本地数据库设计一个测试表并添加一条测试数据()
二、创建实体类
三、使用插件,实现在执行Sql前同时利用实现乐观锁(版本的自动更新)
一、插件介绍
二、拦截器编写依据
四、在拦截器中配置bean使得编写的拦截器生效
五、编写代码测试
何为乐观锁?
乐观锁() 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制 。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性 。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受 。而乐观锁机制在一定程度上解决了这个问题 。乐观锁,大多是基于数据版本()记录机制实现 。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “” 字段来实现 。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一 。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据 。
举个例子!!
假设数据库中帐户信息表中有一个字段,当前值为 1 ;而当前帐户余额字段()为 $100。
1 操作员 A 此时将其读出( =1 ),并从其帐户余额中扣除 $50( $100-$50 ) 。
2 在操作员 A 操作的过程中,操作员B 也读入此用户信息( =1 ),并从其帐户余额中扣除 $20 ( $100-$20 ) 。
3 操作员 A 完成了修改工作,将 =1 的数据连同帐户扣除后余额( =$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,同时数据库记录更新为 2(set =+1 where =1)。
【mybatis实现数据库乐观锁解决并发问题实践】4 操作员 B 完成了数据录入操作,也将 =1 的数据试图向数据库提交( =$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1,数据库记录当前版本也为 2,不满足 “ 提交版本必须等于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回 。
这样,就避免了操作员 B 用基于 =1 的旧数据修改的结果覆盖操作员A 的操作结果的可能 。
具体实现(简单实现) 一、在本地数据库设计一个测试表并添加一条测试数据()
二、创建实体类
@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;}}
因为作者使用的通用没有使用XML的形式,所以实体类与表关联使用了注解形式
三、使用插件,实现在执行Sql前同时利用实现乐观锁(版本的自动更新) 一、插件介绍
允许在己映射语句执行过程中的某一点进行拦截调用 。默认情况下,允许使用插件来拦截的接口和方法包括以下几个:
插件实现拦截器接口,在实现类中对拦截对象和方法进行处理 。
除了需要实现拦截器接口外,还需要给实现类配置拦截器签名 。使用@ 和 @这两个注解来配置拦截器要拦截的接口的方法,接口方法对应的签名基本都是固定的 。
@ 注解的属性是一个 @数组,可以在同一个拦截器中同时拦截不同的接口和方法 。
@ 注解包含以下三个属性 。
二、拦截器编写依据
要实现版本号自动更新,我们需要在SQL被执行前修改SQL,因此我们需要拦截的就是接口的方法,该方法会在数据库执行前被调用,优先于当前接口的其它方法而被执行 。

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

文章插图
/*** 乐观锁:数据版本插件**/@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 = LoggerFactory.getLogger(VersionInterceptor.class);@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取 StatementHandler,实际是 RoutingStatementHandlerStatementHandler handler = (StatementHandler) processTarget(invocation.getTarget());// 包装原始对象,便于获取和设置属性MetaObject metaObject = SystemMetaObject.forObject(handler);// MappedStatement 是对SQL更高层次的一个封装,这个对象包含了执行SQL所需的各种配置信息MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");// SQL类型SqlCommandType sqlType = ms.getSqlCommandType();if(sqlType != SqlCommandType.UPDATE) {return invocation.proceed();}// 获取版本号Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME);if(originalVersion == null || Long.valueOf(originalVersion.toString()) <= 0){return invocation.proceed();}// 获取绑定的SQLBoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");// 原始SQLString originalSql = boundSql.getSql();// 加入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.isProxyClass(target.getClass())) {MetaObject mo = SystemMetaObject.forObject(target);return processTarget(mo.getValue("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 = update.getWhere();if(where != null){AndExpression and = new AndExpression(where, buildVersionEquals(originalVersion));update.setWhere(and);}else{update.setWhere(buildVersionEquals(originalVersion));}return stmt.toString();}catch(Exception e){logger.error(e.getMessage(), e);return originalSql;}}private boolean contains(Update update){List columns = update.getColumns();for(Column column : columns){if(column.getColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){return true;}}return false;}private void buildVersionExpression(Update update){// 列 versionColumn versionColumn = new Column();versionColumn.setColumnName(VERSION_COLUMN_NAME);update.getColumns().add(versionColumn);// 值 version+1Addition add = new Addition();add.setLeftExpression(versionColumn);add.setRightExpression(new LongValue(1));update.getExpressions().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 LongValue(originalVersion.toString()));return equal;}}
在方法中对类型的操作,修改原SQL,加入,修改后的SQL类似下图,更新时就会自动将+1 。同时带上条件,如果该版本号小于数据库记录版本号,则不会更新 。
四、在拦截器中配置bean使得编写的拦截器生效
@Beanpublic Interceptor VersionInterceptor(){return new VersionInterceptor();}
五、编写代码测试
@RequestMapping(value = "http://www.kingceram.com/test/update",method = RequestMethod.POST, headers = "Accept=application/json")public Result update(@RequestBody UserEntity user){user = userService.update(user);return Results.successWithStatus(200,"更新成功");}
使用编写测试数据
当前版本与数据库的版本对应均为1
Debug拦截器中方法,Sql修改成功 。
数据更改,并且实现了版本的自动增长 。如果依旧使用版本1进行数据更改,数据是不会发生变化的 。
这样,数据库的乐观锁的简单实现就完成了