mybatis插件-樂觀鎖

1、業務背景

我司使用mysql數據庫的InnoDB引擎,在執行數據庫更新操做時使用了select ...... for update語句,在必定狀況下可能致使行級鎖轉表級鎖,在高併發的場景下致使性能低下,故而打算使用樂觀鎖解決部分性能問題。java

系統已經上線,修改全部更新代碼改動量大,故決定經過插件方式。mysql

2、樂觀鎖簡介

樂觀鎖經過在數據庫中增長鎖字段,例如version,更新語句以下git

update from TABLE1 set version = version+ 1 where version = versiongithub

每次更新時版本號字段都會加1,此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,若是提交的數據版本號大於數據庫表當前版本號,則予以更新,不然認爲是過時數據不予更新。sql

3、插件使用

  1.  使用說明。

  

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <plugins>
        <plugin interceptor="com.vi.optimistic.lock.interceptor.OptimisticLocker">
            <!--<property name="versionField" value="myVersion"/>-->
            <!--<property name="versionColumn" value="my_version"/>-->
        </plugin>
    </plugins>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="UNPOOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/test" />
                <property name="username" value="root" />
                <property name="password" value="123456" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserDefaultMapper.xml" />
        <mapper resource="mapper/UserVersionMapper.xml" />
    </mappers>
</configuration>

 

加入plugins插件,能夠經過指定versionField 指定實體類名稱,versionColumn 指定表中字段。暫不支持批量更新,後續會完善。數據庫

4、插件原理簡析

一、本插件經過攔截StatementHandler,默認只支持PreparedStatement。express

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})

  二、實現mybatis的Interceptor接口,主要攔截方法爲public Object intercept(Invocation invocation) throws Exception {}方法。apache

  三、得到mybatis的四大對象中的StatementHandler對象,經過SystemMetaObject工具類得到MetaObject對象,加載出MappedStatement對象獲取sql類型,本插件只攔截更新操做。mybatis

  

        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

 

  四、經過MetaObject對象得到mapper中的sql即BoundSql,這也是後續咱們須要修改的sql 主要爲在where語句後添加version = version。併發

        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");

  五、經過MetaObject得到原version值。

        Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_FIELD);

  六、經過jsqlparser工具類修改boundSql 

  七、插入新的boundSql 和 originalVersion (version=version+1)新的鎖值,默認類型爲long(後續會支持int等類型)。

  metaObject.setValue("delegate.boundSql.sql", originalSql);
  metaObject.setValue("delegate.boundSql.parameterObject." + VERSION_FIELD, (Long) originalVersion + 1);

  八、默認的一些方法如生成代理對象。

@Override
    public void setProperties(Properties properties) {
        if (null != properties && !properties.isEmpty()) {
            props = properties;
        }
        if (props != null) {
            VERSION_COLUMN = props.getProperty("versionColumn", "version");
            VERSION_FIELD = props.getProperty("versionField", "version");
        }
    }

 

  

@Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler || target instanceof ParameterHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

  九、初始化配置文件。

    @Override
    public void setProperties(Properties properties) {
        if (null != properties && !properties.isEmpty()) {
            props = properties;
        }
        if (props != null) {
            VERSION_COLUMN = props.getProperty("versionColumn", "version");
            VERSION_FIELD = props.getProperty("versionField", "version");
        }
    }

  十、主要功能代碼

package com.vi.optimistic.lock.interceptor;

import com.vi.optimistic.lock.util.PluginUtil;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.util.Collection;
import java.util.List;
import java.util.Properties;

