数据库分片就绪度审计
1. 结论
68 张表全部用 Ids.newId() 应用层生成,零 AUTO_INCREMENT。
约 6 张表(webhook/短信/WA消息等)只有手机号或第三方 ID,没有 user_id。
约 10 处全局唯一索引(邀请码、登录码、支付回调 ID 等)跨用户生效,分片后需要独立解析层。
需要设计约 11 张日志/流水表只增不减,没有分区或清理策略,这个风险比分片更早会出现。
优先级最高2. 主键设计:已经就绪
schema-id-test.sql)+ 18 个 *SchemaInitializer.java,零 AUTO_INCREMENT 命中。所有表主键都是 id varchar(64) primary key,由 Ids.newId(prefix) 生成(时间戳 base36 + 12 位随机字符),任何分片都能独立生成、无需协调。这一层不需要额外工作。顺带修正了 id-design.html §三 里一处过时描述——那份文档原来说"内部主键可以是 BIGINT AUTO_INCREMENT 或内部 UUID",但实际代码里从来没有另开一层独立的自增主键,业务对象 ID 本身就是数据库主键,已同步更新那份文档。
3. 缺少分片键的表
这些表的自然查询键是手机号/第三方 ID/父表外键,没有自己的 user_id 列,没法直接按"用户 ID 哈希"分片,写入前还得先反查一次才知道归哪个分片:
| 表 | 现有可用关联字段 |
|---|---|
payment_webhook_events | provider_event_id(需 join payment_orders 才能拿到 user_id) |
provider_sms_messages | 仅 phone_number |
wa_inbound_messages / wa_outbound_messages | 仅 phone_number |
claim_attachments | claim_id(需 join cashback_claims) |
payout_transactions | withdraw_id(需 join withdraw_requests) |
另有约 20 张纯国家级配置/目录表(rule_config_versions、campaign、coupon_template、popular_brand_configs 等)本来就没有用户维度,这是正常的,分片时应该明确排除在"按用户分片"范围外,留在共享的控制面 schema 里,不需要改。
4. 分片不安全的唯一约束
这些唯一索引的查询场景是"还不知道是哪个用户之前,先按一个全局值查出用户是谁"(邀请码兑换、登录码确认、支付网关回调去重),分片之后这类查询没法只查一个分片就找到目标行,需要一个不分片的全局解析层:
| 表 | 约束 | 典型场景 |
|---|---|---|
users | uk_referral_code | 邀请码兑换,兑换时还不知道邀请人在哪个分片 |
user_identities | uk_provider_user | 第三方登录回调,按 provider_user_id 查 |
whatsapp_login_intent | uk_country_login_code | WA 登录码确认 |
whatsapp_users | uk_country_phone | 手机号身份查找 |
affiliate_orders | uk_platform_order | 平台订单 Webhook,按平台订单号去重 |
payment_orders | uk_external_id / uk_provider_payment | 支付网关回调,按网关流水号去重 |
wallet_ledger | uk_ledger_ref | ref_id 可能是别的用户的佣金记录(上级分成),归属用户可能跟这条流水本身不在同一分片 |
platform_order_index 表已经带了 shard_key 列,专门做"平台订单号 → 归属用户/分片"的解析,不跟着业务表一起分片。真要分片时,把上面这几张表的唯一约束按同样思路各自建一张解析索引表即可,不是要发明新方案,是把这个模式复制几遍。5. 关系/汇总类表的结构性问题
这几张表按"用户 ID 哈希"简单分片会直接查不动,因为一条记录往往涉及两个不同的用户,分完片可能落在不同分片上:
referral_relation(parent_user_id / root_user_id)、referral_closure(ancestor / descendant)——查"我的整条下线"就是跨分片查询。team_daily_stats(leader_user_id 汇总下属业绩)——汇总的数据来自下属,下属可能分布在不同分片。
这类表分片前需要专门设计(比如按 root_user_id 分片、保证同一条邀请链路整体落在同一分片,而不是每一行各自按自己的 user_id 哈希),id-design.html 现在还没有覆盖到这一层,真要认真规划分片时需要单独讨论。
6. 无限增长、没有归档策略的表(比分片更早会成为问题)
下面这些表全部是只 INSERT 不清理,增长速度比用户数快得多,仓库里搜不到任何 PARTITION BY、定时清理任务:
| 表 | 为什么长得快 |
|---|---|
wallet_ledger | 每一笔钱包变动(返现/佣金/提现)一条,一个订单可能对应好几条 |
coupon_ledger | 每一次发券/用券/过期一条 |
payment_webhook_events | 每次网关回调(含重试) |
share_clicks / affiliate_clicks | 每一次点击,点击量通常是订单量的 10-100 倍 |
wa_inbound_messages / wa_outbound_messages / provider_sms_messages | 每条 WhatsApp/短信,含 OTP 重试和营销消息,增速可能远超用户表本身 |
push_messages | 每条通知 × 每个接收设备 |
risk_events | 每一次风控评估 |
campaign_action_record | 每个用户每次触发活动规则 |
h5_package_update_logs | 每次 App 检查更新 |
DataRetentionCleanupService(seahub-core/.../retention/)按固定的表白名单(表名/时间列硬编码在 Java 常量里,不接受外部输入拼接)分批 DELETE ... LIMIT 清理过期行,配合 DistributedTaskLockExecutor 防止多实例重复跑。默认清理 share_clicks、affiliate_clicks、wa_inbound_messages、wa_outbound_messages、provider_sms_messages、push_messages、campaign_action_record、h5_package_update_logs 这 8 张非资金表(保留 90-365 天,按表而定);wallet_ledger、coupon_ledger、payment_webhook_events、risk_events 这 4 张涉及资金/风控合规的表默认不清理,等业务方确认保留期后再通过 seahub.retention.overrides.<key>.enabled 单独打开。总开关 seahub.retention.enabled 生产默认关闭,id-test 已打开验证。7. 热点行风险
好消息:仓库里没有找到真正的"全局计数器/全局序列"这种最糟糕的热点行模式。唯一值得关注的是:
user_wallets:每次钱包加减都对这一行做for update行锁。对普通用户没问题(每个用户自己的钱包行,天然分散),但对一个下线规模很大的顶部推广者,他自己的钱包行会被大量并发的佣金入账请求同时抢锁——这是"个别热点用户"级别的风险,不是全局问题,值得对头部用户单独观察。
8. 优先级建议
- 最优先,且现在就该做:给 §6 里那些日志/流水表定归档或分区策略——这个不管以后分不分片都需要,而且会比"要不要分片"这个问题更早变得紧急。(已完成,见 §6 绿色标注)
- 顺手做:新表设计时(比如
outbox_event)按 §3/§4 的模式提前加上user_id冗余列,不要等到真分片那天再回头改存量表结构。 - 需要时再做:§4 的全局唯一约束解析表(参考
platform_order_index样板)、§5 的关系树分片策略——这两项工作量较大,等真的启动分片评估时再集中做,不用现在就动手。