档案系统升级日志不全?数据库与代码双重修复指南
一、根因分析:为什么升级日志会丢失
在档案管理系统的升级过程中,日志不完整通常由两个核心原因导致。第一是事务回滚机制,当业务代码执行失败抛出异常时,数据库事务回滚,导致插入日志表的 SQL 也被撤销,虽然业务没做成功,但“尝试升级”这个动作本身应该被记录。第二是异步日志延迟,部分系统采用异步线程记录日志以提升性能,若主线程在日志写入前结束或崩溃,日志就会丢失。
本文将提供一套“数据库触发器 + Spring AOP 事务同步”的双重修复方案,确保无论代码是否抛出异常,无论是否异步,操作日志都能 100% 落盘。
二、方案一:数据库层面触发器兜底(强制记录)
此方案不依赖应用层代码逻辑,直接在数据库层面通过触发器捕获数据变更。即使应用层代码因为 Bug 导致日志未插入,只要核心业务表发生了变动,触发器就会强制将变更记录到日志表中。
1. 创建独立的日志表
我们需要一张结构足够详细的日志表,用于存储触发器捕获的信息。请直接在 MySQL 中执行以下 SQL 建表语句:
```sql CREATE TABLE `sys_upgrade_log_trigger` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `module_name` varchar(64) DEFAULT NULL COMMENT '升级模块名称', `operation_type` varchar(20) DEFAULT NULL COMMENT '操作类型:UPDATE/INSERT/DELETE', `old_data` text COMMENT '变更前数据(JSON格式)', `new_data` text COMMENT '变更后数据(JSON格式)', `operator` varchar(64) DEFAULT NULL COMMENT '操作人', `operate_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', `client_ip` varchar(50) DEFAULT NULL COMMENT '客户端IP', PRIMARY KEY (`id`), KEY `idx_module` (`module_name`), KEY `idx_time` (`operate_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库触发器强制记录的升级日志'; ```2. 编写核心业务表的触发器
假设档案系统的核心配置表名为 `sys_archive_config`。我们需要创建一个 AFTER UPDATE 触发器。当配置表发生更新时,自动将变更写入日志表。
注意:为了获取“变更前”的数据,必须使用 OLD 关键字;为了获取“变更后”的数据,必须使用 NEW 关键字。执行以下 SQL:
```sql DELIMITER $$ CREATE TRIGGER `tr_sys_archive_config_update` AFTER UPDATE ON `sys_archive_config` FOR EACH ROW BEGIN DECLARE v_operator VARCHAR(64); DECLARE v_ip VARCHAR(50); -- 尝试从当前会话变量中获取用户信息(需应用层配合设置,见下文) SET v_operator = IF(@current_user_id IS NULL, 'SYSTEM_TRIGGER', @current_user_id); SET v_ip = IF(@current_client_ip IS NULL, 'UNKNOWN', @current_client_ip); -- 插入日志 INSERT INTO sys_upgrade_log_trigger ( module_name, operation_type, old_data, new_data, operator, client_ip ) VALUES ( 'sys_archive_config', 'UPDATE', CONCAT('{', 'id:', OLD.id, ', version:', OLD.version, ', config:', OLD.config_value, '}'), CONCAT('{', 'id:', NEW.id, ', version:', NEW.version, ', config:', NEW.config_value, '}'), v_operator, v_ip ); END$$ DELIMITER ; ```3. 应用层配合设置会话变量
为了让触发器知道当前是谁在操作,你需要在执行数据库操作前,设置 MySQL 的会话变量。在你的 DAO 或 Repository 层执行业务 SQL 前,加入以下代码片段:
```java // 在Service层方法开始处调用 public void updateConfig(ConfigDTO dto) { // 设置会话变量供触发器使用 jdbcTemplate.execute("SET @current_user_id = '" + SecurityContextHolder.getCurrentUser().getId() + "'"); jdbcTemplate.execute("SET @current_client_ip = '" + WebUtils.getClientIP() + "'"); // 执行业务更新 configMapper.updateById(dto); } ```三、方案二:应用层 Spring AOP 事务同步(解决回滚丢失)
数据库触发器虽然能兜底,但无法记录详细的“业务描述”(例如“升级了OCR识别引擎版本”)。这需要应用层解决。为了解决事务回滚导致日志丢失的问题,必须使用 Spring 的 TransactionSynchronizationManager。
1. Maven 依赖引入

