统一归档

简介: 该工具提供数据库归档功能,支持RENAME全表归档与分页迁移两种模式,具备事务安全、断点续传、避免深分页等特性,结合Redis记录进度,确保数据一致性与系统稳定性。
package com.archive.plugin;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class ArchivePlugin {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final int PAGE_SIZE = 1000;

    /**
     * 模式1: RENAME 归档
     * @param tableName 原表名
     */
    public void archiveByRename(String tableName) {
        String historyTable = tableName + "_history";
        
        log.info("开始 RENAME 归档: {} -> {}", tableName, historyTable);

        // 1. 重命名表
        jdbcTemplate.execute("RENAME TABLE " + tableName + " TO " + historyTable);
        log.info("表已重命名: {} -> {}", tableName, historyTable);

        // 2. 根据历史表重建新表
        jdbcTemplate.execute("CREATE TABLE " + tableName + " LIKE " + historyTable);
        log.info("新表已创建: {}", tableName);
    }

    /**
     * 模式2: 分页迁移归档
     * @param tableName 原表名
     * @param whereCondition SQL WHERE 条件,如 "created_at < '2024-01-01'"
     */
    public void archiveByPagination(String tableName, String whereCondition) {
        String historyTable = tableName + "_history";
        String cursorKey = "archive:cursor:" + tableName;

        log.info("开始分页归档: {} -> {}, 条件: {}", tableName, historyTable, whereCondition);

        // 确保历史表存在
        ensureHistoryTableExists(tableName, historyTable);

        long lastId = getLastId(cursorKey);
        int totalArchived = 0;

        while (true) {
            // 分页查询
            String sql = String.format(
                "SELECT * FROM %s WHERE id > %d AND (%s) ORDER BY id ASC LIMIT %d",
                tableName, lastId, whereCondition, PAGE_SIZE
            );

            List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
            if (rows.isEmpty()) {
                log.info("归档完成,共归档 {} 条记录", totalArchived);
                break;
            }

            // 事务迁移
            int archived = archiveBatch(tableName, historyTable, rows);
            totalArchived += archived;

            // 更新游标
            lastId = (Long) rows.get(rows.size() - 1).get("id");
            saveLastId(cursorKey, lastId);

            log.info("已归档 {} 条,当前游标 lastId: {}", totalArchived, lastId);
        }

        // 清除游标
        redisTemplate.delete(cursorKey);
    }

    /**
     * 批量归档(事务控制)
     */
    @Transactional(rollbackFor = Exception.class)
    public int archiveBatch(String tableName, String historyTable, List<Map<String, Object>> rows) {
        for (Map<String, Object> row : rows) {
            // 插入历史表
            insertIntoHistory(historyTable, row);
            
            // 删除源表
            Long id = (Long) row.get("id");
            jdbcTemplate.update("DELETE FROM " + tableName + " WHERE id = ?", id);
        }
        return rows.size();
    }

    /**
     * 插入历史表
     */
    private void insertIntoHistory(String historyTable, Map<String, Object> row) {
        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();
        Object[] params = new Object[row.size()];
        int i = 0;

        for (Map.Entry<String, Object> entry : row.entrySet()) {
            if (i > 0) {
                columns.append(", ");
                values.append(", ");
            }
            columns.append(entry.getKey());
            values.append("?");
            params[i++] = entry.getValue();
        }

        String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", 
            historyTable, columns, values);
        jdbcTemplate.update(sql, params);
    }

    /**
     * 确保历史表存在
     */
    private void ensureHistoryTableExists(String tableName, String historyTable) {
        String checkSql = "SHOW TABLES LIKE '" + historyTable + "'";
        List<Map<String, Object>> result = jdbcTemplate.queryForList(checkSql);
        
        if (result.isEmpty()) {
            jdbcTemplate.execute("CREATE TABLE " + historyTable + " LIKE " + tableName);
            log.info("历史表已创建: {}", historyTable);
        }
    }

    /**
     * 获取游标
     */
    private long getLastId(String key) {
        String val = redisTemplate.opsForValue().get(key);
        return val == null ? 0L : Long.parseLong(val);
    }

    /**
     * 保存游标
     */
    private void saveLastId(String key, long lastId) {
        redisTemplate.opsForValue().set(key, String.valueOf(lastId));
    }
}

四、核心特性

特性 实现方式
事务安全 @Transactional 保证 Insert + Delete 原子性
断点续传 记录 lastIdarchive_progress
避免深分页 使用 id > lastId 而非 OFFSET
表结构自动复制 CREATE TABLE LIKE
两种归档模式 RENAME(全表) + 分页迁移(条件)

