CRUD 是后端的入口,但真正决定一个系统能不能扛住的,是它周围那一圈机制。下面用"注册"这个最普通不过的接口当线索,把周边的问题和对应的工程方案过一遍——有些是行业标配,有些不常提、但遇到复杂场景时很顶用。这里不打算写成操作手册,只是把真实系统里常见的选择摆出来。


一、一个"注册"会牵出哪些问题

最朴素的注册只有三行:

1
2
3
public void register(String phone, String password) {
userDao.insert(new User(phone, encrypt(password)));
}

上线之后,真实环境会一个接一个地抛出问题:

  • 重复提交:用户连点两下,或脚本一秒刷一千次,数据库扛得住吗?
  • 慢依赖:注册完要发短信,短信接口两三秒才回,注册流程要干等吗?
  • 跨服务:邀请注册送积分,积分服务是别的团队的,他们宕机你的注册也要挂吗?
  • 数据量:单表涨到一千万行,注册都变慢了怎么办?
  • 安全:密码要加密、密钥要轮换,加密服务自己挂了用户登不了录怎么办?
  • 多机房:国内海外两个机房,数据双向同步有延迟,用户刚注册完切到海外登录查不到怎么办?

这些问题不是故意刁难,而是每个有一定规模的系统都会撞上的。下面按主题看解法,每个主题我都分两档:常用做法不常用但有效


二、限流:挡住重复和刷接口

常用做法

在业务逻辑最前面加一道闸门,同一 IP(或同一用户)对注册接口在窗口内只允许 N 次。工程上一般用令牌桶 / 漏桶 / 滑动窗口,状态放 Redis,并用 Lua 脚本保证"判断+扣减"原子,否则并发下限流会失效。优先级上,限流检查必须排在业务校验之前——命中就直接返回"请求频繁",根本不进后面逻辑。

请求处理顺序:限流闸门优先级最高 请求进来 ①限流闸门(优先) 命中→频繁提示 未命中→业务逻辑
1
2
3
4
5
6
7
8
9
10
11
-- 滑动窗口限流(Redis + Lua,原子执行)
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2]) -- 窗口大小,如 5000ms
local limit = tonumber(ARGV[3]) -- 窗口内最大次数,如 1
redis.call('ZREMRANGEBYSCORE', key, 0, now - window) -- 清掉窗口外的
local count = redis.call('ZCARD', key)
if count >= limit then return 0 end
redis.call('ZADD', key, now, now)
redis.call('PEXPIRE', key, window)
return 1

不常用但有效

  • 自适应限流:固定阈值("5 秒 1 次")是拍脑袋定的。Sentinel 的系统保护、Netflix 的 Concurrency Limits 会根据系统实时负载(CPU、线程、延迟)动态调阈值,比写死更稳。
  • 按用户/设备配额而非仅按 IP:NAT、移动网络下大量用户共用一个出口 IP,纯 IP 限流会误伤;结合设备指纹、登录态做配额更准。
  • 行为指纹识别脚本:仅靠限流挡不住低速率的恶意流量。用设备 ID、UA、操作轨迹做指纹,比 IP 维度识别机器人更靠谱。
  • PoW 挑战:对可疑流量发一个轻量工作量证明(类似 Cloudflare 的 challenge),机器人要付出算力成本,真人无感——成本极低地挡掉批量刷接口。

取舍:中小团队用 Redis + 固定窗口足够;自适应限流和 PoW 是"被刷得痛过"之后才值得上的重武器。


三、异步通知:别让慢调用堵住主流程

常用做法

短信这种慢接口,决不能同步等。注册时落一条短信记录、状态标"发送中",真正发送交给 MQ 异步处理,拿到结果再回写状态。注册接口本身立刻返回。

1
2
3
4
5
6
7
8
9
enum SmsStatus { SENDING, SUCCESS, FAILED }
userDao.insert(user);
smsDao.insert(new SmsRecord(phone, SmsStatus.SENDING));
mq.publish(new SendSmsEvent(phone, "注册成功"));
// 消费者回写状态
void onSendSms(SendSmsEvent e) {
boolean ok = smsClient.send(e.phone, e.content);
smsDao.updateStatus(e.phone, ok ? SmsStatus.SUCCESS : SmsStatus.FAILED);
}

不常用但有效

  • Transactional Outbox(事务性发件箱):比"本地消息表 + 定时任务"更标准。把"业务写入"和"消息"放在同一个本地事务写进 outbox 表,再由一个 relay 捞出来发 MQ。好处是不依赖定时任务轮询,也不丢消息——本地事务成功就保证消息最终一定发出去。
Transactional Outbox:同事务写业务+消息,relay 捞出发 MQ 本地事务 写 用户表 写 outbox 表 relay 轮询 MQ
- **幂等消费**:用 message id 或业务去重键,防止同一条短信因重试被发两遍。 - **供应商 webhook 回调**:与其同步等短信网关返回,不如发完就返回,由运营商异步回调结果——把"等待"从你的关键路径上彻底拿掉。 - **独立通知服务收口**:注册服务只发一个领域事件("用户已注册"),所有渠道(短信/邮件/推送)由专门的 Notification Service 统一处理,注册服务不再关心"怎么发"。

取舍:MQ + 状态机是标配;Outbox 是当你被"消息丢了但库写了"这种坑过之后才体会到香的方案。


四、跨服务调用与补偿:加积分失败怎么办

常用做法

跨服务调用失败是常态。把失败的调用先记下来,定时任务周期性重试,成功就删、失败等下一轮;配合指数退避 + 死信队列(DLQ),避免无限重试拖垮下游。