确保你的 pom.xml 中包含 Spring AOP 和 Aspects 依赖(Spring Boot 通常默认包含,但需检查版本):
```xml2. 定义自定义注解 @UpgradeLog
创建一个注解,用于标记在需要进行日志记录的 Service 方法上:
```java package com.archive.annotation; import java.lang.annotation.; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface UpgradeLog { String module(); // 模块名称 String operation(); // 操作描述 } ```3. 编写 AOP 切面核心代码
这是最关键的代码。它利用 Spring 的事务同步机制,将日志插入操作挂起到事务提交之后执行。这样,即使业务代码抛出异常导致事务回滚,我们依然可以选择记录日志;或者仅在事务成功提交后记录。
创建类 UpgradeLogAspect.java:
```java package com.archive.aspect; import com.archive.annotation.UpgradeLog; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManagerManager; import javax.annotation.Resource; import java.lang.reflect.Method; @Aspect @Component public class UpgradeLogAspect { @Resource private UpgradeLogMapper upgradeLogMapper; // 假设你有一个Mapper直接操作日志表 @Around("@annotation(com.archive.annotation.UpgradeLog)") public Object handleLog(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); UpgradeLog annotation = method.getAnnotation(UpgradeLog.class); long startTime = System.currentTimeMillis(); Object result = null; Throwable exception = null; try { // 1. 执行业务逻辑 result = joinPoint.proceed(); return result; } catch (Throwable e) { exception = e; throw e; // 异常必须抛出,确保事务正常回滚 } finally { // 2. 捕获执行状态 long costTime = System.currentTimeMillis() - startTime; final boolean isSuccess = (exception == null); // 3. 构建日志对象 final LogEntity logEntity = new LogEntity(); logEntity.setModule(annotation.module()); logEntity.setOperation(annotation.operation()); logEntity.setParams(getParamsJson(joinPoint.getArgs())); // 需自行实现参数转JSON logEntity.setStatus(isSuccess ? 1 : 0); logEntity.setErrorMsg(exception != null ? exception.getMessage() : ""); logEntity.setCostTime(costTime); logEntity.setCreateTime(new Date()); // 4. 核心逻辑:注册事务同步 if (TransactionSynchronizationManager.isActualTransactionActive()) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCompletion(int status) { // status: 0=提交成功, 1=回滚 // 策略:无论成功还是回滚,都记录日志(根据需求可调整) // 如果只想记录成功的,判断 status == 0 saveLogInNewThread(logEntity); } }); } else { // 如果没有事务,直接保存 saveLogInNewThread(logEntity); } } } private void saveLogInNewThread(LogEntity log) { // 建议使用独立线程或线程池写入日志,防止日志系统故障影响主业务 new Thread(() -> { try { upgradeLogMapper.insert(log); } catch (Exception e) { e.printStackTrace(); // 记录日志失败处理,如写入本地文件 } }).start(); } } ```4. 实际使用示例
在你的 Service 方法上加上注解即可:
```java @Service public class ArchiveUpgradeService { @Transactional(rollbackFor = Exception.class) @UpgradeLog(module = "档案核心服务", operation = "执行V2.0版本数据结构升级") public void upgradeStructure() { // 1. 修改表结构 // 2. 迁移数据 // 3. 如果这里抛出异常,事务会回滚,但上面的AOP会在afterCompletion中依然记录日志 if (true) { throw new RuntimeException("模拟升级失败"); } } } ```四、方案三:历史数据回补脚本(修复已丢失数据)
如果系统已经运行了一段时间,日志表中已经缺失了大量数据,我们可以通过对比“业务表的 update_time”和“日志表的记录时间”,利用 SQL 脚本生成缺失的日志记录。
假设 sys_archive_config 表有 update_time 字段。以下脚本用于找出那些发生了变动但日志表中没有对应时间点记录的数据,并将其插入日志表。
```sql INSERT INTO sys_upgrade_log_trigger ( module_name, operation_type, new_data, operator, operate_time, client_ip ) SELECT 'sys_archive_config' AS module_name, 'DATA_RECOVERY' AS operation_type, CONCAT('Recovered data for ID: ', id) AS new_data, 'ADMIN_RECOVERY_SCRIPT' AS operator, update_time AS operate_time, '127.0.0.1' AS client_ip FROM sys_archive_config WHERE -- 条件:业务表的更新时间 大于 1天前 update_time > DATE_SUB(NOW(), INTERVAL 1 DAY) -- 且 不存在于日志表中(根据ID或时间匹配,这里简化逻辑) AND update_time NOT IN ( SELECT operate_time FROM sys_upgrade_log_trigger WHERE module_name = 'sys_archive_config' ); ```执行上述 SQL 前,请务必先备份 sys_upgrade_log_trigger 表。执行后,检查插入的行数,即为修复成功的日志条数。
五、实施检查清单
为了确保方案落地,请按照以下顺序逐一检查:
- 数据库环境检查:确认 MySQL 版本大于 5.7,支持触发器。
- 触发器部署:执行方案一的建表和建触发器 SQL,并手动修改一条 sys_archive_config 数据,验证日志表是否自动新增记录。
- 会话变量测试:在应用层调用更新接口,检查触发器写入的 operator 字段是否为当前用户 ID,而非 'SYSTEM_TRIGGER'。
- AOP 配置:确保 UpgradeLogAspect 类被 Spring 扫描到(通常放在主程序包及其子包下)。
- 事务回滚测试:在带有 @UpgradeLog 注解的方法中手动抛出异常,检查日志表是否记录了 status=0 的失败日志,且没有被回滚掉。
- 数据恢复:在测试环境执行方案三的回补脚本,确认逻辑正确后,再在生产环境执行。