五、优化建议

  1. 异步执行:用 @Async 或线程池执行归档任务
  2. 限流保护:每批次间加 Thread.sleep(100) 避免数据库压力
  3. 监控告警:归档进度慢或失败时发送告警
  4. 支持回滚:保留归档表,必要时可逆向恢复
  5. 多表并行:多个表可并行归档(注意锁表风险)
目录
相关文章
|
缓存 运维 安全
【干货】桌面运维当中,我最常见遇到的几个问题!
作为体制内单位的信息化部门,不管大小事凡是涉及到信息化相关的都会来找我们,平常碰到最多的当然是电脑使用方面的了,比如什么C盘满了让我们帮忙清一下,电脑太慢了让我们帮忙看看啥的,一般新来的小伙子们就会被分配去干这些事情,但是由于在大学或者研究生阶段若非兴趣使然其实很难去了解计算机的一些基础运维知识,这里我也整理了自己常用的一些命令和技巧,帮助小伙伴快速入门。这篇文章主要是针对Windows操作系统而言的,因为目前大部分还依然使用的是Windows操作系统哈
【干货】桌面运维当中,我最常见遇到的几个问题!
|
1月前
|
Linux 数据安全/隐私保护 Windows
CentOS-6.3-x86_64-minimal 安装教程详细步骤新手入门指南(附安装包)
准备2G以上U盘及ISO刻录工具,下载CentOS 6.3 minimal版镜像,使用Rufus或dd写入U盘。将U盘插入目标电脑,通过BIOS选择U盘启动,进入安装界面后按提示选择语言、键盘布局,自动分区并设置主机名、网络及时区,配置root密码后开始安装。安装完成后重启,拔出U盘,以root账号登录系统,即可使用命令行进行操作。
398 157
|
3月前
|
人工智能 自然语言处理 安全
Playwright MCP 与 Claude 的完美协作:打造网页操作智能体
当AI大脑遇上灵活机械臂:Claude与Playwright MCP深度融合,实现自然语言驱动浏览器自动化。从搜索论文到登录发帖,复杂网页操作一气呵成,打造真正“能想会做”的智能数字助手。
|
2月前
|
存储 安全 固态存储
四款WIN PE工具,都可以实现U盘安装教程
Windows PE是基于NT内核的轻量系统,用于系统安装、分区管理及故障修复。本文推荐多款PE制作工具,支持U盘启动,兼容UEFI/Legacy模式,具备备份还原、驱动识别等功能,操作简便,适合新旧电脑维护使用。
1467 109
|
1月前
|
安全 网络架构
对比外部公网IP与局域网内部IP的差异性
综上所述,外部公网IP地址与局域网内部IP地址在功能、应用范围、安全性与管理方式上存在明显的差异性。公网IP地址为网络设备提供了在整个互联网中可识别的唯一身份,而内网IP仅在私有网络中有效,且安全性相对较高。理解这些差异能有助于更好地配合网络地址的规划、管理与安全策略的设计。
202 8
|
2月前
|
安全 C++ Windows
使用教程!Geek UninstallerV1.5.3.170 深度卸载神器完全使用指南!彻底告别软件残留
Geek Uninstaller是一款免费、轻量级的深度卸载工具,能彻底清除软件残留文件和注册表项,解决系统卡慢、空间占用等问题。支持普通卸载、强制卸载及Windows预装应用清理,操作简单,无需安装,一键释放硬盘空间,提升系统性能。
547 8
|
2月前
|
监控 安全 数据安全/隐私保护
U盘如何防泄密?这几个技术手段迎刃而解
安得卫士提供U盘防泄密四大核心措施:准入控制、操作管控、行为审计与离线防护。通过注册授权、权限细分、敏感数据拦截、全流程操作审计及加密外发控制,实现U盘数据全周期安全防护,有效防范数据泄露风险。
165 6
|
2月前
|
人工智能 自然语言处理 搜索推荐
2025年汽车行业AIGC供应商推荐
2025年智能出行浪潮下,AIGC视频供应商加速汽车行业智能化转型。集之互动、可灵、即梦凭借全栈式解决方案、数据驱动与AI交互优化,助力车企降本增效,提升用户体验与合规安全,推动产业创新发展。
168 1
|
2月前
|
运维 数据安全/隐私保护 内存技术
NVMe 盘故障排查 5 步速查
通过系统日志、NVMe健康状态、错误日志及Pangu集群摘要等多维度诊断NVMe盘故障,判断是否持续报错、介质损坏或文件系统异常,结合硬件与集群状态精准定位问题,指导及时换盘或观察处理。
122 0