← 返回 用户与业务 ID 设计

数据库分片就绪度审计

v1.0 · 2026-07-18 · 全仓库 68 张表逐一核查,回答"未来用户量暴涨,现在的表结构是否已经准备好"
这不是"要不要分片"的讨论,现在完全不需要动手分片。这是把"以后真要分片时,哪些表已经没问题、哪些表需要先动一刀"提前列清楚,越早知道,越早能在日常开发里顺手改掉,不用等到真被数据量逼到墙角才手忙脚乱。

1. 结论

主键设计

68 张表全部用 Ids.newId() 应用层生成,零 AUTO_INCREMENT

已就绪
缺分片键的表

约 6 张表(webhook/短信/WA消息等)只有手机号或第三方 ID,没有 user_id

需要补列
分片不安全的唯一约束

约 10 处全局唯一索引(邀请码、登录码、支付回调 ID 等)跨用户生效,分片后需要独立解析层。

需要设计
无归档策略的表

约 11 张日志/流水表只增不减,没有分区或清理策略,这个风险比分片更早会出现。

优先级最高

2. 主键设计:已经就绪

全仓库核查(2026-07-18):45+ 张表定义(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_eventsprovider_event_id(需 join payment_orders 才能拿到 user_id)
provider_sms_messagesphone_number
wa_inbound_messages / wa_outbound_messagesphone_number
claim_attachmentsclaim_id(需 join cashback_claims)
payout_transactionswithdraw_id(需 join withdraw_requests)

另有约 20 张纯国家级配置/目录表(rule_config_versionscampaigncoupon_templatepopular_brand_configs 等)本来就没有用户维度,这是正常的,分片时应该明确排除在"按用户分片"范围外,留在共享的控制面 schema 里,不需要改。

4. 分片不安全的唯一约束

这些唯一索引的查询场景是"还不知道是哪个用户之前,先按一个全局值查出用户是谁"(邀请码兑换、登录码确认、支付网关回调去重),分片之后这类查询没法只查一个分片就找到目标行,需要一个不分片的全局解析层:

约束典型场景
usersuk_referral_code邀请码兑换,兑换时还不知道邀请人在哪个分片
user_identitiesuk_provider_user第三方登录回调,按 provider_user_id 查
whatsapp_login_intentuk_country_login_codeWA 登录码确认
whatsapp_usersuk_country_phone手机号身份查找
affiliate_ordersuk_platform_order平台订单 Webhook,按平台订单号去重
payment_ordersuk_external_id / uk_provider_payment支付网关回调,按网关流水号去重
wallet_ledgeruk_ledger_refref_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 检查更新
这个比分片更紧迫:这些表(尤其是消息类和点击类)大概率会比用户表本身早得多地涨到"大表"量级,拖慢查询、占满存储——不用等到"要不要分片"这个问题出现,现在就该给这几张表定个保留期(比如只留最近 N 个月,或者按月分区),不是分片专属的问题。
2026-07-19 已实现并部署 id-test:DataRetentionCleanupService(seahub-core/.../retention/)按固定的表白名单(表名/时间列硬编码在 Java 常量里,不接受外部输入拼接)分批 DELETE ... LIMIT 清理过期行,配合 DistributedTaskLockExecutor 防止多实例重复跑。默认清理 share_clicksaffiliate_clickswa_inbound_messageswa_outbound_messagesprovider_sms_messagespush_messagescampaign_action_recordh5_package_update_logs 这 8 张非资金表(保留 90-365 天,按表而定);wallet_ledgercoupon_ledgerpayment_webhook_eventsrisk_events 这 4 张涉及资金/风控合规的表默认不清理,等业务方确认保留期后再通过 seahub.retention.overrides.<key>.enabled 单独打开。总开关 seahub.retention.enabled 生产默认关闭,id-test 已打开验证。

7. 热点行风险

好消息:仓库里没有找到真正的"全局计数器/全局序列"这种最糟糕的热点行模式。唯一值得关注的是:

  • user_wallets:每次钱包加减都对这一行做 for update 行锁。对普通用户没问题(每个用户自己的钱包行,天然分散),但对一个下线规模很大的顶部推广者,他自己的钱包行会被大量并发的佣金入账请求同时抢锁——这是"个别热点用户"级别的风险,不是全局问题,值得对头部用户单独观察。

8. 优先级建议

  1. 最优先,且现在就该做:给 §6 里那些日志/流水表定归档或分区策略——这个不管以后分不分片都需要,而且会比"要不要分片"这个问题更早变得紧急。(已完成,见 §6 绿色标注)
  2. 顺手做:新表设计时(比如 outbox_event)按 §3/§4 的模式提前加上 user_id 冗余列,不要等到真分片那天再回头改存量表结构。
  3. 需要时再做:§4 的全局唯一约束解析表(参考 platform_order_index 样板)、§5 的关系树分片策略——这两项工作量较大,等真的启动分片评估时再集中做,不用现在就动手。