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 = LoggerFactory.getLogger(VersionInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取 StatementHandler,实际是 RoutingStatementHandler
        StatementHandler 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();
        }
        // 获取绑定的SQL
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        // 原始SQL
        String originalSql = boundSql.getSql();
        // 加入version的SQL
        originalSql = addVersionToSql(originalSql, originalVersion);
        // 修改 BoundSql
        metaObject.setValue("delegate.boundSql.sql", originalSql);

        // proceed() 可以执行被拦截对象真正的方法,该方法实际上执行了method.invoke(target, args)方法
        return invocation.proceed();
    }

    /**
     * Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象.
     *
     * @param target 被拦截的对象
     * @return 代理对象
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 设置参数
     */
    @Override
    public 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){
        // 列 version
        Column versionColumn = new Column();
        versionColumn.setColumnName(VERSION_COLUMN_NAME);
        update.getColumns().add(versionColumn);

        // 值 version+1
        Addition 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 = originalVersion
        EqualsTo equal = new EqualsTo();
        equal.setLeftExpression(column);
        equal.setRightExpression(new LongValue(originalVersion.toString()));
        return equal;
    }

}

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

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

    @Bean
    public 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编写测试数据

mybatis实现数据库乐观锁解决并发问题实践_第1张图片

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

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

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

mybatis实现数据库乐观锁解决并发问题实践_第2张图片

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

你可能感兴趣的:(分布式锁并发实践)