前言
在应用开发过程中,在某些情况下,需要实现数据的变更记录。以便于在未来进行数据操作变更的回溯。
常规的做法是在对应修改数据的 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数据变更记录的资料请关注代码网其它相关文章!
发表评论