
金融场景下的数据归档与合规留存大表 15 年全生命周期管理的实现路径一、监管说交易记录保留 15 年但热数据只需要 3 个月金融监管对数据留存的要求极其严苛——《金融机构客户身份识别和交易记录保存管理办法》要求交易记录至少保存 5 年《反洗钱法》要求大额和可疑交易记录保存不低于 5 年而某些保险、证券产品因合同有效期长达 15-30 年数据也要保留这么久。但对于在线系统来说90%的查询集中在最近 3 个月的数据超过 6 个月的数据几乎只在监管审计时才会被访问。这个矛盾在数据库层面表现为热数据需要 SSD 低延迟访问冷数据只需要便宜存着。把所有数据都放在在线库中成本线性增长但查询价值指数衰减。以大行为例日均交易量在千万级别每笔交易的明细数据约 1KB15 年的数据总量超过 500TB——全部放在高性能 SSD 上存储成本是归档到对象存储的 10-20 倍。二、冷热分层的自动化归档策略分区、压缩与分级存储具体而言系统构建了三层存储架构热数据层0-3 个月保留在 MySQL 在线库中使用 SSD 存储以确保全部索引可用温数据层3-24 个月迁移至 ClickHouse 归档库采用 HDD 存储压缩比可达 10:1 且支持 SQL 查询冷数据层2-15 年则归档至对象存储OSS/S3使用 Parquet 格式压缩比高达 20:1仅支持离线查询。数据流转方面每日通过 pt-archiver 将热数据定时归档至温数据层季度通过 Spark 或 DataX 将温数据归档至冷数据层。查询层则通过统一路由机制根据数据热度自动指向在线库、ClickHouse 或 Presto/Spark 引擎。分区管理是归档策略的基础。在 MySQL 中按月 RANGE 分区是最直接的方式PARTITION BY RANGE (TO_DAYS(txn_date))。每月的新数据写入当月分区3 个月前的分区被标记为待归档。归档操作就是将一个完整分区的数据导出到外部存储然后在 MySQL 中 DROP 该分区——没有行锁性能影响几乎为零。压缩策略按分层递进。温数据层使用 ClickHouse 的 LZ4/ZSTD 压缩加合适的编码时间戳用 Delta、状态码用 Dictionary压缩比通常 8-12:1。冷数据层用 Parquet 格式 snappy 压缩分区多级目录按年/月组织方便按时间范围检索。Parquet 的列存结构支持谓词下推可以在不解压全量数据的情况下过滤出需要的行。三、基于分区表的历史数据归档脚本import pymysql import datetime import loggingfrom typing import List, Optionallogger logging.getLogger(name)class ArchiveManager:MySQL分区表归档管理器def __init__(self, conn: pymysql.Connection): self.conn conn def list_archivable_partitions(self, table: str, retention_months: int 3) - List[str]: 列出应该归档的分区超过保留期的 cutoff datetime.date.today() - datetime.timedelta(daysretention_months * 30) partitions [] cursor self.conn.cursor() try: cursor.execute(f SELECT PARTITION_NAME, PARTITION_DESCRIPTION FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME %s AND PARTITION_NAME IS NOT NULL ORDER BY PARTITION_ORDINAL_POSITION , (table,)) for row in cursor.fetchall(): part_name, part_desc row[0], row[1] try: part_date datetime.datetime.strptime( str(int(part_desc)), %Y%m%d ).date() if part_date cutoff: partitions.append(part_name) except (ValueError, TypeError): logger.warning(fCannot parse partition date: {part_name} {part_desc}) continue finally: cursor.close() return partitions def archive_partition(self, table: str, partition: str, archive_db: str archive_db) - bool: 归档单个分区导出→验证→删除 try: cursor self.conn.cursor() # Step 1: 验证分区行数 cursor.execute(f SELECT COUNT(*) FROM {table} PARTITION ({partition}) ) row_count cursor.fetchone()[0] logger.info(fArchiving partition {partition} with {row_count} rows) # Step 2: 导出到归档库使用INSERT SELECT cursor.execute(f INSERT INTO {archive_db}.{table} SELECT * FROM {table} PARTITION ({partition}) ) # Step 3: 验证归档完整性 cursor.execute(f SELECT COUNT(*) FROM {archive_db}.{table} WHERE partition_key {partition} ) archived_count cursor.fetchone()[0] if archived_count ! row_count: logger.error( fArchive verification failed: foriginal{row_count}, archived{archived_count} ) self.conn.rollback() return False # Step 4: DROP分区 cursor.execute(fALTER TABLE {table} DROP PARTITION {partition}) self.conn.commit() logger.info(fSuccessfully archived partition {partition}) return True except Exception as e: logger.error(fArchive failed for partition {partition}: {e}) self.conn.rollback() return False## 四、归档数据的可查询性保障跨存储介质的透明查询 归档最大的副作用是数据查询路径变复杂——热数据在MySQL温数据在ClickHouse冷数据在对象存储。如果让业务方自己判断去哪查就等同于不可用。跨介质透明查询有两种实现方式。 **统一查询网关**最灵活。类似Presto/Trino的联邦查询引擎将MySQL、ClickHouse和对象存储中的Parquet文件注册为统一的Catalog用户用同一套SQL跨数据源查询。代价是查询优化器的复杂度和跨源JOIN的低效。 **分区路由**更简单。在MySQL中保留一个分区路由表当查询跨越归档边界时如查询过去2年的所有交易应用层根据查询的时间范围拆分为多个子查询——最近3个月查MySQL、3-24个月查ClickHouse、2年以上查离线存储——然后合并结果。这是最务实的方案代价是应用层需要感知分区逻辑。 ## 五、总结 金融数据的归档管理不是一个存储优化问题而是一个合规要求驱动的工程问题。三个核心要素是基于分区的无锁归档避免在线业务受影响、归档完整性验证不能丢了数据还不知道、跨介质查询的工程化方案归档不等于不可查询。在实践中最容易被忽视的是归档数据的可查询性——数据不仅需要存着还需要在监管审计时能在合理时间内查出来。分层存储统一查询入口是这个矛盾的标准解法。