SpringBoot-原生Mybatis通过拦截器实现Version乐观锁

乐观锁与悲观锁

乐观锁与悲观锁都是为了解决并发场景下的数据处理问题。乐观锁不会对数据上锁,只是在执行修改时判断在此期间是否有其他人修改了数据,如果有被其他人修改了,则放弃此次修改操作,否则就执行更新操作。悲观锁在操作数据时是直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能进行任何数据操作。

基于以上特性,通常对于读 > 写的场景下,使用乐观锁会有更好的性能。对于写 > 读的场景,使用悲观锁确保数据的安全性,准确性和一致性。

背景

很多年前项目中使用的持久层框架是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:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值