KernelRanch
返回文章列表

项目:用 Workers 与 KV 做一个免费短链服务

2026-11-10 #Cloudflare

把长链接变成 https://你的域名/853ybv 这种六位码。整套东西是一个 Worker 加一个 KV 命名空间,没有数据库、没有服务器、不用备案(节点在海外)。做完你会清楚三件事:KV 的读写延迟在真实网络里是什么量级、短码怎么生成才不容易撞、以及为什么这个服务不该用 KV 做点击统计。

前置条件只有一个能跑 wrangler deploy 的环境。注册与登录见 47-cf-free-tier-overview.md,部署流程见 53-first-free-site-with-workers.md。KV 本身的读写语义在 59-workers-kv-tutorial.md 里讲得更细,这篇只讲怎么把它拼成一个能用的东西。

1. 数据模型

一张表就够:

keyvalue说明
link:853ybv{"url":"https://...","createdAt":"2026-11-10T..."}一个短码一条记录
rl:<ip>:<path>"7"限流计数,见 78-rate-limiting.md

短码固定六位,字母表去掉容易看错的 0、1、l、o、i,剩 31 个字符,空间是 31^6 ≈ 8.87 亿。按每天新增一千条算,跑二十年占用不到万分之一,随机生成足够用,不需要自增 ID。

2. 建 KV 命名空间

npx wrangler kv namespace create cf97-links

真实输出:

🌀 Creating namespace with title "cf97-links"
✨ Success!
[[kv_namespaces]]
binding = "cf97_links"
id = "c9f251cb8ffb4b6ea87ba0ddd6172f66"

把这段配置抄进 wrangler.toml,binding 名改成大写(LINKS),代码里用 env.LINKS 访问。命名空间 ID 是账号级的,换项目也能复用,但建议一个项目一个,删的时候不会连带删掉别人的数据。

3. 代码

src/index.js 分三个分支:表单页、生成接口、跳转。

const ALPHABET = "23456789abcdefghjkmnpqrstuvwxyz";

function newCode(len = 6) {
  const bytes = new Uint8Array(len);
  crypto.getRandomValues(bytes);
  let out = "";
  for (let i = 0; i < len; i++) out += ALPHABET[bytes[i] % ALPHABET.length];
  return out;
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname.slice(1);

    if (request.method === "POST" && url.pathname === "/api/shorten") {
      const { url: target } = await request.json();
      const parsed = new URL(target);
      if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
        return Response.json({ error: "http/https only" }, { status: 400 });
      }
      const code = newCode(6);
      await env.LINKS.put(`link:${code}`, JSON.stringify({
        url: parsed.toString(),
        createdAt: new Date().toISOString()
      }));
      return Response.json({ code, short: `${url.origin}/${code}` }, { status: 201 });
    }

    if (request.method === "GET" && /^[0-9a-z]{6}$/.test(path)) {
      const raw = await env.LINKS.get(`link:${path}`);
      if (!raw) return new Response("Not Found", { status: 404 });
      const { url: target } = JSON.parse(raw);
      return Response.redirect(target, 302);
    }

    return new Response("Not Found", { status: 404 });
  }
};

两个细节值得说。crypto.getRandomValues 是 Workers 运行时自带的,别用 Math.random,后者在同一个 isolate 里可预测。Response.redirect 默认给 302,想固定跳转就用 301,但 301 会被浏览器长期缓存,改目标后用户可能一年都跳不回来。

4. 部署

npx wrangler deploy

真实输出(2026-11-10,wrangler 4.146.0):

Total Upload: 3.46 KiB / gzip: 1.59 KiB
Worker Startup Time: 1 ms
env.LINKS (c9f251cb8ffb4b6ea87ba0ddd6172f66)  KV Namespace
Uploaded cf97-url-shortener (2.58 sec)
Deployed cf97-url-shortener triggers (1.08 sec)
  https://cf97-url-shortener.hyysn.workers.dev
Current Version ID: 2a3a7084-2ced-4fba-b076-8afe070f08c6

5. 实测

请求状态耗时内容
POST /api/shorten(首次)2012366 ms{"code":"853ybv","short":".../853ybv"}
GET /853ybv302996 mslocation: https://developers.cloudflare.com/workers/
GET /853ybv(连续 5 次)3022035 / 1060 / 1659 / 1583 / 1002 ms同上
GET /zzzzzz404—Not Found
POST 传 ftp://400—{"error":"http/https only"}
GET /200—1250 字节 HTML,cf-ray: ...-HKG

