基于SpringBoot + MyBatis-Plus高效实现数据变更记录
前言
在应用开发过程中,在某些情况下,需要实现数据的变更记录。以便于在未来进行数据操作变更的回溯。
常规的做法是在对应修改数据的 service 方法中手动记录数据的变更。这种方式实现起来简单,不用费什么脑子。但却并不是最高效的。
public void updateOrder(Order order) {
Order oldOrder = orderMapper.selectById(order.getId()); // 第一次查询
// ... 一堆业务逻辑 ...
orderMapper.updateById(order); // 业务更新
logService.saveChangeLog(oldOrder, order);
}
试想一下,如果修改数据的 service 方法很多,或者项目在开发快结束的时候临时决定需要额外添加数据变更记录。此时一个个的手动添加无疑是一件费时费力切低效的操作。这样做的后果是 严重侵入业务、重复代码泛滥、事务边界混乱、性能极差。
那有什么办法高效实现数据操作变更记录,而不需要一个一个地方去加呢?答案当然是有,今天小编就给大家介绍下如何高效实现数据的变更记录。
实现原理
我们主要通过拦截器 + 事件驱动架构实现。通过 MyBatis-Plus 的拦截器插件拦截新增、修改、删除操作,发布事件通知,由事件消费者进行消费,并记录到变更记录表中。
该方案的优势:
- 高效,只需要编写拦截器与事件消费者即可,不需要一个个方法修改,避免漏写、写错的情况。
- 无业务代码侵入,完全解耦。
- 高性能,因为是通过异步消息实现记录,不会阻塞原有业务方法。
代码实战
第一步:自定义 MyBatis 拦截器(捕获变更时机)
这是核心钩子,用于在数据发生变更时发布一个事件,而非直接记录。
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
@Component
@Slf4j
public class DataChangeInterceptor implements Interceptor {
@Autowired
private ApplicationEventPublisher eventPublisher; // 事件发布器
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 1. 仅拦截增删改操作
SqlCommandType commandType = ms.getSqlCommandType();
if (commandType == SqlCommandType.INSERT ||
commandType == SqlCommandType.UPDATE ||
commandType == SqlCommandType.DELETE) {
// 2. 获取实体信息(MyBatis-Plus增强)
if (parameter instanceof Map) {
// 处理Wrapper等复杂参数,提取实体
} else if (parameter != null) {
// 3. 关键:在操作执行前,根据ID查询旧数据(仅UPDATE需要)
Object oldData = null;
if (commandType == SqlCommandType.UPDATE) {
oldData = fetchOldData(parameter, ms); // 根据主键查旧数据
}
// 4. 执行原始SQL操作
Object result = invocation.proceed();
// 5. 异步发布变更事件(不阻塞主流程)
if ((int)result > 0) {
DataChangeEvent event = new DataChangeEvent(
this,
commandType,
oldData,
parameter, // 新数据
ThreadLocalUtil.getCurrentOperator() // 操作人从线程上下文获取
);
eventPublisher.publishEvent(event); // 异步处理
}
return result;
}
}
return invocation.proceed();
}
private Object fetchOldData(Object entity, MappedStatement ms) {
// 利用MyBatis-Plus的TableInfo工具类,反射获取主键值和实体类型
TableInfo tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
if (tableInfo != null) {
Object idValue = tableInfo.getPropertyValue(entity, tableInfo.getKeyProperty());
return sqlSessionTemplate.selectOne(ms.getId() + "_selectById", idValue); // 复用Mapper查询
}
return null;
}
}
第二步:设计领域事件(封装变更内容)
事件对象应携带变更的所有元数据。
@Data
public class DataChangeEvent {
private final SqlCommandType changeType; // 操作类型
private final Object oldData; // 变更前数据(JSON字符串或实体)
private final Object newData; // 变更后数据
private final String operator; // 操作人(从ThreadLocal或安全上下文获取)
private final LocalDateTime changeTime = LocalDateTime.now();
private final String entityClassName; // 实体类名
// 关键:将数据转换为JSON,避免后续序列化问题
public String getOldDataJson() {
return JSON.toJSONString(oldData);
}
public String getNewDataJson() {
return JSON.toJSONString(newData);
}
}
第三步:异步事件监听器(真正执行记录)
这是性能关键,必须异步化,且要有降级策略。
@Component
@Slf4j
public class DataChangeEventListener {
@Async("dataChangeExecutor") // 指定独立线程池,不占用业务资源
@EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新事务,与业务事务分离
public void handleDataChangeEvent(DataChangeEvent event) {
try {
// 1. 构建变更记录实体
ChangeLog changeLog = new ChangeLog();
changeLog.setEntityClass(event.getEntityClassName());
changeLog.setChangeType(event.getChangeType().name());
changeLog.setOldData(event.getOldDataJson());
changeLog.setNewData(event.getNewDataJson());
changeLog.setOperator(event.getOperator());
// 2. 计算具体变更的字段(精细化记录)
if (event.getChangeType() == SqlCommandType.UPDATE) {
Map<String, Object> fieldChanges = DiffUtil.diff(
event.getOldDataJson(),
event.getNewDataJson()
);
changeLog.setChangedFields(JSON.toJSONString(fieldChanges));
}
// 3. 持久化到数据库(或发送到消息队列)
changeLogMapper.insert(changeLog);
} catch (Exception e) {
// 4. 降级策略:记录失败时,至少打印日志或存入死信队列
log.error("数据变更记录失败,事件内容:{}", JSON.toJSONString(event), e);
// 可在此处将事件发送至Redis或Kafka进行重试
}
}
}
第四步:关键配置与优化
1. 独立线程池
防止监听器阻塞影响主业务。
spring:
task:
execution:
pool:
data-change-executor:
core-size: 2
max-size: 5
queue-capacity: 1000 # 缓冲区,抗瞬时峰值
2. 变更日志表设计
CREATE TABLE `change_log` ( `id` bigint NOT NULL COMMENT '主键', `entity_class` varchar(255) NOT NULL COMMENT '实体类名', `entity_id` varchar(64) NOT NULL COMMENT '实体ID', -- 从数据中提取 `change_type` varchar(10) NOT NULL COMMENT '操作类型', `changed_fields` json DEFAULT NULL COMMENT '变更的字段(JSON)', -- 快速定位 `old_data` json DEFAULT NULL COMMENT '完整旧数据', `new_data` json DEFAULT NULL COMMENT '完整新数据', `operator` varchar(64) DEFAULT NULL COMMENT '操作人', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_entity` (`entity_class`,`entity_id`), -- 按实体查询 KEY `idx_time` (`create_time`) -- 按时间范围查询 ) COMMENT='数据变更记录表';
3. 操作人信息自动注入
public class OperatorContext {
private static final ThreadLocal<String> CURRENT_OPERATOR = new ThreadLocal<>();
public static void setOperator(String operatorId) {
CURRENT_OPERATOR.set(operatorId);
}
// 在拦截器或Spring Security过滤器中设置
}
总结
- 业务零侵入:业务开发无需关心记录逻辑,专注核心功能。
- 性能无损:主流程仅增加一次事件发布(内存操作),记录过程完全异步化。
- 数据完整:通过拦截器在执行前后捕获数据,保证变更前后的完整快照。
- 架构扩展:事件驱动架构允许你轻松扩展,如:
- 将变更记录同时写入Elasticsearch供快速检索。
- 将重大变更发送消息通知相关系统。
- 实现操作回放功能(通过旧数据+新数据反向操作)。
以上就是基于SpringBoot + MyBatis-Plus高效实现数据变更记录的详细内容,更多关于SpringBoot MyBatis-Plus数据变更记录的资料请关注脚本之家其它相关文章!
相关文章
spring通过filter,Interceptor统一处理ResponseBody的返回值操作
这篇文章主要介绍了spring通过filter,Interceptor统一处理ResponseBody的返回值操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧2020-09-09
为何HashSet中使用PRESENT而不是null作为value
这篇文章主要介绍了为何HashSet中使用PRESENT而不是null作为value,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-10-10
详解mybatis-plus配置找不到Mapper接口路径的坑
这篇文章主要介绍了详解mybatis-plus配置找不到Mapper接口路径的坑,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2020-10-10


最新评论