Files
ProductionDataBaseSync_Data…/README.md
2026-07-15 14:03:27 +08:00

200 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ProductionDataBaseSync_DataMacro
Access → SQL Server 单向增量同步(数据宏驱动)。把各 Access 库的业务数据周期性同步到 SQL Server 镜像表,作为 Access → SQL Server 迁移期的过渡数据层。
设计详见 `docs/superpowers/specs/2026-07-14-access-datamacro-sync-design.md`
## 背景
生产数据实际承载在网络共享下的多个 Access `.accdb`(按车间/年份分库)。旧机制靠客户端前端 VBA 写变更日志,但 VBA 只在特定表单事件触发,批量改表、直接改表等路径会绕过 → 漏数据。
改用 Access **数据宏**(表级引擎触发器,任何写路径必触发):每张业务表挂 `After Insert/Update/Delete`,变更写入各库本地 `TableChangeLog`。本程序就是「读各库日志 → 增量同步到 SQL」的搬运器理论上 100% 捕获、对客户端零侵入。
## 架构
每个 Access 库通过数据宏把变更写入本地 `TableChangeLog`;同步服务周期性把这些变更搬运到 SQL Server 镜像表。单库一轮分三段:
| 阶段 | 动作 | 说明 |
| --- | --- | --- |
| Capture | `SELECT` 读取 `TableChangeLog` 最旧的 N 条I/U 按 `RecordID` 回读整行 | 只读 Access |
| Apply | 调用 `dbo.usp_SyncApply` 写入 SQL 镜像表 | 按 `ID` 精确落库,保序「最后操作胜」 |
| Cleanup | `DELETE` 已应用的 `TableChangeLog` 行 | 按 `ID` 列表精确删除,遇锁自动退避重试 |
关键设计点:
- **无水位线表**——Access 日志「应用成功即删」,日志本身就是待处理队列;`dbo.SyncQueue` 的唯一索引 `(SourceFile, SourceTable, SourceLogID)` 兜底去重,重复捕获幂等。
- **保序「最后操作胜」**——同一 `RecordID` 多次操作(先 Insert 后 Delete 等)按日志顺序取最后一条,保证最终态与 Access 一致。
- **每表一个事务**——单表失败只回滚该表,失败行标 `error` 重试,超限标 `dead` 待人工。
- **`SyncQueue` 长期保留**作审计/重试日志;`applied` 行清理后标 `cleaned`,超保留期再 purge控制表增长。
## 环境要求
- **Python 3.10+**(实测 3.13)。代码用 `X | None` 等新语法。
- **ODBC 驱动**(系统级,非 pip 安装,需预先装好):
- `Microsoft Access Driver (*.accdb, *.mdb)`ACE Redist 2016
- `ODBC Driver 17 for SQL Server`
- **SQL Server ≥ 2017**(存储过程用 `STRING_AGG ... WITHIN GROUP`)。
- 执行账号需对目标表有 `ALTER` 权限(`SET IDENTITY_INSERT` 要求)。
## 安装
```bash
python -m venv .venv
.venv/Scripts/python.exe -m pip install --upgrade pip
.venv/Scripts/python.exe -m pip install -r requirements.txt
```
依赖(`requirements.txt``pyodbc``PyYAML``pydantic``pytest`
## SQL 端部署
首次需在目标库建好暂存表与 apply 存储过程(两个脚本都幂等,可重复执行):
```bash
sqlcmd -S <SERVER>,1433 -U <USER> -P <PASSWORD> -d <DB> -C -N o -i sql/01_sync_queue.sql
sqlcmd -S <SERVER>,1433 -U <USER> -P <PASSWORD> -d <DB> -C -N o -i sql/02_sync_apply.sql
```
- `sql/01_sync_queue.sql`:建 `dbo.SyncQueue` + 去重/清理索引 + `CleanedAt` 列。
- `sql/02_sync_apply.sql``dbo.usp_SyncApply` 集合化 apply 存储过程。
> 连接串/凭据以 `config.yaml` 为准README 不硬编码。
## 配置
编辑 `config.yaml`(从 `config.example.yaml` 复制并填入真实凭据;该文件 gitignored。关键段
- **`sql_server`**`conn_str`ODBC 连接串)与 `sync_queue_table`(默认 `dbo.SyncQueue`)。
- **`access`**`driver`ACE 驱动名)与 `roots`(年份→根目录映射,如 `2026: "\\\\srv\\生产进度表\\2026年数据"`)。
- **`runtime`**`poll_interval_seconds`(轮询间隔)、`capture_batch_size`/`apply_batch_size`/`cleanup_batch_size`(各段批大小)、`max_retries`/`retry_backoff_seconds`(重试)、`cleanup_lock_retries`Access 锁重试次数)、`cleaned_retention_hours``cleaned` 行保留多久后 purge
- **`files`**:每个 Access 文件一条映射:
- `file` / `root`(对应 `access.roots` 的 key/ `schema`SQL 目标 schema
- `year_suffix`:拼到表名后(`2026年数据``_YEAR2026``2025年数据`/合同表用 `""`)。
- `exclude_tables` / `include_tables`:排除/包含规则,**exclude 优先于 include**。`TableChangeLog` 必须排除。
## 命令行
统一入口 `main.py`(仓库根目录),三个功能块都用它调用。配置固定读取同目录的 `config.yaml`,不在命令中指定:
```bash
.venv/Scripts/python.exe main.py fullsync [--db FILE] [--table NAME] [--clear-change-log]
.venv/Scripts/python.exe main.py incremental [--loop] [--poll-interval N]
.venv/Scripts/python.exe main.py compare [--granularity count|ids] [--db FILE] [--table NAME] [--report PATH]
```
| 子命令 | 说明 | 退出码 |
| --- | --- | --- |
| `fullsync` | 一次性全量同步TRUNCATE + 批量 INSERT绕开增量队列。 | 0 |
| `incremental` | 增量同步一轮capture→apply→cleanup`--loop` 切持续轮询(服务模式)。 | 0 |
| `compare` | 数据一致性核对:默认行数总量,`--granularity ids` 精确到 ID 集合差异。 | 全一致 0 / 有不一致 1 |
> 三个块共用同一份 `config.yaml`,目标表集合完全一致(由 `sync.targets` 统一解析)。`main.py` 在根目录、自行把 `src/` 加入 `sys.path`,无需 `-m`、无需设环境变量;控制台强制 UTF-8中文表名不乱码。
>
> 旧入口 `-m sync.service` / `-m sync.fullsync` 保留为兼容,行为不变。
## 全量同步
用于从零重建镜像表或修复 Access 与 SQL 之间的漂移。会**清空目标表再全量写入**,绕开增量队列:
```bash
.venv/Scripts/python.exe main.py fullsync # 全部库、全部表
.venv/Scripts/python.exe main.py fullsync --db OEM.accdb # 仅单个库
.venv/Scripts/python.exe main.py fullsync --table 表壳焊接记录 # 仅单表(作用于所有库)
.venv/Scripts/python.exe main.py fullsync --clear-change-log # 同步后同时清空 TableChangeLog谨慎
```
- `year_suffix` 通过 `FileMapping` 拼到表名后(如 `表壳焊接记录``表壳焊接记录_YEAR2026`)。
- 写入时 `SET IDENTITY_INSERT ON`,保留 Access 原 ID保证后续增量的 `RecordID` 匹配不错位。
- 无镜像表的目标表按设计跳过(`target table missing`),不报错。
## 增量同步
以 Access 数据宏日志为唯一变更源,每轮跑一遍 capture → apply → cleanup三段说明见上面「架构」。这是**主用模式**,生产上常驻运行。两种调用方式:
```bash
.venv/Scripts/python.exe main.py incremental # 跑一轮就退出(手动/按需补跑)
.venv/Scripts/python.exe main.py incremental --loop # 持续轮询(服务模式,不退出)
.venv/Scripts/python.exe main.py incremental --loop --poll-interval 30 # 覆盖 runtime.poll_interval_seconds
```
- 生产环境以 nssm 服务 `DataMacroSync` 常驻(即 `--loop` 模式见下文「NSSM 服务」;手动单轮适合验证或临时补跑积压。
- `--loop` 持续轮询直到进程被停(`nssm stop` 或 Ctrl+C默认单轮跑完即退出。
- 单轮一次最多处理每库 `capture_batch_size` 条日志;积压多时连续跑几轮或用 `--loop` 直到清空。
- 每个文件的 capture/cleanup 独立隔离单文件失败不影响其它apply 失败不阻塞 cleanup失败行 `error` 下轮重试、超 `max_retries``dead` 待人工。
- 幂等:`SyncQueue` 唯一索引去重,重复 capture、中断续跑都不会重写或漏写。
- 与全量同步共用同一份 `config.yaml``sync.targets`,目标表集合完全一致;全量是「从零重建」的补充手段,不替代增量。
## 数据对比
核对 Access 源表与 SQL 镜像表是否一致。两种粒度:
- **行数总量**(默认):逐表比对 `COUNT(*)`
- **ID 集合**`--granularity ids`):逐表比对两边 `ID` 集合报告「Access 有 / SQL 无」与「SQL 有 / Access 无」的 ID每表前 50 个 + 总数)。
```bash
.venv/Scripts/python.exe main.py compare # 全部库、全部表,行数总量
.venv/Scripts/python.exe main.py compare --db 氩弧焊.accdb # 仅单个库
.venv/Scripts/python.exe main.py compare --granularity ids --table 表壳焊接记录 # 单表 ID 级
.venv/Scripts/python.exe main.py compare --report report.txt # 同时写入报告文件UTF-8
```
- 无镜像表按设计跳过(`[SKIPPED no mirror]`),不计为不一致——这类表多半是该排除却没排除(如 `*_停` 停用表、`USysApplicationLog`),可作为配置清理的线索。
- 任一表不一致时退出码 `1`(便于脚本化);全部一致为 `0`
- 实时增量同步存在秒级延迟窗口,刚写入 Access 的行可能尚未到 SQL属正常稍后再核或对照 `SyncQueue` 的 pending 行)。
## NSSM 服务114
增量同步在 host 114 上以 nssm 服务 `DataMacroSync` 常驻运行。常用操作(经 `ssh 114`
```bash
ssh 114 "nssm status DataMacroSync" # 查状态SERVICE_RUNNING / SERVICE_STOPPED
ssh 114 "nssm stop DataMacroSync" # 停
ssh 114 "nssm start DataMacroSync" # 起
ssh 114 "nssm restart DataMacroSync" # 重启
ssh 114 "nssm list" # 列出所有 nssm 服务
```
## 测试
```bash
.venv/Scripts/python.exe -m pytest # 仅单元测试(默认)
RUN_INTEGRATION=1 .venv/Scripts/python.exe -m pytest # 含集成测试(需能连真实 Access + SQL Server
```
- 单元测试用 mock不依赖数据库集成测试`@pytest.mark.integration`)连 `config.yaml` 里的真实库,且自带清理。
- `pyproject.toml` 仅用于配置 pytest`pythonpath = ["src", "."]``testpaths``integration` 标记)。
## 项目结构
```
main.py 统一命令行入口fullsync / incremental / compare
config.yaml 真实配置gitignoredconfig.example.yaml 是模板
requirements.txt 依赖
pyproject.toml pytest 配置
sql/
01_sync_queue.sql dbo.SyncQueue 建表 + 索引(幂等)
02_sync_apply.sql dbo.usp_SyncApply 存储过程
src/sync/
config.py Pydantic 配置模型 + load_config
targets.py 共享目标表解析exclude/include全量/增量/对比共用)
serialize.py Access 值 → JSON 可序列化
access_reader.py 读 Access日志/整行/计数/ID/删除日志)
sql_writer.py 写 SQLSyncQueue/apply/计数/ID/全量灌表)
capture.py 增量编排:读日志→回读整行→入队
cleanup.py 清理编排:回删已应用日志
service.py 主循环 cycle() / run()
fullsync.py 一次性全量同步
compare.py 数据一致性对比count / ids
logging_setup.py 日志配置(滚动文件 + 控制台)
tests/ 单元 + 集成测试
docs/superpowers/ 设计文档与实现计划
```
## 常见问题
- **cleanup 报 `-1102 无法更新;当前被锁定`**Access 是文件型数据库cleanup 反写 `DELETE` 与生产客户端数据宏写日志争用页级锁。服务已对锁冲突自动退避重试(`access_reader.delete_log_ids` 捕获 `pyodbc.Error` 并判断 `-1102`/「被锁定」)。偶发属正常,持续刷错再排查。
- **`No module named 'pydantic_core'` / `pyodbc`**venv 解释器与轮子 ABI 不匹配(常见于 Python 3.13 装到 cp310 轮子)。修复:`.venv/Scripts/python.exe -m pip install --force-reinstall --no-cache-dir pyodbc pydantic`
- **compare/fullsync 报 `[SKIPPED no mirror]` / `target table missing`**:该表在 Access 里但 SQL 端没有镜像表(多为 `*_停` 停用表、`USysApplicationLog` 等系统表,或尚未建镜像的新表)。若是该停用的表,加进对应 `exclude_tables`;若该同步,先在 SQL 建表再 fullsync。
- **`SyncQueue` 出现 `error`/`dead` 行**`error` 会在下轮自动重试(未超 `max_retries``dead` 是超限放弃,需人工看 `ErrorMsg` 排查后处理。
- **`UserWarning: Field name "schema" ... shadows ... BaseModel`**`FileMapping.schema` 字段名与 Pydantic 基类属性重名,仅告警、不影响功能。
- **控制台中文乱码**`main.py` 已强制 stdout/stderr 为 UTF-8若仍乱码设环境变量 `PYTHONIOENCODING=utf-8`,或用 `compare --report` 输出 UTF-8 文件。