|
| 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` 等内部隐藏列。 |
0 commit comments