3. Outbox 表结构设计:标准字段定义与索引策略
Outbox 表是分布式事务中“发件箱模式”的核心载体,其结构设计直接决定了数据写入、状态轮询、清理归档以及下游消费的性能。一个标准且经过压测验证的 Outbox 表,需要在字段定义上兼顾业务语义与存储效率,在索引策略上平衡写入吞吐与查询速度。
3.1 标准 Outbox 表字段定义
以下字段定义基于业界通用实践(如 Transactional Outbox 模式参考实现),并针对高并发压测场景进行了优化调整。每个字段的选择都对应特定的压测关注点。
| 字段名 | 类型(MySQL/PostgreSQL) | 约束 | 压测关注点 |
|---|---|---|---|
| id | BIGINT UNSIGNED / BIGSERIAL | PRIMARY KEY | 自增主键保证写入顺序,避免页分裂;压测中关注自增锁竞争 |
| aggregate_id | VARCHAR(64) / UUID | NOT NULL, INDEX | 业务聚合根标识,用于去重与幂等;压测中关注 UUID 随机性对索引插入性能的影响 |
| event_type | VARCHAR(128) | NOT NULL, INDEX | 事件类型(如 OrderCreated);压测中关注枚举值分布对索引选择性的影响 |
| payload | JSON / JSONB / TEXT | NOT NULL | 事件体;压测中关注大 payload 对 binlog 复制延迟和磁盘 IO 的影响 |
| created_at | DATETIME(3) / TIMESTAMPTZ | NOT NULL, INDEX | 记录创建时间;压测中关注时间范围查询的索引下推效率 |
| status | TINYINT / SMALLINT | NOT NULL, DEFAULT 0, INDEX | 状态枚举(0=待发送, 1=已发送, 2=发送失败);压测中关注状态过滤的索引选择性 |
| version | INT UNSIGNED | NOT NULL, DEFAULT 1 | 乐观锁版本号;压测中关注 CAS 更新时的冲突率与重试开销 |
| trace_id | VARCHAR(64) | NULLABLE, INDEX | 全链路追踪 ID;压测中关注可选字段对存储碎片的影响 |
压测建议:在压测准备阶段,建议将
payload 字段的测试数据大小设置为 1KB、4KB、16KB 三档,分别模拟轻量事件、中等事件和富事件场景,以观察不同负载下的磁盘吞吐与网络传输瓶颈。
3.2 索引策略:写入与查询的平衡艺术
Outbox 表的索引设计需要在“快速写入”与“高效轮询”之间找到平衡点。过多的索引会拖慢 INSERT 性能,过少的索引则会导致轮询查询变成全表扫描。以下索引策略经过压测验证,适用于大多数业务场景。
3.2.1 核心索引组合
- 主键索引(id):自增 BIGINT,保证写入顺序,减少页分裂。压测中建议关闭自增锁的 innodb_autoinc_lock_mode=2(交错模式)以提升并发写入吞吐。
- 状态+时间联合索引(status, created_at):这是轮询查询的核心索引。压测中,该索引用于快速定位“待发送”且“最早创建”的记录,避免扫描大量已发送数据。
- 聚合ID索引(aggregate_id):用于幂等性检查和去重查询。如果业务中大量使用幂等写入,该索引的命中率会很高。
3.2.2 索引策略对比表
| 索引类型 | 索引列 | 压测写入吞吐影响 | 压测轮询延迟影响 | 适用场景 |
|---|---|---|---|---|
| 单列索引 | status | 低(1个B+树维护) | 中(需回表过滤时间) | 低并发、小数据量 |
| 联合索引 | (status, created_at) | 中(2列B+树维护) | 低(索引内完成排序过滤) | 高并发、大数据量(推荐) |
| 覆盖索引 | (status, created_at, id) | 中高(3列维护) | 极低(无需回表) | 极高并发轮询、对延迟极度敏感 |
| 函数索引 | (DATE(created_at)) | 低(函数计算开销) | 低(按天分区查询) | 按天清理归档场景 |
压测避坑:不要在
payload 字段上建立索引。JSON 或 TEXT 类型的索引不仅占用大量空间,还会严重拖慢写入性能。如果需要对 payload 内部字段进行过滤,请考虑使用 MySQL 8.0+ 的 JSON 多值索引或 PostgreSQL 的 GIN 索引,但务必在压测中验证其写入开销。
3.3 Outbox 表索引结构示意图
下图展示了 Outbox 表的核心索引结构及其在压测中的查询路径。联合索引 (status, created_at) 是轮询查询的关键,它允许数据库直接定位到最早待发送的记录,而无需扫描全表。
3.4 压测场景下的索引优化建议
- 避免冗余索引:不要单独为
status和created_at分别建立单列索引,联合索引已经覆盖了这两种查询模式。冗余索引会增加写入时的索引维护开销。 - 监控索引选择性:在压测中,通过
SHOW INDEX FROM outbox观察Cardinality值。如果status字段的区分度很低(例如 90% 的数据都是“待发送”),联合索引的效果会下降,此时可以考虑引入shard_id或partition_key作为索引前缀。 - 考虑索引覆盖:如果轮询查询只需要返回
id、aggregate_id和payload,可以将联合索引扩展为覆盖索引(status, created_at, id, aggregate_id, payload),避免回表操作。但需在压测中验证索引体积膨胀对写入性能的影响。 - 分区表与索引协同:对于日增量超过千万级的压测场景,建议使用 RANGE 分区(按
created_at按月或按周分区),配合局部索引。压测中需关注分区裁剪是否生效,以及跨分区查询的性能退化。
压测关键指标:在 10 万 TPS 写入并发下,联合索引
(status, created_at) 的 B+Tree 深度应控制在 3 层以内(约 2000 万条数据以内)。如果深度超过 4 层,轮询查询的延迟会显著上升,此时应考虑分区或分表策略。
3.5 字段与索引的 DDL 示例
以下 DDL 语句展示了标准 Outbox 表的完整定义,包含推荐的索引策略。该 DDL 已在压测环境中验证,适用于 MySQL 8.0+ 和 PostgreSQL 15+。
-- MySQL 8.0+ 标准 Outbox 表 DDL
CREATE TABLE outbox (
id BIGINT UNSIGNED AUTO_INCREMENT,
aggregate_id VARCHAR(64) NOT NULL COMMENT '聚合根ID',
event_type VARCHAR(128) NOT NULL COMMENT '事件类型',
payload JSON NOT NULL COMMENT '事件体',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
status TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '状态:0=待发送,1=已发送,2=失败',
version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '乐观锁版本',
trace_id VARCHAR(64) DEFAULT NULL COMMENT '追踪ID',
PRIMARY KEY (id),
INDEX idx_status_time (status, created_at),
INDEX idx_aggregate_id (aggregate_id),
INDEX idx_trace_id (trace_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='分布式事务Outbox表';
-- PostgreSQL 15+ 标准 Outbox 表 DDL
CREATE TABLE outbox (
id BIGSERIAL,
aggregate_id VARCHAR(64) NOT NULL,
event_type VARCHAR(128) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status SMALLINT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
trace_id VARCHAR(64) DEFAULT NULL,
PRIMARY KEY (id)
);
CREATE INDEX idx_outbox_status_time ON outbox (status, created_at);
CREATE INDEX idx_outbox_aggregate_id ON outbox (aggregate_id);
CREATE INDEX idx_outbox_trace_id ON outbox (trace_id);
压测准备:在压测开始前,建议使用
ALTER TABLE outbox ENGINE=InnoDB; 重建表以消除碎片,并使用 ANALYZE TABLE outbox; 更新统计信息,确保优化器能选择正确的索引。