📝 docs: add sync mechanism documentation with Mermaid diagrams
Add documentation for incremental sync (change-log driven polling, per-PK verification, fault recovery) and full sync (batch transfer, auto table creation, IDENTITY_INSERT handling). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
210
docs/incremental-sync.md
Normal file
210
docs/incremental-sync.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 增量同步机制 (run_incremental_sync.py)
|
||||
|
||||
## 概述
|
||||
|
||||
增量同步是一个**长轮询服务**,持续监控 SQL Server 中的 `TableChangeLog` 变更日志表,将 Access 数据源的变更实时同步到 SQL Server 目标表。
|
||||
|
||||
- **入口脚本**: `run_incremental_sync.py`
|
||||
- **计划任务**: `AutoRun-run_incremental_sync`(用户登录时自动启动)
|
||||
- **轮询间隔**: 30 秒(`POLL_INTERVAL`)
|
||||
|
||||
## 整体架构
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph 数据源侧
|
||||
A[Access .accdb 文件] -->|VBA 宏写入日志| B[TableChangeLog]
|
||||
end
|
||||
|
||||
subgraph 增量同步服务
|
||||
B -->|轮询 Synced=0| C[run_incremental_sync.py]
|
||||
C -->|读最新数据| A
|
||||
C -->|DELETE + INSERT| D[SQL Server 目标表]
|
||||
C -->|标记 Synced=1| B
|
||||
end
|
||||
|
||||
subgraph 监控
|
||||
C -->|心跳| E[Uptime Kuma]
|
||||
end
|
||||
```
|
||||
|
||||
## 变更日志驱动
|
||||
|
||||
Access 端的 VBA 宏在数据变更时,向 `TableChangeLog` 表写入一条记录:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `LogID` | 自增主键 |
|
||||
| `TableAddress` | Access 文件路径(多种格式) |
|
||||
| `TableName` | 发生变更的 Access 表名 |
|
||||
| `RecordID` | 变更记录的主键值 |
|
||||
| `Synced` | 同步标记,0=未同步,1=已同步 |
|
||||
|
||||
### 路径匹配策略
|
||||
|
||||
VBA 端写入的 `TableAddress` 有多种格式,系统构造 4 种候选值进行匹配:
|
||||
|
||||
```
|
||||
;DATABASE=\\192.168.110.114\生产进度表\成品入库.accdb ← 网络路径前缀
|
||||
LOCAL=\\server\share\file.accdb ← 本地等号前缀
|
||||
LOCAL:\\server\share\file.accdb ← 本地冒号前缀
|
||||
\\192.168.110.114\生产进度表\成品入库.accdb ← 裸路径
|
||||
```
|
||||
|
||||
## 核心同步流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([服务启动]) --> POLL[轮询 TableChangeLog<br/>WHERE Synced=0]
|
||||
|
||||
POLL -->|无记录| SLEEP[休眠 POLL_INTERVAL 秒]
|
||||
SLEEP --> POLL
|
||||
|
||||
POLL -->|发现未同步记录| GROUP[按 TableName 分组<br/>合并同一表的 record_ids]
|
||||
|
||||
GROUP --> CONNECT[连接 Access 数据库]
|
||||
|
||||
CONNECT --> FOR_EACH{{遍历每个表}}
|
||||
|
||||
FOR_EACH --> CHECK_TABLE{目标表是否存在?}
|
||||
|
||||
CHECK_TABLE -->|不存在| AUTO_CREATE[自动建表<br/>ensure_schema + create_table_from_access]
|
||||
CHECK_TABLE -->|存在| READ_ACCESS
|
||||
AUTO_CREATE --> READ_ACCESS
|
||||
|
||||
READ_ACCESS[A. 从 Access 读取最新数据<br/>SELECT WHERE PK IN ...]
|
||||
|
||||
READ_ACCESS --> EXTRACT_PK[提取 inserted_pk_set<br/>Access 实际返回的主键集合]
|
||||
|
||||
EXTRACT_PK --> DELETE[B. 删除目标表旧记录<br/>DELETE WHERE PK IN ...]
|
||||
|
||||
DELETE --> HAS_NEW{{有新数据?}}
|
||||
|
||||
HAS_NEW -->|有| IDENTITY_CHECK{含 IDENTITY 列?}
|
||||
IDENTITY_CHECK -->|是| ID_ON[SET IDENTITY_INSERT ON]
|
||||
IDENTITY_CHECK -->|否| INSERT
|
||||
ID_ON --> INSERT[C. 批量插入新记录<br/>executemany]
|
||||
INSERT --> ID_OFF[SET IDENTITY_INSERT OFF]
|
||||
ID_OFF --> VERIFY
|
||||
|
||||
HAS_NEW -->|无| VERIFY
|
||||
|
||||
VERIFY[D. 提交前逐主键校验] --> VERIFY_OK{校验通过?}
|
||||
|
||||
VERIFY_OK -->|通过| MARK_SYNCED[E. 标记 Synced=1]
|
||||
MARK_SYNCED --> COMMIT[F. 提交事务]
|
||||
COMMIT --> POST_VERIFY
|
||||
|
||||
POST_VERIFY{提交后复核<br/>ENABLE_POST_COMMIT_VERIFY} -->|关闭| SUCCESS
|
||||
POST_VERIFY -->|开启| POST_OK{复核通过?}
|
||||
POST_OK -->|通过| SUCCESS[记录同步成功日志]
|
||||
POST_OK -->|失败| REVERT[回滚 Synced=0<br/>下一轮重试]
|
||||
|
||||
VERIFY_OK -->|失败| ROLLBACK[回滚事务<br/>保持 Synced=0]
|
||||
ROLLBACK --> NEXT_TABLE
|
||||
|
||||
SUCCESS --> NEXT_TABLE
|
||||
REVERT --> NEXT_TABLE
|
||||
|
||||
NEXT_TABLE{{下一张表?}} -->|是| FOR_EACH
|
||||
NEXT_TABLE -->|否| CLOSE_ACC[关闭 Access 连接]
|
||||
CLOSE_ACC --> SUMMARY[输出文件级汇总]
|
||||
SUMMARY --> POLL
|
||||
|
||||
style VERIFY fill:#f9f,stroke:#333
|
||||
style POST_VERIFY fill:#f9f,stroke:#333
|
||||
style AUTO_CREATE fill:#bbf,stroke:#333
|
||||
```
|
||||
|
||||
## 逐主键校验机制
|
||||
|
||||
传统的"数量对比"方法存在缺陷:少插和漏删的错误可能互相抵消。本系统采用**逐主键确认**方式:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph 输入
|
||||
A[record_ids<br/>日志中的主键列表]
|
||||
B[inserted_pk_set<br/>Access 实际读到的主键]
|
||||
end
|
||||
|
||||
subgraph 删除校验
|
||||
C[差集: record_ids - inserted_pk_set<br/>= 应删除的主键]
|
||||
D[查询目标表<br/>这些主键是否还存在?]
|
||||
C --> D
|
||||
D -->|还存在任一条| E[❌ 校验失败]
|
||||
D -->|全部不存在| F[✅ 删除校验通过]
|
||||
end
|
||||
|
||||
subgraph 插入校验
|
||||
G[查询目标表<br/>inserted_pk_set 是否都在?]
|
||||
G -->|有任一条查不到| E
|
||||
G -->|全部存在| H[✅ 插入校验通过]
|
||||
end
|
||||
|
||||
A --> C
|
||||
B --> C
|
||||
B --> G
|
||||
```
|
||||
|
||||
### 校验函数说明
|
||||
|
||||
| 函数 | 作用 |
|
||||
|------|------|
|
||||
| `fetch_existing_pks()` | 分批查询目标表,返回实际存在的主键集合(IN 子句每批不超过 900 个参数) |
|
||||
| `verify_sync_result()` | 执行删除校验 + 插入校验,失败时抛出 `SyncVerificationError` |
|
||||
| `_norm_key()` | 主键归一化为字符串,规避 Access/SQL Server 类型差异 |
|
||||
| `_chunked()` | 将列表分批,避免超出 SQL Server 参数上限 |
|
||||
|
||||
## 故障恢复
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Pending: VBA 写入日志 Synced=0
|
||||
Pending --> Syncing: 同步服务读取
|
||||
Syncing --> Verified: 校验通过 + 提交
|
||||
Verified --> Committed: Synced=1 标记生效
|
||||
Committed --> [*]: 同步完成
|
||||
|
||||
Syncing --> Rollback: 校验失败 / 异常
|
||||
Rollback --> Pending: 事务回滚 Synced=0<br/>下一轮自动重试
|
||||
|
||||
Committed --> Pending: 提交后复核失败<br/>Synced 撤回为 0
|
||||
```
|
||||
|
||||
### 三层保障
|
||||
|
||||
1. **提交前校验** — 删/插完成后、标记 Synced=1 前,逐主键确认结果正确
|
||||
2. **事务回滚** — 校验失败或异常时回滚事务,保持 `Synced=0`,下一轮自动重试
|
||||
3. **提交后复核** — `commit()` 后再查一次数据库确认数据已持久化(可通过 `ENABLE_POST_COMMIT_VERIFY` 开关控制)
|
||||
|
||||
## 轮询策略
|
||||
|
||||
```
|
||||
while True:
|
||||
has_work = process_sync_task()
|
||||
|
||||
if has_work:
|
||||
sleep(0.1) # 有积压,快速重试
|
||||
else:
|
||||
sleep(30) # 无工作,标准间隔
|
||||
```
|
||||
|
||||
有未处理数据时以 0.1 秒间隔快速处理积压;无数据时按 `POLL_INTERVAL` 休眠。
|
||||
|
||||
## 自动建表
|
||||
|
||||
当目标表在 SQL Server 中不存在时,系统根据 Access 表的列定义自动建表:
|
||||
|
||||
```
|
||||
Access cursor.description → 类型映射 → CREATE TABLE 语句
|
||||
```
|
||||
|
||||
| Access 类型 | SQL Server 类型 |
|
||||
|-------------|----------------|
|
||||
| `int` | `INT`(主键时追加 `IDENTITY(1,1) PRIMARY KEY`) |
|
||||
| `float` | `FLOAT` |
|
||||
| `bool` | `BIT` |
|
||||
| `datetime.datetime` | `DATETIME` |
|
||||
| `decimal.Decimal` | `DECIMAL(p, s)` |
|
||||
| `str` (size ≤ 4000) | `NVARCHAR(size)` |
|
||||
| `str` (size > 4000) | `NVARCHAR(MAX)` |
|
||||
Reference in New Issue
Block a user