这是 Beta 探索课程,内容结构、实验步骤和示例可能会继续调整。
批量操作
那天下午,我收到了一封加急邮件。
你好,我是某电商的技术负责人。我们马上要做双十一大促,有 5000 个产品链接需要缩短,用于短信营销。每条短信按字符计费,长链接太贵了。
能不能今天搞定?我们明天就要开始发短信了。
5000 个链接。我的 API 一次只能处理一个。
当前架构
生产版:可运营短链接平台
创建、跳转、统计、安全和管理链路各自独立,又围绕短码状态协同。
用户入口
短链域名
管理后台
开放 API
核心服务
创建服务
跳转服务
链接管理
数据能力
点击事件
实时统计
离线报表
治理能力
内容审核
风控拦截
申诉复核
存储
MySQL
Redis
对象/分析存储
最快的方案
客户催得紧,我决定先出一个最快的方案。
落地思路
- 接口接收一个链接数组,逐条创建短链接,每条都走一遍正常的创建流程:生成短码、写
urls、返回短码。- 短码要逐个分配,不能批量预先取号:批量取号会占着一段 ID 不释放,失败时那些 ID 就浪费了。
- 一批内某条失败不能中断整批,失败的条目要单独收集,最后一起返回给调用方。
- 兜底是整批加一个数量上限:一次最多接受多少条,超出的直接拒绝,避免一个请求把连接池和事务日志撑爆。
问题很快暴露
客户分 5 批提交了 5000 个链接。每批 1000 个,平均耗时 15 秒。
“15 秒?太慢了!我们点提交后页面一直在转圈,还以为挂了。”
我查了一下瓶颈:1000 次数据库 INSERT,每次约 15ms,总计 15 秒。
把 1000 次提交合并为 1 次
“原来每次 INSERT 都是一次磁盘写入。如果把 1000 条打包成一次事务呢?”
落地思路
- 把一批链接塞进同一个事务:先全部走一遍校验(URL 合法、短码不重复),再统一 INSERT,最后一起提交。
- 校验和写入必须都在事务里完成,否则中途失败会留下「校验过但没写进去」和「写了一半」的中间状态。
- INSERT 用多值批量语法一次提交,1000 条只产生一次磁盘写入——这就是 15 秒降到 1.5 秒的原因。
- 兜底是事务回滚:任何一条不合法就整体回滚并告诉调用方是哪一条,不留半截数据。
效果
| 方案 | 1000 条耗时 | 原因 |
|---|---|---|
| 逐条 INSERT | 15 秒 | 1000 次磁盘写入 |
| 事务批量 | 1.5 秒 | 1 次磁盘写入 |
“10 倍提升,客户满意了。”
客户的第二个需求
“能不能支持 CSV 文件导入?我们运营团队只会用 Excel。”
落地思路
- 上传的是 CSV 文件,先整文件读进来解析,按行取出必需列
url和可选列title、tag。- 表头要校验:必需列
url缺失就直接报错,并把期望的列名列出来,不要逐行报错让用户自己猜。- 解析出来的每一行都要过 URL 校验,非法行不中断整批,记下行号和原因放进失败清单。
- 兜底是文件大小和行数上限:超限直接拒绝并提示改用异步接口,别让一次上传把服务压垮。
CSV 格式说明
我写了个简单的文档给客户:
# CSV 文件格式
## 必需列
- url(或 URL、链接)
## 可选列
- title(链接标题)
- tag(标签,用于分组管理)
## 示例
url,title,tag
https://example.com/product/1,iPhone 15,手机
https://example.com/product/2,MacBook Pro,电脑
https://example.com/product/3,AirPods,耳机
新问题:10 万条链接
一个月后,一个更大的客户找上门。
“我们有 10 万条 链接需要缩短,用于全渠道营销推广。”
10 万条。即使事务优化后,也需要 150 秒。HTTP 请求早就超时了。
“我意识到,同步方案的天花板到了。”
架构改造
“我花了一个周末引入了 Celery 异步任务队列。”
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 客户端 │────▶│ API 服务 │────▶│ Redis │ ← 任务队列
└──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ Worker │ ← 后台处理
└──────────┘
│
▼
┌──────────┐
│ MySQL │
└──────────┘
提交任务
落地思路
- 接口不直接处理数据:把链接清单写进任务记录,生成一个任务 ID 立刻返回,客户端拿这个 ID 去查进度。
- 任务记录里要存下原始清单和创建者,异步 Worker 后面只靠任务 ID 就能把活干完,不用再回调接口。
- 提交成功不等于创建成功,最终结果要通过进度查询接口拿,这两件事必须分清楚。
- 兜底是任务本身也要有状态:待处理 / 处理中 / 已完成 / 部分失败,提交完成后状态就是待处理。
Worker 处理
落地思路
- Worker 从 Celery 队列里取出任务,按批次读出链接清单,逐条走创建流程写进
urls。- 每处理一条就更新一次进度(已处理数 / 总数),进度是 Worker 写的,不是查询接口现算的。
- 失败的条目不能中断整个任务:收集失败原因继续下一条,任务最终状态是「部分失败」并带上失败清单。
- 兜底是任务重试:Worker 中途挂掉时任务重新入队,已处理的部分靠去重跳过,不会重复创建。
效果
| 方案 | 10 万条耗时 | 客户体验 |
|---|---|---|
| 同步接口 | 150 秒(超时) | ❌ 页面卡死 |
| 异步任务 | ~30 秒(后台) | ✅ 秒级响应,后台处理 |
查询接口
“客户提交后总想知道进度。我加了一个查询接口。”
落地思路
- 接口按任务 ID 返回状态、已处理数、总数、成功数、失败数和最近一条错误原因。
- 进度要从任务记录里直接读出来,不要每次查询都去数一遍数据库,那是把查询变成全量扫描。
- 状态是终态(已完成 / 部分失败 / 失败)时直接返回最终结果,并附上结果文件的下载入口。
- 兜底是任务记录本身要能查到:任务 ID 丢了也要能按创建者和时间范围找回任务。
客户端轮询
交互要点
- 前端逻辑重点是展示任务状态、错误原因和下一步操作。
- 轮询或进度刷新要有节制,避免给后端制造新的压力。
批量删除
“后来客户又提出:大促结束了,那些活动链接能不能批量删除?”
落地思路
- 删除走软删除:把这一批短链接的状态改成已删除,而不是物理 DELETE,这样误删还能恢复。
- 批量操作要放在一个事务里:要么这批全部标记成功,要么全部回滚,不能删一半。
- 涉及大量记录时按 ID 范围分段提交,避免一条 UPDATE 长时间锁住太多行。
- 兜底是删除要留痕:谁在什么时候删的哪一批、删的是哪些短码,都要有记录。
批量更新
落地思路
- 批量更新支持改标题、标签、目标 URL 这几类字段,先校验每条的更新内容是否合法,再统一落库。
- 改目标 URL 的条目要额外失效缓存:数据库改了但 Redis 里还是旧值,用户会继续跳到旧地址。
- 部分失败要按条记录:哪些成功了、哪些失败了、失败原因是什么,最后一起返回给调用方。
- 兜底是更新和删除一样走软标记,真正的物理清理交给定时任务,不在用户请求里做。
“引入 Celery 后,我算了一笔账。”
| 组件 | 月成本 | 说明 |
|---|---|---|
| 额外 Redis 实例 | ¥40/月 | Celery broker |
| Worker 服务器 | ¥60/月 | 2 核 4G |
| 合计 | ¥100/月 | — |
“多花了 ¥100/月,但换来了企业客户的满意度。这个投入值不值?看收入就知道了——批量功能上线后,企业客户付费转化提升了 30%。”
发现内容有问题?
告诉我们哪里需要改进。