延迟数字要打折看:这是从本机用 PowerShell 发起的,每次请求都重新建 TLS 连接,还包含进程启动开销。真实用户点短链时连接是复用的,边缘执行只占其中很小一段。但这组数字说明了一个事实:一次跳转包含一次 KV 读,端到端在秒级而不是毫秒级,别拿它做需要低延迟的场景。

6. 三个会踩的坑

短码碰撞。随机生成不保证唯一,put 会直接覆盖已有记录且不报错。轻度使用可以不管;要稳妥就在写之前 get 一次,撞了就重生成,两次 get 的成本远低于一次数据事故。

KV 最终一致性。写入后立刻在同机房读能读到,其他机房最长可能延迟 60 秒才可见。用户刚生成完短链马上点开,大概率命中同一机房,感知不到;但把短链发到群里,外地用户秒开可能看到 404。要立刻全局可见,只能用 Durable Objects。

不要用 KV 计数。免费额度是每天 1,000 次写入(不同 key),同一 key 每秒只能写一次。给每条短链加个点击计数,等于每次跳转都写一次,一天一千次点击就把额度打满,而且热点短链会直接触发每秒一次的写入限制。跳转路径只读不写,是 KV 最适合的形态。

7. 换成自己的域名

workers.dev 子域是共享的,地址长、不好记,也不适合印在名片上。换自己的域名要两步:域名接入并处于 Active 状态(见 49-add-domain-to-cloudflare.md),然后在 wrangler.toml 里声明路由。

routes = [
  { pattern = "s.example.com", custom_domain = true }
]

custom_domain = true 会自动创建 DNS 记录并签发边缘证书,不需要手工加 CNAME。绑完之后原来的 workers.dev 地址仍然能用,两个入口并存;只想留自定义域就把默认路由关掉。域名解析通常一两分钟内生效,证书签发可能多等几分钟,这段时间访问会看到 522 或证书告警,属于正常过程。

短链服务对域名有个额外要求:域名要短。s.example.com 比 example.com 长三个字符,一万条短链就多传 30 KB。预算允许时用二级域甚至更短的主域做短链专用域名。

8. 常见问题

能不能自定义短码?可以,POST 时多传一个 code 字段,先 get 确认不存在再 put。代价是要防枚举,六位随机码无所谓,自定义码就得配合限流。

短链过期怎么处理?put 时加 { expirationTtl: 86400 },最短 60 秒。过期后 get 返回 null,自动走 404 分支。注意 KV 的过期是惰性的,删除可能延后,但读不到就够用了。

访问量大了会不会挂?跳转本身很轻,真正的上限是 KV 每天 10 万次读和 Workers 每天 10 万次请求,两个额度会同时消耗。超了返回 Error 1027,处理方式见 102-cost-control-quotas.md。

小结

这个项目的全部资产是 80 行代码、一个 KV 命名空间、一条部署命令。上传包 3.46 KiB,冷启动 1 ms,部署到全球生效 1 秒多。它足够支撑一个个人或小团队的短链需求,前提是接受 KV 的写入额度与最终一致性。

真正决定它能用多久的不是代码,是额度与写入模式。跳转走只读,把统计放到别处(比如 Workers Analytics Engine 或日志),这个服务就能一直免费跑下去。下一步可以看 98-project-image-host-r2.md,同样的思路换个存储介质。

数据来源与检索时间

检索时间 2026-11-10。部署与访问数据为本机真实执行。

来源URL对应数据
Workers KV 文档https://developers.cloudflare.com/kv/put / get / expirationTtl 语义、最终一致性最长 60 秒
KV 限额https://developers.cloudflare.com/kv/platform/limits/100,000 读/天、1,000 写/天、同 key 1 次/秒、key 上限 512 字节
Workers 限额https://developers.cloudflare.com/workers/platform/limits/100,000 请求/天、Error 1027

核验说明

  • 真实执行:创建 KV 命名空间 cf97-links(id c9f251cb...),部署 Worker cf97-url-shortener,跑通生成、跳转、404、400 四个分支,状态码与耗时均为实测。
  • 未做实机验证:跨机房一致性延迟(需要多地探针)、短码碰撞概率(样本量不够)、过期删除的实际延迟。
  • 验证完成后已 wrangler delete cf97-url-shortener,并删除了 KV 命名空间 cf97-links;文中部署输出与请求记录为删除前的结果。