乐观锁与悲观锁
乐观锁与悲观锁都是为了解决并发场景下的数据处理问题。乐观锁不会对数据上锁,只是在执行修改时判断在此期间是否有其他人修改了数据,如果有被其他人修改了,则放弃此次修改操作,否则就执行更新操作。悲观锁在操作数据时是直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能进行任何数据操作。
基于以上特性,通常对于读 > 写的场景下,使用乐观锁会有更好的性能。对于写 > 读的场景,使用悲观锁确保数据的安全性,准确性和一致性。
背景
很多年前项目中使用的持久层框架是Hiberate,使用版本号控制实现的乐观锁。现在很多项目都换成了Mybatis框架,更加轻量级,支持定制化SQL,使其更加灵活以及具有更好的性能。那Mybatis如何实现乐观锁?原生的Mybatis没有实现乐观锁,它的增强版本Mybatis Plus对乐观锁进行了实现。Mybatis Plus的乐观锁也是通过版本号机制实现的。由于我们项目中使用的原生Mybatis,不具备乐观锁功能,如果每个mapper文件中的sql update语句都手动加上verion = version + 1,工作量大而且也容易遗漏。所以我通过Mybatis的拦截器实现了自动在update语句中加上version加1,实现了乐观锁。
原生Mybatis实现乐观锁思路
1. 在数据库的表中增加version字段,默认为0
2. 使用Mybatis的拦截器,拦截语句处理器StatementHandler中的prepare方法,这是对sql语句进行预处理的方法
3. 从StatementHandler中获取SQL类型,如果不是UPDATE,则直接放行;如果是UPDATE,则进行后续的加version字段处理
代码实现
拦截器:
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }))
@SuppressWarnings("all")
public class VersionInterceptor implements Interceptor {
private static final String VERSION_COLUMN_NAME = "version";
private static final Logger logger = LoggerFactory.getLogger(VersionInterceptor.class);
// 配置哪些不需要加version
private static final String[] IGNORE_IDS = { "com.xxx.yyy.zzz.batchAdd"};
@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();
}
// 这些不需要进行加版本号控制
if (Arrays.asList(IGNORE_IDS).contains(ms.getId())) {
return invocation.proceed();
}
Object paramObject = metaObject.getValue("delegate.boundSql.parameterObject");
Object originalVersion = null;
// BaseEntity是父实体类,所有自定义实体类集成了BaseEntity
if (paramObject instanceof BaseEntity) {
Class clazz = paramObject.getClass();
// 获取版本号
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);
}
if (originalVersion != null) {
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<Column> 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;
}
}
初始化拦截器
// 可以在springboot启动类中注册拦截器
@Bean
public Interceptor VersionInterceptor (){
return new versionInterceptor();
}
效果展示
修改数据,Mapper文件中的update方法没有加version字段:
修改update语句自动拼接上version,并且version加1: