📝 docs: document unified CLI, incremental sync, and compare
This commit is contained in:
191
README.md
191
README.md
@@ -1,50 +1,199 @@
|
|||||||
# ProductionDataBaseSync_DataMacro
|
# ProductionDataBaseSync_DataMacro
|
||||||
|
|
||||||
Access → SQL Server 增量同步(数据宏驱动)。设计详见 `docs/superpowers/specs/2026-07-14-access-datamacro-sync-design.md`。
|
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 镜像表。单库一轮分三段:
|
每个 Access 库通过数据宏把变更写入本地 `TableChangeLog`;同步服务周期性把这些变更搬运到 SQL Server 镜像表。单库一轮分三段:
|
||||||
|
|
||||||
| 阶段 | 动作 | 说明 |
|
| 阶段 | 动作 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Capture | `SELECT` 读取 `TableChangeLog` 中最旧的 N 条 | 只读 Access |
|
| Capture | `SELECT` 读取 `TableChangeLog` 最旧的 N 条,I/U 按 `RecordID` 回读整行 | 只读 Access |
|
||||||
| Apply | 调用 `dbo.usp_SyncApply` 写入 SQL 镜像表 | 按 `ID` 精确落库 |
|
| Apply | 调用 `dbo.usp_SyncApply` 写入 SQL 镜像表 | 按 `ID` 精确落库,保序「最后操作胜」 |
|
||||||
| Cleanup | `DELETE` 已应用的 `TableChangeLog` 行 | 按 `ID` 列表精确删除,遇锁自动退避重试 |
|
| Cleanup | `DELETE` 已应用的 `TableChangeLog` 行 | 按 `ID` 列表精确删除,遇锁自动退避重试 |
|
||||||
|
|
||||||
## 命令
|
关键设计点:
|
||||||
|
- **无水位线表**——Access 日志「应用成功即删」,日志本身就是待处理队列;`dbo.SyncQueue` 的唯一索引 `(SourceFile, SourceTable, SourceLogID)` 兜底去重,重复捕获幂等。
|
||||||
|
- **保序「最后操作胜」**——同一 `RecordID` 多次操作(先 Insert 后 Delete 等)按日志顺序取最后一条,保证最终态与 Access 一致。
|
||||||
|
- **每表一个事务**——单表失败只回滚该表,失败行标 `error` 重试,超限标 `dead` 待人工。
|
||||||
|
- **`SyncQueue` 长期保留**作审计/重试日志;`applied` 行清理后标 `cleaned`,超保留期再 purge,控制表增长。
|
||||||
|
|
||||||
| 用途 | 命令 |
|
## 环境要求
|
||||||
| --- | --- |
|
|
||||||
| 增量同步服务(常驻,由 nssm 托管 `DataMacroSync`) | `.venv/Scripts/python.exe -m sync.service` |
|
|
||||||
| 一次性全量同步(TRUNCATE + 全量 INSERT,所有库所有表) | `.venv/Scripts/python.exe -m sync.fullsync config.yaml` |
|
|
||||||
|
|
||||||
> 上述命令均使用项目自带的 `.venv`。同步类命令由 nssm 以服务方式运行,无需手动设置环境变量。
|
- **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` 复制并填入真实凭据)。关键段:
|
编辑 `config.yaml`(从 `config.example.yaml` 复制并填入真实凭据;该文件 gitignored)。关键段:
|
||||||
|
|
||||||
- `sql_server`:SQL Server 连接串与 `SyncQueue` 表名。
|
- **`sql_server`**:`conn_str`(ODBC 连接串)与 `sync_queue_table`(默认 `dbo.SyncQueue`)。
|
||||||
- `access`:ACE ODBC 驱动名与各根目录(`roots`)映射。
|
- **`access`**:`driver`(ACE 驱动名)与 `roots`(年份→根目录映射,如 `2026: "\\\\srv\\生产进度表\\2026年数据"`)。
|
||||||
- `runtime`:轮询间隔、批大小、重试与保留策略。
|
- **`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` / `schema` / `year_suffix` / `exclude_tables` / `include_tables`)。
|
- **`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 之间的漂移。会**清空目标表再全量写入**,绕开增量队列:
|
用于从零重建镜像表或修复 Access 与 SQL 之间的漂移。会**清空目标表再全量写入**,绕开增量队列:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/Scripts/python.exe -m sync.fullsync config.yaml # 全部库、全部表
|
.venv/Scripts/python.exe main.py fullsync # 全部库、全部表
|
||||||
.venv/Scripts/python.exe -m sync.fullsync config.yaml --db OEM.accdb # 仅单个库
|
.venv/Scripts/python.exe main.py fullsync --db OEM.accdb # 仅单个库
|
||||||
.venv/Scripts/python.exe -m sync.fullsync config.yaml --table 表壳焊接记录 # 仅单表(作用于所有库)
|
.venv/Scripts/python.exe main.py fullsync --table 表壳焊接记录 # 仅单表(作用于所有库)
|
||||||
.venv/Scripts/python.exe -m sync.fullsync config.yaml --clear-change-log # 同步后同时清空 TableChangeLog(谨慎)
|
.venv/Scripts/python.exe main.py fullsync --clear-change-log # 同步后同时清空 TableChangeLog(谨慎)
|
||||||
```
|
```
|
||||||
|
|
||||||
- `year_suffix` 通过 `FileMapping` 拼到表名后(如 `表壳焊接记录` → `表壳焊接记录_YEAR2026`)。
|
- `year_suffix` 通过 `FileMapping` 拼到表名后(如 `表壳焊接记录` → `表壳焊接记录_YEAR2026`)。
|
||||||
|
- 写入时 `SET IDENTITY_INSERT ON`,保留 Access 原 ID,保证后续增量的 `RecordID` 匹配不错位。
|
||||||
- 无镜像表的目标表按设计跳过(`target table missing`),不报错。
|
- 无镜像表的目标表按设计跳过(`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 真实配置(gitignored);config.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 写 SQL(SyncQueue/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`/“被锁定”)。偶发属正常,持续刷错再排查。
|
- **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`。
|
- **`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 文件。
|
||||||
|
|||||||
Reference in New Issue
Block a user