基于SpringBoot + MyBatis-Plus高效實現數據變更記錄
前言
在應用開發(fā)過程中,在某些情況下,需要實現數據的變更記錄。以便于在未來進行數據操作變更的回溯。
常規(guī)的做法是在對應修改數據的 service 方法中手動記錄數據的變更。這種方式實現起來簡單,不用費什么腦子。但卻并不是最高效的。
public void updateOrder(Order order) {
Order oldOrder = orderMapper.selectById(order.getId()); // 第一次查詢
// ... 一堆業(yè)務邏輯 ...
orderMapper.updateById(order); // 業(yè)務更新
logService.saveChangeLog(oldOrder, order);
}
試想一下,如果修改數據的 service 方法很多,或者項目在開發(fā)快結束的時候臨時決定需要額外添加數據變更記錄。此時一個個的手動添加無疑是一件費時費力切低效的操作。這樣做的后果是 嚴重侵入業(yè)務、重復代碼泛濫、事務邊界混亂、性能極差。
那有什么辦法高效實現數據操作變更記錄,而不需要一個一個地方去加呢?答案當然是有,今天小編就給大家介紹下如何高效實現數據的變更記錄。
實現原理
我們主要通過攔截器 + 事件驅動架構實現。通過 MyBatis-Plus 的攔截器插件攔截新增、修改、刪除操作,發(fā)布事件通知,由事件消費者進行消費,并記錄到變更記錄表中。
該方案的優(yōu)勢:
- 高效,只需要編寫攔截器與事件消費者即可,不需要一個個方法修改,避免漏寫、寫錯的情況。
- 無業(yè)務代碼侵入,完全解耦。
- 高性能,因為是通過異步消息實現記錄,不會阻塞原有業(yè)務方法。
代碼實戰(zhàn)
第一步:自定義 MyBatis 攔截器(捕獲變更時機)
這是核心鉤子,用于在數據發(fā)生變更時發(fā)布一個事件,而非直接記錄。
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
@Component
@Slf4j
public class DataChangeInterceptor implements Interceptor {
@Autowired
private ApplicationEventPublisher eventPublisher; // 事件發(fā)布器
@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. 關鍵:在操作執(zhí)行前,根據ID查詢舊數據(僅UPDATE需要)
Object oldData = null;
if (commandType == SqlCommandType.UPDATE) {
oldData = fetchOldData(parameter, ms); // 根據主鍵查舊數據
}
// 4. 執(zhí)行原始SQL操作
Object result = invocation.proceed();
// 5. 異步發(fā)布變更事件(不阻塞主流程)
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,避免后續(xù)序列化問題
public String getOldDataJson() {
return JSON.toJSONString(oldData);
}
public String getNewDataJson() {
return JSON.toJSONString(newData);
}
}
第三步:異步事件監(jiān)聽器(真正執(zhí)行記錄)
這是性能關鍵,必須異步化,且要有降級策略。
@Component
@Slf4j
public class DataChangeEventListener {
@Async("dataChangeExecutor") // 指定獨立線程池,不占用業(yè)務資源
@EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新事務,與業(yè)務事務分離
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. 持久化到數據庫(或發(fā)送到消息隊列)
changeLogMapper.insert(changeLog);
} catch (Exception e) {
// 4. 降級策略:記錄失敗時,至少打印日志或存入死信隊列
log.error("數據變更記錄失敗,事件內容:{}", JSON.toJSONString(event), e);
// 可在此處將事件發(fā)送至Redis或Kafka進行重試
}
}
}
第四步:關鍵配置與優(yōu)化
1. 獨立線程池
防止監(jiān)聽器阻塞影響主業(yè)務。
spring:
task:
execution:
pool:
data-change-executor:
core-size: 2
max-size: 5
queue-capacity: 1000 # 緩沖區(qū),抗瞬時峰值
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 '創(chuàng)建時間', 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過濾器中設置
}
總結
- 業(yè)務零侵入:業(yè)務開發(fā)無需關心記錄邏輯,專注核心功能。
- 性能無損:主流程僅增加一次事件發(fā)布(內存操作),記錄過程完全異步化。
- 數據完整:通過攔截器在執(zhí)行前后捕獲數據,保證變更前后的完整快照。
- 架構擴展:事件驅動架構允許你輕松擴展,如:
- 將變更記錄同時寫入Elasticsearch供快速檢索。
- 將重大變更發(fā)送消息通知相關系統(tǒng)。
- 實現操作回放功能(通過舊數據+新數據反向操作)。
以上就是基于SpringBoot + MyBatis-Plus高效實現數據變更記錄的詳細內容,更多關于SpringBoot MyBatis-Plus數據變更記錄的資料請關注腳本之家其它相關文章!
相關文章
Java Web開發(fā)防止多用戶重復登錄的完美解決方案
在web項目開發(fā)中,很多情況下都可以讓同一個賬號信息在不同的登錄入口登錄很多次,這樣子做的不是很完善。一般解決這種情況有兩種解決方案,小編呢主要以第二種方式給大家介紹具體的實現方法,對java web 防止多用戶重復登錄的解決方案感興趣的朋友一起看看吧2016-11-11
解決IntelliJ IDEA創(chuàng)建spring boot無法連接http://start.spring.io/問題
這篇文章主要介紹了解決IntelliJ IDEA創(chuàng)建spring boot無法連接http://start.spring.io/問題,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
NoHttpResponseException問題排查解決記錄分析
這篇文章主要為大家介紹了NoHttpResponseException問題排查解決記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08

