| name | consistency-multitenancy |
| description | 设计事务边界、一致性策略、多租户隔离和数据生命周期时使用。适用于高并发写入、幂等、多租户 SaaS、软删归档和审计字段设计。融合 ACID + Saga + 乐观锁 + RLS + Tenant Per Schema。 |
一致性与多租户(Consistency & Multi-tenancy)
参考来源:Pat Helland《Life beyond Distributed Transactions》、Martin Kleppmann《Designing Data-Intensive Applications》、AWS SaaS 多租户白皮书、Stripe / Slack / Notion 多租户实践。
适用场景
- 判断事务边界和一致性要求
- 设计幂等写入、并发冲突处理、乐观锁
- 设计多租户字段、租户隔离、复合唯一约束
- 设计软删、归档、审计字段和数据生命周期
- 评估跨表、跨服务、跨数据库的一致性方案
- 行级安全(RLS)配置
- Tenant Per Schema vs Shared Schema 选型
核心原则
1. 把隔离边界和一致性要求显式写入 schema 与约束
"tenant_id 字段" ≠ "多租户隔离"
还需要:查询过滤 + 唯一约束 + 权限边界 + 测试
2. 事务不应无限扩大
边界应等于业务不变量边界
3. 幂等和唯一性优先由数据库约束兜底
不靠应用代码判重
4. 不在事务里做外部调用
HTTP / 邮件 / 第三方 API 都不能放事务
5. 强一致 vs 最终一致:业务驱动
涉及钱:强一致
不影响财务:最终一致 + 补偿
6. 多租户三层防御:应用层 + 数据库层 + 测试层
单层防御必定漏
7. 软删除是甜蜜陷阱
破坏唯一约束、查询要带 WHERE deleted_at IS NULL
事务(ACID)速查
A - Atomicity(原子性):要么全成,要么全败
C - Consistency(一致性):事务前后业务规则一致
I - Isolation(隔离性):并发事务互不干扰
D - Durability(持久性):提交后数据不丢
隔离级别(PostgreSQL / MySQL)
| 级别 | 脏读 | 不可重复读 | 幻读 | 写偏斜 |
|---|
| Read Uncommitted | ✓ | ✓ | ✓ | ✓ |
| Read Committed(默认) | ✗ | ✓ | ✓ | ✓ |
| Repeatable Read(PG/MySQL 默认变体) | ✗ | ✗ | ✗ | ✓ |
| Serializable | ✗ | ✗ | ✗ | ✗ |
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
建议:
- 默认 Read Committed 已能满足大多数业务
- 关键金融场景用 Serializable + 重试
- 慎用 Read Uncommitted(一般不需要)
事务边界设计
错:事务过大
@transaction.atomic
def create_order(user_id, items):
order = Order.objects.create(...)
OrderItem.objects.bulk_create(...)
payment_result = call_payment_gateway(order)
if payment_result.success:
order.status = 'paid'
order.save()
对:事务最小化 + Saga
@transaction.atomic
def create_order(user_id, items):
order = Order.objects.create(status='pending', ...)
OrderItem.objects.bulk_create(...)
return order
def process_payment(order_id):
order = Order.objects.get(id=order_id)
result = call_payment_gateway(order)
if result.success:
order.update(status='paid')
else:
order.update(status='failed')
compensate(order)
幂等性设计
Idempotency-Key 模式(写接口必备)
CREATE TABLE idempotency_keys (
key varchar(128) PRIMARY KEY,
request_hash varchar(64) NOT NULL,
response_body jsonb,
status varchar(32) NOT NULL,
created_at timestamptz DEFAULT now(),
expires_at timestamptz NOT NULL DEFAULT (now() + INTERVAL '24 hours')
);
CREATE INDEX idx_idempotency_expires ON idempotency_keys(expires_at);
业务唯一约束(最强幂等)
CREATE TABLE payments (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL,
amount_cents bigint NOT NULL,
status varchar(32) NOT NULL,
created_at timestamptz DEFAULT now(),
CONSTRAINT uq_order_payment UNIQUE (order_id, status)
DEFERRABLE INITIALLY DEFERRED
);
乐观锁(version 字段)
CREATE TABLE orders (
id bigserial PRIMARY KEY,
status varchar(32) NOT NULL,
version integer NOT NULL DEFAULT 1,
...
);
UPDATE orders
SET status = 'paid', version = version + 1
WHERE id = ? AND version = ?;
悲观锁(SELECT FOR UPDATE)
BEGIN;
SELECT * FROM accounts WHERE id = ? FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = ?;
COMMIT;
何时用:
乐观锁:冲突率低(< 5%),重试代价小
悲观锁:冲突率高,必须串行
多租户隔离三层防御
第一层:应用层
class TenantMiddleware:
def process_request(self, request):
request.tenant_id = get_tenant_from_token(request)
class TenantAwareManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(tenant_id=current_tenant_id())
第二层:数据库层(RLS / 视图)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
SET LOCAL app.current_tenant_id = '123';
SELECT * FROM orders;
第三层:测试层
def test_cross_tenant_isolation():
user_a = create_user(tenant=1)
user_b = create_user(tenant=2)
order_a = create_order(user=user_a)
response = client.get(f'/orders/{order_a.id}', as_user=user_b)
assert response.status_code == 404
多租户数据模型
Shared Schema(共享 Schema,最常用)
CREATE TABLE orders (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL REFERENCES tenants(id),
...
);
CREATE UNIQUE INDEX uq_orders_number
ON orders(tenant_id, order_number);
优势:
劣势:
- 单租户故障可能影响全部
- 大租户 vs 小租户性能干扰
- 跨租户查询风险
适合:SaaS 中小租户、共用功能
Schema Per Tenant(每租户独立 Schema)
CREATE SCHEMA tenant_acme;
CREATE TABLE tenant_acme.orders (...);
CREATE SCHEMA tenant_globex;
CREATE TABLE tenant_globex.orders (...);
优势:
劣势:
- 升级复杂(每个 schema 都要 migrate)
- 资源消耗大
- 应用层切换 schema 复杂
适合:百级租户、企业版
Database Per Tenant
每个租户独立数据库实例
优势:
- 完全隔离
- 性能 / 备份 / 安全独立
劣势:
- 资源消耗大
- 运维复杂
适合:大型企业客户、合规要求强(PII / 医疗)
唯一性约束设计
多租户场景
CREATE UNIQUE INDEX uq_users_email ON users(email);
CREATE UNIQUE INDEX uq_users_email_tenant
ON users(tenant_id, email);
软删除场景
CREATE UNIQUE INDEX uq_users_email
ON users(tenant_id, email);
CREATE UNIQUE INDEX uq_users_email_active
ON users(tenant_id, email)
WHERE deleted_at IS NULL;
软删除 vs 硬删除决策
软删除(deleted_at):
✅ 用户可恢复
✅ 关联数据完整
✅ 审计需要
❌ 唯一约束复杂
❌ 查询都要带 WHERE
❌ 数据膨胀
硬删除:
✅ 简单
✅ 性能好
❌ 不可恢复
❌ 关联数据连锁删除
归档(删除前移到归档表):
✅ 主表干净
✅ 历史数据可查
❌ 跨表查询复杂
推荐:
- 用户数据:软删除(GDPR 30 天后硬删)
- 业务数据:软删除 + 6 个月归档
- 临时数据(缓存 / 会话):硬删除
- 审计日志:永不删除
数据生命周期
CREATE TABLE orders (
id bigserial PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
archived_at timestamptz,
retention_until timestamptz,
created_by bigint REFERENCES users(id),
updated_by bigint REFERENCES users(id),
deleted_by bigint REFERENCES users(id),
version integer NOT NULL DEFAULT 1
);
标准流程
1. 识别业务不变量和并发写入场景
- 哪些状态不能同时存在
- 哪些操作不能重复执行
- 哪些字段必须唯一
↓
2. 判断强一致、最终一致或补偿机制
- 涉及钱 → 强一致
- 通知 / 报表 → 最终一致
- 跨服务 → Saga
↓
3. 设计事务边界、锁策略、唯一约束、版本字段
- 事务最小化
- 数据库约束兜底
- 乐观锁优先
↓
4. 设计租户字段、复合约束、默认过滤规则
- 三层防御
- 复合唯一含 tenant_id
- RLS 兜底
↓
5. 设计删除、归档、审计和恢复策略
- 软删 + 部分索引
- 归档表
- 审计日志
↓
6. 输出一致性和隔离检查清单
多租户检查表
| 项目 | 检查点 |
|---|
| 租户键 | 每张租户数据表是否有明确 tenant_id 字段 |
| 查询 | 是否默认带 tenant_id 过滤(中间件 / RLS) |
| 唯一性 | 唯一约束是否包含租户维度 |
| 后台操作 | 跨租户操作是否有审计和授权 |
| 测试 | 是否有跨租户越权回归用例 |
| 备份 | 单租户备份恢复是否可行 |
| 监控 | 单租户性能 / 错误是否可观测 |
| 删除 | 租户注销时数据如何处理(GDPR) |
配套模板
templates/consistency-checklist-template.md — 一致性 + 多租户 + 软删除 + 审计 + RLS 配置完整清单
质量自检
□ 列出业务不变量
□ 说明事务边界和失败补偿
□ 幂等键、唯一约束、版本字段明确
□ 多租户三层防御(应用 + 数据库 + 测试)
□ 唯一约束包含 tenant_id
□ 软删除有部分唯一索引
□ 删除、归档、审计、恢复策略明确
□ 事务不包含外部调用
□ 隔离级别选择有依据
□ 跨服务一致性方案(Saga / 事件)
□ RLS 配置(PG)兜底
□ 与安全工作流协作评审
常见坑
- 有 tenant_id 但唯一约束没带 tenant_id——同 email 跨租户冲突
- 后台管理接口绕过租户过滤——越权读取
- 长事务包外部调用——锁持有 30 秒,影响并发
- 幂等只靠前端防重复点击——后端裸奔
- 软删后唯一约束和恢复逻辑冲突——无法重新注册
- 乐观锁 version 不增——更新成功但无法检测并发
- 隔离级别用 Read Uncommitted——读到未提交数据
- 跨表强一致用分布式事务——2PC 性能差,应该 Saga
- 多租户共享表无 RLS 兜底——应用层一漏全漏
- 审计日志在主表——主表膨胀,审计被污染
- 删除级联到所有关联表——删一个用户影响 50 张表
- 保留期限不明——GDPR 违规
- 跨租户备份恢复无方案——单租户问题影响所有
- 测试不覆盖跨租户越权——上线后才发现
与其他 skill 的协作
上游:
schema-design → 提供表结构基础
api-designer → 提供权限边界
下游:
index-access-pattern → 设计租户维度索引
data-operations-safety → 评估跨租户操作和数据修复风险
security-engineer → 评审敏感数据和权限边界
backend-engineer → 实现租户中间件
qa-engineer → 跨租户越权回归用例
相关参考
references/consistency-multitenancy-guide.md — ACID 深度、Saga 模式、SaaS 多租户三种架构对比、RLS 实战、PostgreSQL 隔离级别真相、Slack / Notion 真实案例