Skip to content

Commit b444675

Browse files
committed
*: Add active-active doc
1 parent fca56d5 commit b444675

9 files changed

+407
-1
lines changed

TOC.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
- [日志过滤器](/ticdc/ticdc-filter.md)
179179
- [DDL 同步](/ticdc/ticdc-ddl.md)
180180
- [双向复制](/ticdc/ticdc-bidirectional-replication.md)
181+
- [Active-Active 表](/active-active-table.md)
181182
- 监控告警
182183
- [基本监控指标](/ticdc/ticdc-summary-monitor.md)
183184
- [详细监控指标](/ticdc/monitor-ticdc.md)

active-active-table.md

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
---
2+
title: Active-Active 表
3+
summary: 介绍 Active-Active 表在 TiDB 层面的作用、创建方式、相关系统变量与限制。
4+
---
5+
6+
# Active-Active 表
7+
8+
Active-Active 表是 TiDB 为 Active-Active(双活)同步场景提供的表能力。它通过隐藏列记录写入时间戳,并结合软删除(`SOFTDELETE`)机制,为多集群多写场景下的冲突解决(Last Write Wins,LWW)提供基础能力。
9+
10+
> **说明:**
11+
>
12+
> 本文档仅介绍 Active-Active 表在 TiDB 层如何创建和使用。如何配置 TiCDC 进行双向同步请参阅相关文档。
13+
14+
## 使用前提
15+
16+
- 你需要部署多个 TiDB 集群,并在集群间部署 TiCDC 同步链路(用于跨集群同步变更)。
17+
- 你需要确保各集群的 PD 生成的时间戳在全局范围内可比较且不会冲突。为此,需要为每个集群配置 PD 的 `tso-max-index``tso-unique-index`(详见 [PD 配置文件](/pd-configuration-file.md#tso-max-index)[PD 配置文件](/pd-configuration-file.md#tso-unique-index))。
18+
- 建议为各集群配置 NTP 等时间同步机制,避免因时钟漂移导致事务提交失败或等待时间过长。
19+
20+
> **注意:**
21+
>
22+
> Active-Active 同步不提供跨集群的全局事务一致性,属于最终一致性方案。对于同一行的并发写入可能产生“丢失更新”等现象,请谨慎评估业务适用性。
23+
24+
## 创建 Active-Active 表
25+
26+
Active-Active 表通过表选项 `ACTIVE_ACTIVE='ON'` 启用,并且**必须同时启用软删除**`SOFTDELETE=RETENTION ...`)。`SOFTDELETE` 选项仅支持 `RETENTION ...``'OFF'`,不支持 `'ON'`
27+
28+
### 通过数据库选项统一启用(推荐)
29+
30+
你可以在创建数据库时启用 `ACTIVE_ACTIVE``SOFTDELETE`,该数据库下新创建的表会自动继承这些选项:
31+
32+
```sql
33+
CREATE DATABASE aa_example ACTIVE_ACTIVE='ON' SOFTDELETE=RETENTION 7 DAY;
34+
35+
USE aa_example;
36+
CREATE TABLE message (
37+
id INT PRIMARY KEY,
38+
text VARCHAR(10)
39+
);
40+
```
41+
42+
通过 `SHOW CREATE TABLE` 可以看到这些选项会以注释形式展示(用于 MySQL 兼容),例如:
43+
44+
```sql
45+
SHOW CREATE TABLE message\G
46+
```
47+
48+
示例输出如下(内容会包含 `/*T![active_active] ACTIVE_ACTIVE='ON' */``/*T![softdelete] SOFTDELETE=RETENTION 7 DAY */` 等片段):
49+
50+
```text
51+
*************************** 1. row ***************************
52+
Table: message
53+
Create Table: CREATE TABLE `message` (
54+
`id` int NOT NULL,
55+
`text` varchar(10) DEFAULT NULL,
56+
PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */
57+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin /*T![active_active] ACTIVE_ACTIVE='ON' */ /*T![softdelete] SOFTDELETE=RETENTION 7 DAY */ /*T![softdelete] SOFTDELETE_JOB_ENABLE='ON' */ /*T![softdelete] SOFTDELETE_JOB_INTERVAL='24h' */
58+
```
59+
60+
### 在建表时单独启用
61+
62+
你也可以在建表时直接指定选项:
63+
64+
```sql
65+
CREATE TABLE message (
66+
id INT PRIMARY KEY,
67+
text VARCHAR(10)
68+
) ACTIVE_ACTIVE='ON' SOFTDELETE=RETENTION 7 DAY;
69+
```
70+
71+
### 调整 Soft Delete 相关选项
72+
73+
你可以通过 `ALTER TABLE` 调整 `SOFTDELETE` 的保留期,或配置软删除后台清理任务的开关与执行间隔:
74+
75+
```sql
76+
-- 调整保留期
77+
ALTER TABLE message SOFTDELETE=RETENTION 14 DAY;
78+
79+
-- 配置清理任务开关与执行间隔(例如 '24h'、'30m')
80+
ALTER TABLE message SOFTDELETE_JOB_ENABLE='ON' SOFTDELETE_JOB_INTERVAL='24h';
81+
```
82+
83+
> **注意:**
84+
>
85+
> Active-Active 表不支持将 `SOFTDELETE` 设置为 `'OFF'`,否则会报错。
86+
87+
## 隐藏列与冲突解决(LWW)
88+
89+
启用 Active-Active 后,TiDB 会为表增加一个隐藏列 `_tidb_origin_ts`,用于记录该行数据在上游集群的原始提交时间戳(TiCDC 写入下游时填充)。当 `_tidb_origin_ts``NULL` 时表示该行由本地事务写入;当 `_tidb_origin_ts` 不为 `NULL` 时表示该行由上游变更同步而来。
90+
91+
同时,TiDB 提供一个只读列 `_tidb_commit_ts` 用于查询该行在本地集群的提交时间戳。`_tidb_commit_ts` 不属于真实表结构,不能用于 `ADD COLUMN``ADD INDEX` 等 DDL。
92+
93+
在冲突处理时,可以用如下表达式表示一行数据用于 LWW 冲突解决的时间戳:
94+
95+
```sql
96+
IFNULL(_tidb_origin_ts, _tidb_commit_ts)
97+
```
98+
99+
示例:
100+
101+
```sql
102+
DROP TABLE IF EXISTS message_lww;
103+
CREATE TABLE message_lww (
104+
id INT PRIMARY KEY,
105+
text VARCHAR(10)
106+
) ACTIVE_ACTIVE='ON' SOFTDELETE=RETENTION 7 DAY;
107+
108+
INSERT INTO message_lww VALUES (1, 'local'), (2, 'up');
109+
110+
-- 为了展示效果,这里通过手动写入 _tidb_origin_ts 来模拟 TiCDC 在下游写入时填充该列。正式环境下不建议修改 _tidb_origin_ts 列,否则可能导致 Active-Active 同步结果不一致。
111+
UPDATE message_lww SET _tidb_origin_ts=464677399908313272 WHERE id=2;
112+
113+
SELECT
114+
id,
115+
_tidb_origin_ts,
116+
_tidb_commit_ts,
117+
IFNULL(_tidb_origin_ts, _tidb_commit_ts) AS lww_ts
118+
FROM message_lww
119+
ORDER BY id;
120+
```
121+
122+
```text
123+
+----+--------------------+--------------------+--------------------+
124+
| id | _tidb_origin_ts | _tidb_commit_ts | lww_ts |
125+
+----+--------------------+--------------------+--------------------+
126+
| 1 | <null> | 464677389206814721 | 464677389206814721 |
127+
| 2 | 464677399908313272 | 464677437959045121 | 464677399908313272 |
128+
+----+--------------------+--------------------+--------------------+
129+
```
130+
131+
- `id=1``_tidb_origin_ts``NULL`,表示该行由本地事务写入,此时 `lww_ts` 取值来自 `_tidb_commit_ts`
132+
- `id=2``_tidb_origin_ts` 不为 `NULL`,表示该行由上游变更同步而来,此时 `lww_ts` 取值来自 `_tidb_origin_ts`
133+
134+
### 本地写入覆盖上游写入时的行为
135+
136+
当你在本地集群对一行“来自上游”的数据执行写入(例如 `UPDATE`)时,TiDB 会把这次写入视为本地写入,并将该行的 `_tidb_origin_ts` 重置为 `NULL`。同时,本地事务的提交时间戳会保证大于该行的“LWW 时间戳”(即更新前的 `IFNULL(_tidb_origin_ts, _tidb_commit_ts)`),以避免旧写入覆盖新写入。若本地 PD 分配的 TSO 落后于该行的“LWW 时间戳”,事务提交可能会短暂等待重试;在时钟漂移较大时,事务也可能失败。
137+
138+
示例(继续使用上一节的 `message_lww`):
139+
140+
```sql
141+
SELECT id, text, _tidb_origin_ts FROM message_lww ORDER BY id;
142+
143+
UPDATE message_lww SET text='local2' WHERE id=2;
144+
145+
SELECT id, text, _tidb_origin_ts FROM message_lww ORDER BY id;
146+
```
147+
148+
```text
149+
+----+-------+--------------------+
150+
| id | text | _tidb_origin_ts |
151+
+----+-------+--------------------+
152+
| 1 | local | NULL |
153+
| 2 | up | 464677399908313272 |
154+
+----+-------+--------------------+
155+
+----+--------+-----------------+
156+
| id | text | _tidb_origin_ts |
157+
+----+--------+-----------------+
158+
| 1 | local | NULL |
159+
| 2 | local2 | NULL |
160+
+----+--------+-----------------+
161+
```
162+
163+
> **注意:**
164+
>
165+
> 不建议业务显式修改 `_tidb_origin_ts`,否则可能导致 Active-Active 同步结果不一致。
166+
167+
## 软删除语义与数据恢复
168+
169+
Active-Active 表必须启用 `SOFTDELETE`。启用软删除后,TiDB 会增加隐藏列 `_tidb_softdelete_time`,并通过系统变量 [`tidb_translate_softdelete_sql`](/system-variables.md#tidb_translate_softdelete_sql) 控制软删除语义:
170+
171+
-`tidb_translate_softdelete_sql=ON`(默认)时:
172+
- `DELETE` 会被重写为更新 `_tidb_softdelete_time`(标记为软删除)。
173+
- `SELECT` 会自动过滤软删除数据。
174+
- 查询中不能显式引用 `_tidb_softdelete_time`
175+
-`tidb_translate_softdelete_sql=OFF` 时:
176+
- 软删除数据不会被自动过滤,你可以查询或写入 `_tidb_softdelete_time`
177+
178+
> **注意:**
179+
>
180+
> 不要在 `tidb_translate_softdelete_sql=OFF` 的情况下对 Active-Active 表(或启用 `SOFTDELETE` 的表)执行 `DELETE` 操作,否则可能会造成 Active-Active 同步的不一致。
181+
182+
### 通过 EXPLAIN 查看 DML 改写
183+
184+
`tidb_translate_softdelete_sql=ON` 时,你可以通过 `EXPLAIN` 观察到 TiDB 对软删除表 DML 的改写:
185+
186+
插入(`INSERT`):
187+
188+
```sql
189+
EXPLAIN INSERT INTO message (id, text) VALUES (1, 'hello');
190+
```
191+
192+
```text
193+
+----------+---------+------+---------------+----------------------------------------------------------------------------------+
194+
| id | estRows | task | access object | operator info |
195+
+----------+---------+------+---------------+----------------------------------------------------------------------------------+
196+
| Insert_1 | N/A | root | | ReplaceConflictIfExpr: not(isnull(aa_example.message._tidb_softdelete_time)) |
197+
+----------+---------+------+---------------+----------------------------------------------------------------------------------+
198+
```
199+
200+
其中 `ReplaceConflictIfExpr` 表示 `INSERT` 在软删除表上会额外处理“主键冲突但该行已软删除”的情况,从而符合软删除语义。
201+
202+
更新(`UPDATE`):
203+
204+
```sql
205+
EXPLAIN UPDATE message SET text='world' WHERE id=1;
206+
```
207+
208+
```text
209+
+---------------------+---------+------+---------------+------------------------------------------------------+
210+
| id | estRows | task | access object | operator info |
211+
+---------------------+---------+------+---------------+------------------------------------------------------+
212+
| Update_4 | N/A | root | | N/A |
213+
| └─Selection_7 | 0.00 | root | | isnull(aa_example.message._tidb_softdelete_time) |
214+
| └─Point_Get_6 | 1.00 | root | table:message | handle:1 |
215+
+---------------------+---------+------+---------------+------------------------------------------------------+
216+
```
217+
218+
其中 `Selection` 节点会追加 `isnull(_tidb_softdelete_time)` 过滤条件,确保 `UPDATE` 只作用于未软删除的数据。
219+
220+
删除(`DELETE`):
221+
222+
```sql
223+
EXPLAIN DELETE FROM message WHERE id=1;
224+
```
225+
226+
```text
227+
+---------------------+---------+------+---------------+------------------------------------------------------+
228+
| id | estRows | task | access object | operator info |
229+
+---------------------+---------+------+---------------+------------------------------------------------------+
230+
| Update_4 | N/A | root | | N/A |
231+
| └─Selection_7 | 0.00 | root | | isnull(aa_example.message._tidb_softdelete_time) |
232+
| └─Point_Get_6 | 1.00 | root | table:message | handle:1 |
233+
+---------------------+---------+------+---------------+------------------------------------------------------+
234+
```
235+
236+
`DELETE` 的执行计划会显示为 `Update`,表示 `DELETE` 会被改写为更新 `_tidb_softdelete_time`(软删除标记),而非物理删除。
237+
238+
### 软删除与恢复示例
239+
240+
下面示例展示 `DELETE` 执行软删除后的效果,以及如何通过 `RECOVER VALUES` 恢复数据:
241+
242+
```sql
243+
DROP TABLE IF EXISTS message_recover;
244+
CREATE TABLE message_recover (
245+
id INT PRIMARY KEY,
246+
text VARCHAR(10)
247+
) ACTIVE_ACTIVE='ON' SOFTDELETE=RETENTION 7 DAY;
248+
249+
INSERT INTO message_recover VALUES (1,'hello');
250+
DELETE FROM message_recover WHERE id=1;
251+
252+
-- 关闭语义转换,仅用于查看内部隐藏列(不要在 OFF 时执行 DELETE)
253+
SET @@tidb_translate_softdelete_sql=OFF;
254+
SELECT id, text, _tidb_softdelete_time FROM message_recover;
255+
256+
SET @@tidb_translate_softdelete_sql=ON;
257+
RECOVER VALUES FROM message_recover WHERE id = 1;
258+
SELECT * FROM message_recover;
259+
```
260+
261+
示例输出如下(其中 `_tidb_softdelete_time` 的值会随执行时间变化):
262+
263+
```text
264+
+----+-------+----------------------------+
265+
| id | text | _tidb_softdelete_time |
266+
+----+-------+----------------------------+
267+
| 1 | hello | 2026-03-04 11:15:44.843440 |
268+
+----+-------+----------------------------+
269+
270+
+----+-------+
271+
| id | text |
272+
+----+-------+
273+
| 1 | hello |
274+
+----+-------+
275+
```
276+
277+
软删除数据在保留期到期后,会由后台清理任务执行物理删除(Hard Delete)。你可以通过全局变量 [`tidb_softdelete_job_enable`](/system-variables.md#tidb_softdelete_job_enable) 控制是否调度该清理任务。
278+
279+
## 监控与排查
280+
281+
- 只读 SESSION 变量 [`tidb_cdc_active_active_sync_stats`](/system-variables.md#tidb_cdc_active_active_sync_stats) 仅供 TiCDC 读取,用于获取 Active-Active 同步的冲突跳过统计信息。
282+
- `INFORMATION_SCHEMA.TIDB_SOFTDELETE_TABLE_STATS` 用于查看 Soft Delete 表的行数估算与软删除行数估算(依赖统计信息)。
283+
284+
## 使用限制
285+
286+
- Active-Active 表必须同时启用 `SOFTDELETE`
287+
- Active-Active 表必须显式指定主键。
288+
- Active-Active 表与 SoftDelete 表当前不支持 `UNIQUE` 索引(包括 `ADD UNIQUE INDEX``CREATE UNIQUE INDEX`)。
289+
- Active-Active 表与 SoftDelete 表不支持外键(`FOREIGN KEY`)。
290+
- SoftDelete 表不支持多表 `DELETE ... JOIN ...` 等涉及多表的 `DELETE` 语句。
291+
- Active-Active 表与 SoftDelete 表不支持临时表(`TEMPORARY TABLE` / `GLOBAL TEMPORARY TABLE`)。
292+
- 目前不支持通过 `ALTER TABLE` 修改表的 `ACTIVE_ACTIVE` 启用状态。
293+
- 不支持通过 DDL 删除、重命名或修改 `_tidb_origin_ts``_tidb_softdelete_time` 等内部隐藏列。

keywords.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ TiDB 从 v7.5.3 和 v7.6.0 开始提供 [`INFORMATION_SCHEMA.KEYWORDS`](/informa
6161

6262
- ACCOUNT
6363
- ACTION
64+
- ACTIVE_ACTIVE
6465
- ADD (R)
6566
- ADMIN
6667
- ADVISE
@@ -573,6 +574,7 @@ TiDB 从 v7.5.3 和 v7.6.0 开始提供 [`INFORMATION_SCHEMA.KEYWORDS`](/informa
573574
- RESTORES
574575
- RESTRICT (R)
575576
- RESUME
577+
- RETENTION
576578
- REUSE
577579
- REVERSE
578580
- REVOKE (R)
@@ -626,6 +628,9 @@ TiDB 从 v7.5.3 和 v7.6.0 开始提供 [`INFORMATION_SCHEMA.KEYWORDS`](/informa
626628
- SLOW
627629
- SMALLINT (R)
628630
- SNAPSHOT
631+
- SOFTDELETE
632+
- SOFTDELETE_JOB_ENABLE
633+
- SOFTDELETE_JOB_INTERVAL
629634
- SOME
630635
- SOURCE
631636
- SPATIAL (R)

pd-configuration-file.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ PD 配置文件比命令行参数支持更多的选项。你可以在 [conf/conf
120120
+ 默认值:50ms
121121
+ 最小值:1ms
122122

123+
### `tso-max-index`
124+
125+
+ 用于在多 PD 集群场景下生成全局唯一的 TSO。`tso-max-index`[`tso-unique-index`](#tso-unique-index) 共同决定 TSO 逻辑部分的分配方式,从而避免不同集群产生的 TSO 冲突。
126+
+ 默认值:0
127+
+`tso-max-index` 设置为非 0 值时,需要为每个 PD 集群设置不同的 `tso-unique-index`,并确保 `tso-unique-index` 的取值不超过 `tso-max-index`
128+
+ 该配置常用于 Active-Active 场景下跨集群比较时间戳,更多信息请参考 [Active-Active 表](/active-active-table.md)
129+
130+
### `tso-unique-index`
131+
132+
+ 用于在多 PD 集群场景下标识当前 PD 集群的唯一索引。不同集群必须配置不同值,以避免生成的 TSO 冲突。
133+
+ 默认值:0
134+
+ 通常需要与 [`tso-max-index`](#tso-max-index) 配合使用。例如,若有 N 个集群,建议将 `tso-max-index` 设置为 N,并将各集群的 `tso-unique-index` 设置为 1..N(每个集群不同)。
135+
+ 该配置常用于 Active-Active 场景下跨集群比较时间戳,更多信息请参考 [Active-Active 表](/active-active-table.md)
136+
123137
## pd-server
124138

125139
pd-server 相关配置项。

sql-statements/sql-statement-alter-database.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@ AlterDatabaseStmt ::=
1515
1616
DatabaseOption ::=
1717
DefaultKwdOpt ( CharsetKw '='? CharsetName | 'COLLATE' '='? CollationName | 'ENCRYPTION' '='? EncryptionOpt )
18+
| ActiveActiveOption
19+
| SoftDeleteOption
20+
| SoftDeleteJobEnableOption
21+
| SoftDeleteJobIntervalOption
22+
23+
ActiveActiveOption ::=
24+
"ACTIVE_ACTIVE" EqOpt ( 'ON' | 'OFF' )
25+
26+
SoftDeleteOption ::=
27+
"SOFTDELETE" EqOpt "RETENTION" NUM TimeUnit
28+
| "SOFTDELETE" EqOpt 'OFF'
29+
30+
SoftDeleteJobEnableOption ::=
31+
"SOFTDELETE_JOB_ENABLE" EqOpt ( 'ON' | 'OFF' )
32+
33+
SoftDeleteJobIntervalOption ::=
34+
"SOFTDELETE_JOB_INTERVAL" EqOpt stringLit
1835
```
1936

2037
## 示例

sql-statements/sql-statement-alter-table.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ AlterTableSpec ::=
5353
| 'REMOVE' 'TTL'
5454
| TTLEnable EqOpt ( 'ON' | 'OFF' )
5555
| TTLJobInterval EqOpt stringLit
56+
| 'SOFTDELETE' EqOpt 'RETENTION' NUM TimeUnit
57+
| 'SOFTDELETE' EqOpt 'OFF'
58+
| 'SOFTDELETE_JOB_ENABLE' EqOpt ( 'ON' | 'OFF' )
59+
| 'SOFTDELETE_JOB_INTERVAL' EqOpt stringLit
5660
)
5761
| PlacementPolicyOption
5862

0 commit comments

Comments
 (0)