/**
 * 攔截默認PreparedStatement
 * <p>MyBatis樂觀鎖插件<br>
 *
 * @author vi
 * @version 0.0.1
 * @date 2018-04-01
 * @since JDK1.8
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class OptimisticLocker implements Interceptor {
    private static final Log log = LogFactory.getLog(OptimisticLocker.class);
    //數據庫列名
    private static String VERSION_COLUMN = "version";
    //實體類字段名
    private static String VERSION_FIELD = "version";
    //攔截類型
    private static final String METHOD_TYPE = "prepare";

    private static Properties props = null;

    @Override
    public Object intercept(Invocation invocation) throws Exception {
        String interceptMethod = invocation.getMethod().getName();
        if (!METHOD_TYPE.equals(interceptMethod)) {
            return invocation.proceed();
        }
        StatementHandler handler = (StatementHandler) PluginUtil.processTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        SqlCommandType sqlCmdType = ms.getSqlCommandType();
        if (sqlCmdType != SqlCommandType.UPDATE) {
            return invocation.proceed();
        }
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        //TODO 批量更新時須要取list中的參數,後續完善。
        //原樂觀鎖值
        Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_FIELD);
        if (originalVersion == null || Long.parseLong(originalVersion.toString()) <= 0) {
            throw new BindingException("value of version field[" + VERSION_FIELD + "]can not be empty");
        }
        String originalSql = boundSql.getSql();
        if (log.isDebugEnabled()) {
            log.debug("originalSql: " + originalSql);
        }
        originalSql = addVersionToSql(originalSql, VERSION_COLUMN, originalVersion);
        metaObject.setValue("delegate.boundSql.sql", originalSql);
        metaObject.setValue("delegate.boundSql.parameterObject." + VERSION_FIELD, (Long) originalVersion + 1);
        if (log.isDebugEnabled()) {
            log.debug("originalSql after add version: " + originalSql);
            log.debug("delegate.boundSql.parameterObject." + VERSION_FIELD + originalSql);
        }
        return invocation.proceed();
    }

    private String addVersionToSql(String originalSql, String versionColumnName, Object originalVersion) {
        try {
            Statement stmt = CCJSqlParserUtil.parse(originalSql);
            if (!(stmt instanceof Update)) {
                return originalSql;
            }
            Update update = (Update) stmt;
            if (!contains(update, versionColumnName)) {
                buildVersionExpression(update, versionColumnName);
            }
            Expression where = update.getWhere();
            if (where != null) {
                AndExpression and = new AndExpression(where, buildVersionEquals(versionColumnName, originalVersion));
                update.setWhere(and);
            } else {
                update.setWhere(buildVersionEquals(versionColumnName, originalVersion));
            }
            return stmt.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return originalSql;
        }
    }

    private boolean contains(Update update, String versionColumnName) {
        List<Column> columns = update.getColumns();
        for (Column column : columns) {
            if (column.getColumnName().equalsIgnoreCase(versionColumnName)) {
                return true;
            }
        }
        return false;
    }

    private void buildVersionExpression(Update update, String versionColumnName) {

        List<Column> columns = update.getColumns();
        Column versionColumn = new Column();
        versionColumn.setColumnName(versionColumnName);
        columns.add(versionColumn);

        List<Expression> expressions = update.getExpressions();
        Addition add = new Addition();
        add.setLeftExpression(versionColumn);
        add.setRightExpression(new LongValue(1));
        expressions.add(add);
    }

    private Expression buildVersionEquals(String versionColumnName, Object originalVersion) {
        EqualsTo equal = new EqualsTo();
        Column column = new Column();
        column.setColumnName(versionColumnName);
        equal.setLeftExpression(column);
        LongValue val = new LongValue(originalVersion.toString());
        equal.setRightExpression(val);
        return equal;
    }


    private Class<?> getMapper(MappedStatement ms) {
        String namespace = getMapperNamespace(ms);
        Collection<Class<?>> mappers = ms.getConfiguration().getMapperRegistry().getMappers();
        for (Class<?> clazz : mappers) {
            if (clazz.getName().equals(namespace)) {
                return clazz;
            }
        }
        return null;
    }

    private String getMapperNamespace(MappedStatement ms) {
        String id = ms.getId();
        int pos = id.lastIndexOf(".");
        return id.substring(0, pos);
    }

    private String getMapperShortId(MappedStatement ms) {
        String id = ms.getId();
        int pos = id.lastIndexOf(".");
        return id.substring(pos + 1);
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler || target instanceof ParameterHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        if (null != properties && !properties.isEmpty()) {
            props = properties;
        }
        if (props != null) {
            VERSION_COLUMN = props.getProperty("versionColumn", "version");
            VERSION_FIELD = props.getProperty("versionField", "version");
        }
    }
}

   5、源碼說明

    一、源碼中附有測試案例和使用教程,還有一些功能須要後期完善,如使用h2內存數據庫方便測試。

    二、最近在看一些mybatis的源碼。想要理解插件的工做原理,須要對mybatis的運行流程熟悉,不然插件可能會破壞mybatis的功能,以後會帶來一些mybatis的源碼分析。

    源碼地址:https://github.com/binary-vi/binary.github.io/tree/master/locker

相關文章
相關標籤/搜索