一、業務背景
我司使用mysql數據庫的InnoDB引擎,在執行數據庫更新操作時使用了select ...... for update語句,在一定情況下可能導致行級鎖轉表級鎖,在高並發的場景下導致性能低下,故而打算使用樂觀鎖解決部分性能問題。
系統已經上線,修改所有更新代碼改動量大,故決定通過插件方式。
二、樂觀鎖簡介
樂觀鎖通過在數據庫中增加鎖字段,例如version,更新語句如下
update from TABLE1 set version = version+ 1 where version = version
每次更新時版本號字段都會加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 指定表中字段。暫不支持批量更新,后續會完善。
四、插件原理簡析
1、本插件通過攔截StatementHandler,默認只支持PreparedStatement。
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
2、實現mybatis的Interceptor接口,主要攔截方法為public Object intercept(Invocation invocation) throws Exception {}方法。
3、獲得mybatis的四大對象中的StatementHandler對象,通過SystemMetaObject工具類獲得MetaObject對象,加載出MappedStatement對象獲取sql類型,本插件只攔截更新操作。
MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
4、通過MetaObject對象獲得mapper中的sql即BoundSql,這也是后續我們需要修改的sql 主要為在where語句后添加version = version。
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
5、通過MetaObject獲得原version值。
Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_FIELD);
6、通過jsqlparser工具類修改boundSql
7、插入新的boundSql 和 originalVersion (version=version+1)新的鎖值,默認類型為long(后續會支持int等類型)。
metaObject.setValue("delegate.boundSql.sql", originalSql);
metaObject.setValue("delegate.boundSql.parameterObject." + VERSION_FIELD, (Long) originalVersion + 1);
8、默認的一些方法如生成代理對象。
@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; } }
9、初始化配置文件。
@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"); } }
10、主要功能代碼
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"); } } }
五、源碼說明
1、源碼中附有測試案例和使用教程,還有一些功能需要后期完善,如使用h2內存數據庫方便測試。
2、最近在看一些mybatis的源碼。想要理解插件的工作原理,需要對mybatis的運行流程熟悉,否則插件可能會破壞mybatis的功能,之后會帶來一些mybatis的源碼分析。
源碼地址:https://github.com/binary-vi/binary.github.io/tree/master/locker