不常用但有效

  • Saga 模式:把"注册送积分"这种跨服务流程拆成一串本地事务,每步配一个补偿操作(正向创建、逆向撤销)。某步失败就按相反顺序回滚,比"要么全成要么全废"的两阶段提交更适合长流程。
  • TCC(Try-Confirm-Cancel):比 Saga 更强一致,但侵入业务(要写 try/confirm/cancel 三套),一般只在金融级场景用。
  • 幂等键(Idempotency-Key):客户端或网关生成,保证任何重试都不会重复执行——这是比"定时重试"更根本的防重复手段。
  • 防羊毛:别只靠"唯一手机号":一个号绑一个手机号只是最弱一环。真实风控是设备指纹 + 图关联(同设备多账号)+ 风险评分模型 + 规则引擎(同 IP 段、同设备聚集)。手机号绑定拦得住小白,拦不住有接码平台的职业羊毛党。
  • T+1 对账:两个系统跑批核对,差异自动修复,是最终一致性的最后一道保险。

五、数据规模:单表一千万之后

常用做法

顺序一般是:先加索引(多数慢查询是缺索引或索引失效),再读写分离(读走从库卸压力),真到极限才分库分表(按 user_id / 手机号 hash)。

不常用但有效

  • CQRS:把写模型和读模型分开,读用专门优化的存储(物化视图、ES),写仍走主库。列表页、统计页不再和写入抢同一张表。
  • 冷热分层:老数据归档到廉价存储(对象存储 / 列存),热数据留主库。很多"大表慢"其实只是冷数据占着地方。
  • 专用存储替代 MySQL:全文检索走 ES,分析走 ClickHouse,时序走 TSDB——别什么都往关系库塞。
  • 单表设计(如 DynamoDB):反范式、把关联拍平,靠 GSI 查询,彻底避免 join 和分片痛点,适合特定高并发场景。
  • 分片键选错比不分更糟:按注册时间分片会造成"最新分片被狂写"的热点;按手机号 hash 才能打散。分片键是分库分表里最该想清楚的一件事。

六、密钥与加密:轮换、降级与格式

常用做法

字段加密 + 密钥版本号拼密文v1:<密文>),解密时按前缀取对应版本密钥;密钥托管给 KMS,主密钥不出 KMS(信封加密:KEK 加密 DEK,DEK 加密数据)。

不常用但有效

  • 应用层信封加密 + 本地 KEK 缓存:加密服务挂了怎么办?常用说法是"只能靠运维"。更具体的兜底是把 KEK 在应用本地做短缓存 + 熔断器,服务抖动时本地仍能解密,撑过那几秒——这不是认输,是明确的降级路径。
  • HSM(硬件安全模块):密钥永不离开硬件,合规要求高的金融场景标配。
  • 格式保留加密(FPE):加密后保持原格式(手机号还是 11 位),现有校验、模糊查询不用改就能跑,又不泄露明文。
  • 字段级代理加密:在 DB 代理层(如 CipherTrust、ProxySQL 插件)透明加解密,业务代码零侵入——比"每个字段手搓加解密"干净得多。
  • Shamir 秘密分享:主密钥分片给多人/多机,重建需达到阈值,避免单点泄露或被单人挟持。

取舍:KMS + 版本号是及格线;FPE 和代理加密在你"既要加密又要能按原字段查"时才值得上。


七、多机房:同步、冲突与单元化

常用做法

主从复制 + 最终一致,登录态和用户主数据交给**统一身份认证(SSO / IdP)**托管,各机房向它要,而不是机房之间互相迁就对账。

不常用但有效

  • CDC(Change Data Capture,如 Debezium):捕获数据库 binlog 跨机房同步,比应用层"双写"可靠得多——双写任一步失败就会不一致,CDC 只认 binlog 这一个事实源。
  • Active-Active + CRDT / 冲突解决:两地同时可写,用无冲突复制数据类型(CRDT)或 LWW + 向量时钟解决冲突,而不是"主写从读"。代价是高复杂度和可能的冲突窗口。
  • 单元化(Unit Architecture):按用户维度切"单元",每个单元自包含(自己的 DB、缓存、服务),故障爆炸半径被限制在一个单元内。蚂蚁、字节的大规模系统大多走这条路。
  • 用户路由 + 数据局部性:用户在哪个单元注册,后续请求都路由到那,天然避免跨单元读写——很多"跨机房同步难"的问题,从架构上就绕开了。

八、一张表:常用 vs 不常用但有效

主题 常用做法 不常用但有效
限流 令牌桶/滑动窗口 + Redis + Lua,IP 维度 自适应限流、设备指纹配额、PoW 挑战
异步通知 MQ + 状态机 + 重试 Transactional Outbox、幂等消费、webhook 回调、独立通知服务
跨服务 失败记录 + 定时重试 + DLQ Saga 补偿、TCC、幂等键、风控图谱、T+1 对账
数据规模 索引 → 读写分离 → 分库分表 CQRS、冷热分层、专用存储、单表设计、分片键优化
密钥加密 KMS + 信封加密 + 版本号 本地 KEK 缓存降级、HSM、FPE、代理加密、Shamir 分片
多机房 主从 + 最终一致 + SSO CDC、Active-Active+CRDT、单元化、数据局部性路由

九、收尾

CRUD 不是后端的全部,它只是入口。真正拉开系统之间差距的,是上面这些围绕 CRUD 的机制——限流、异步、补偿、分片、加密、多活。一个主题里,"常用做法"让你及格,"不常用但有效"的那一档,往往是系统撞到真实规模或真实攻击之后,才会被逼出来、也才真正显出价值的东西。


参考资料