Cloudflare Workers D1 数据库
KV 适合简单的键值存储,但遇到需要多字段关联查询、条件过滤、聚合统计的业务,就要用关系型数据库。这一篇学习 D1,基于 SQLite 的边缘数据库,并掌握 KV 加 D1 的缓存旁路模式。
D1 是什么
D1 是 Cloudflare 提供的关系型数据库,底层基于 SQLite,支持标准 SQL。它和 Worker 绑定在一起,代码里直接执行 SQL 语句,不需要单独部署数据库服务器。
KV 与 D1 对比
先弄清两者的定位差异
| 对比项 | KV | D1 |
|---|---|---|
| 数据结构 | 键值对 | 表、行、列 |
| 查询方式 | 按键名精确查找 | SQL,支持条件、排序、聚合 |
| 一致性 | 最终一致 | 强一致(单主) |
| 读取速度 | 极快,就近节点直接返回 | 相对慢,需要查数据库 |
| 适用场景 | 配置、缓存、简单数据 | 业务数据、复杂查询 |
| 单条限制 | 值最大 25MB | 行数据视表结构而定 |
结论是,KV 当缓存和配置用,D1 当正式数据库用。
创建数据库
wrangler d1 create my-db
命令输出包含 database_id,形如
✅ Successfully created DB 'my-db'
┌───────────────┬──────────────────────────────────┐
│ database_id │ a1b2c3d4e5f6a7b8c9d0e1f2 │
└───────────────┴──────────────────────────────────┘
绑定配置
在 wrangler.toml 中添加 D1 绑定
name = "my-worker"
main = "src/index.js"
compatibility_date = "2026-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "a1b2c3d4e5f6a7b8c9d0e1f2"
绑定后代码里通过 env.DB 操作数据库。
建表
先写一个 schema.sql 文件
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER
);
分别应用到本地和线上数据库
# 应用到本地(开发环境)
wrangler d1 execute my-db --local --file=./schema.sql
# 应用到线上
wrangler d1 execute my-db --remote --file=./schema.sql
之后每次改表结构都要同步执行这两条命令,保持本地和线上结构一致。
增删改查
D1 的 API 核心是 prepare 加 bind 加执行方法
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const parts = url.pathname.split("/").filter(Boolean);
const method = request.method;
if (parts[0] === "api" && parts[1] === "users") {
// 查询列表
if (method === "GET" && !parts[2]) {
const { results } = await env.DB.prepare(
"SELECT * FROM users"
).all();
return Response.json(results);
}
// 查询单个
if (method === "GET" && parts[2]) {
const user = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(parts[2]).first();
if (!user) return Response.json({ error: "用户不存在" }, { status: 404 });
return Response.json(user);
}
// 新增
if (method === "POST") {
const body = await request.json();
const { meta } = await env.DB.prepare(
"INSERT INTO users (name, age) VALUES (?, ?)"
).bind(body.name, body.age).run();
return Response.json({ id: meta.last_row_id }, { status: 201 });
}
// 删除
if (method === "DELETE" && parts[2]) {
await env.DB.prepare(
"DELETE FROM users WHERE id = ?"
).bind(parts[2]).run();
return Response.json({ ok: true });
}
}
return new Response("页面不存在", { status: 404 });
},
};
三个执行方法的分工
| 方法 | 用途 | 典型语句 |
|---|---|---|
.all() |
查询多行 | SELECT |
.first() |
查询第一行 | SELECT … LIMIT |
.run() |
执行写操作 | INSERT、UPDATE、DELETE |
查询结果的结构也要记住,.all() 返回 { results: [...] },.first() 直接返回一行对象,.run() 返回 { meta: { last_row_id } },last_row_id 是自增主键的新值。
参数化查询防注入
上面所有 SQL 都用了 ? 占位符加 .bind() 传参,这是必须养成的安全习惯。
// 正确,参数化
const user = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(id).first();
// 错误,拼接字符串有注入风险
const user = await env.DB.prepare(
`SELECT * FROM users WHERE id = ${id}`
).first();
参数化查询由平台负责转义,能彻底防住 SQL 注入,永远不要用字符串拼接方式构造 SQL。
KV 加 D1 缓存旁路模式
D1 查询比 KV 慢,频繁查询的接口可以先用 KV 缓存结果,这就是缓存旁路(Cache Aside)模式,三条规则
| 时机 | 操作 |
|---|---|
| 读数据 | 先查 KV,命中直接返回,未命中查 D1 并写回 KV |
| 写数据 | 只写 D1,同时删除对应 KV 缓存 |
| 缓存时间 | 给 KV 设置 TTL,防止数据长期不更新 |
const CACHE_TTL = 300; // 5 分钟
// 查询用户列表,带缓存
async function getUsers(env) {
// 1. 先查 KV
const cached = await env.MY_KV.get("cache:users");
if (cached) {
return { results: JSON.parse(cached), fromCache: true };
}
// 2. 未命中,查 D1
const { results } = await env.DB.prepare("SELECT * FROM users").all();
// 3. 写回 KV,带过期时间
await env.MY_KV.put("cache:users", JSON.stringify(results), {
expirationTtl: CACHE_TTL,
});
return { results, fromCache: false };
}
// 新增用户时删除缓存
async function createUser(env, body) {
await env.DB.prepare("INSERT INTO users (name, age) VALUES (?, ?)")
.bind(body.name, body.age).run();
await env.MY_KV.delete("cache:users");
}
这个模式能显著降低 D1 的读取压力,是生产环境最常用的组合拳。注意缓存删不掉时会短暂读到旧数据,配合 TTL 兜底,业务上可接受即可。
免费额度
D1 免费计划提供 5GB 存储,每天 500 万行读取、10 万行写入。个人项目远用不完,但同样建议关注仪表盘用量。
到这里,数据层的能力已经齐了。下一篇学习定时任务,让 Worker 在固定时间自动执行。