Cloudflare D1 实战模式与最佳实践:从分页、缓存到多租户与备份恢复
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
本文围绕 Cloudflare D1(无服务器 SQLite 数据库)在 Cloudflare Workers 场景下的十余种高频数据访问模式展开,覆盖分页、动态条件查询、批量写入、KV 缓存、查询优化、多租户架构、会话存储、事件分析,以及付费计划专属的读复制与 Sessions API 长任务模式,最后给出基于 wrangler CLI 的时间旅行恢复与备份导入导出方案。阅读完本文,你将掌握一套可直接复制到 Worker 生产代码中的 D1 数据层写法,并理解每条模式背后的 API 机制与平台限制。
本文内容以仓库中 patterns.md 为骨架,结合同一参考集内的 README.md(D1 能力概览与平台限制)、api.md(查询方法 API)、configuration.md(wrangler.jsonc 配置与迁移)、gotchas.md(常见错误排查)交叉印证而成。你可以通过 SKILL.md 中的存储决策树(Relational SQL →d1/)了解 D1 在 Cloudflare 平台存储体系中的定位。
一、前置基础:D1 的核心 API 与运行环境
在进入模式代码之前,先厘清 D1 的几个关键事实(均出自本仓库 d1/README.md):
- 能力定位:D1 是 Cloudflare 托管的无服务器 SQLite 数据库,具备 SQLite 的 SQL 语义兼容性,并通过多数据库横向扩展(每库上限 10 GB,付费计划)支撑大规模场景。
- 架构哲学:D1 面向"每用户/每租户/每实体一个数据库"的模式设计,而非把一切塞进单个大库——这与下文的多租户 SaaS 模式直接呼应。
- 核心查询方法(详见 d1/api.md):
.all():返回全部行{ results, success, meta };.first():返回首行或null,.first(colName)返回单列值;.run():执行 INSERT/UPDATE/DELETE,返回meta(含rows_read、rows_written、last_row_id、changes);.raw():返回数组的数组,处理大数据集更高效。
- 安全底线:任何模式都必须使用
prepare()+bind()预编译语句,严禁字符串拼接 SQL(见 d1/gotchas.md 中 "SQL Injection Vulnerability" 条目)。
绑定声明与类型定义在 d1/configuration.md 中给出,所有模式示例中的env.DB即来自wrangler.jsonc的d1_databases配置,代码侧的Env接口形如:
interface Env { DB: D1Database; CACHE: KVNamespace; // KV 缓存模式需要 DB_REPLICA?: D1Database; // 读复制模式需要(付费计划) }二、分页模式(Pagination):COUNT 与数据一次往返
列表接口最朴素的分页实现是"先 COUNT 再 SELECT",但两次串行查询会放大延迟。D1 的batch()可以把多条语句打包在一次网络往返中执行,且整体具备原子事务语义(见 d1/api.md 的 Batch Operations 一节)。
async function getUsers({ page, pageSize }: { page: number; pageSize: number }, env: Env) { const offset = (page - 1) * pageSize; const [countResult, dataResult] = await env.DB.batch([ env.DB.prepare('SELECT COUNT(*) as total FROM users'), env.DB.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?').bind(pageSize, offset) ]); return { data: dataResult.results, total: countResult.results[0].total, page, pageSize, totalPages: Math.ceil(countResult.results[0].total / pageSize) }; }要点拆解:
(page - 1) * pageSize计算 OFFSET,注意 SQLite 的LIMIT ? OFFSET ?两处占位符都通过bind()顺序绑定;- 返回结构同时携带
total与totalPages,前端无需再发请求即可渲染分页条; - 若分页仅需"上一页/下一页"而无需总页数,可改用 keyset(游标)分页,用
WHERE created_at < ? ORDER BY created_at DESC LIMIT ?避免深分页扫描,但这不属于本模式必须项; - 数据量极大时,
COUNT(*)本身会扫描全表,配合下文索引策略缓解。
三、条件查询模式(Conditional Queries):动态 WHERE 的安全拼装
搜索/筛选接口的难点在于查询条件是运行时可变的:可能只有 name、可能只有 email,也可能三条件齐全。模式代码用两个数组分别收集"条件片段"与"绑定参数",最后统一bind(...params):
async function searchUsers(filters: { name?: string; email?: string; active?: boolean }, env: Env) { const conditions: string[] = [], params: (string | number | boolean | null)[] = []; if (filters.name) { conditions.push('name LIKE ?'); params.push(`%${filters.name}%`); } if (filters.email) { conditions.push('email = ?'); params.push(filters.email); } if (filters.active !== undefined) { conditions.push('active = ?'); params.push(filters.active ? 1 : 0); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; return await env.DB.prepare(`SELECT * FROM users ${whereClause}`).bind(...params).all(); }三点需要特别说明:
- WHERE 片段是代码静态拼接,参数永远走
bind()——name LIKE ?里的?由%${filters.name}%在运行时填充,这保证了不受 SQL 注入影响。被拼接进 SQL 文本的只有WHERE、AND等固定关键字,而非用户输入。 - 布尔值转 0/1:SQLite 没有原生布尔类型,底层用 INTEGER(0/1) 存储。代码里
filters.active ? 1 : 0正是 d1/gotchas.md 中 "Boolean Type Issues" 的对应写法。 - 无任何条件时生成空
whereClause,回退为全表SELECT * FROM users,逻辑自洽。
四、批量写入模式(Bulk Insert):同语句多参数打包
导入、批量注册、数据回填等场景需要一次性写入成百上千条记录。D1 支持"同一条 prepare 语句 + 不同绑定参数"的批量执行:
async function bulkInsertUsers(users: Array<{ name: string; email: string }>, env: Env) { const stmt = env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); const batch = users.map(user => stmt.bind(user.name, user.email)); return await env.DB.batch(batch); }实现原理与约束(依据 d1/api.md 与 d1/configuration.md 的 Plan Tiers 表格):
batch()接收多个已 bind 好的 Statement,一次往返执行,全部成功或全部失败(原子性);- 免费计划单次 batch 上限1,000 条语句,付费计划10,000 条。超过上限会触发 "Batch size exceeded",此时需要分块:
for (let i = 0; i < stmts.length; i += MAX_BATCH) await env.DB.batch(stmts.slice(i, i + MAX_BATCH))(见 d1/gotchas.md); - 注意批量 INSERT 需提前在迁移中建好表与约束,否则每条都会失败(但整体回滚)。
五、KV 缓存模式(Caching with KV):Cache-Aside 读缓存
热点数据(如用户资料、配置项)每次穿透 D1 会徒增读取计费与延迟。模式采用经典的Cache-Aside:先查 KV,未命中再查 D1 并回填 KV,设置 TTL:
async function getCachedUser(userId: number, env: { DB: D1Database; CACHE: KVNamespace }) { const cacheKey = `user:${userId}`; const cached = await env.CACHE?.get(cacheKey, 'json'); if (cached) return cached; const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); if (user) await env.CACHE?.put(cacheKey, JSON.stringify(user), { expirationTtl: 300 }); return user; }关键点:
get(cacheKey, 'json')让 KV 自动完成 JSON 序列化/反序列化;expirationTtl: 300即 5 分钟过期,配合数据变更侧主动CACHE.delete(cacheKey)可实现"写后失效";- 使用可选链
env.CACHE?.使代码在未配置 KV binding 的环境下优雅降级,直接回源 D1; - 根据仓库 bindings/README.md 的存储选型指南,KV 定位为"键值缓存、CDN 背书读取",与 D1 的关系是"缓存 + 主库",KV 中不应存放强一致要求的数据。
六、查询优化模式(Query Optimization):索引、限量与告别 N+1
原文档给出的优化对照非常直接,此处逐条展开:
// ✅ Use indexes in WHERE clauses —— 为高频过滤列建索引 const users = await env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(email).all(); // ✅ Limit result sets —— 始终限制返回行数,避免全表拉取 const recentPosts = await env.DB.prepare('SELECT * FROM posts ORDER BY created_at DESC LIMIT 100').all(); // ✅ Use batch() for multiple independent queries —— 多条独立查询一次往返 const [user, posts, comments] = await env.DB.batch([ env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId), env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(userId), env.DB.prepare('SELECT * FROM comments WHERE user_id = ?').bind(userId) ]); // ❌ Avoid N+1 queries —— 循环内逐条查询,多次往返 for (const post of posts) { const author = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(post.user_id).first(); // Bad: multiple round trips } // ✅ Use JOINs instead —— 一次查询关联数据 const postsWithAuthors = await env.DB.prepare(` SELECT posts.*, users.name as author_name FROM posts JOIN users ON posts.user_id = users.id `).all();配合仓库文档进一步说明:
- 索引的落地方式:在迁移文件中建索引,如
CREATE INDEX idx_users_email ON users(email);。复合索引、覆盖索引、部分索引的具体写法见 d1/configuration.md 的 Indexing Strategy 一节(例如CREATE INDEX idx_active_users ON users(email) WHERE active = 1;)。 - 验证索引是否生效:用
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?检查查询计划,这是 d1/gotchas.md 中 "Missing Indexes" 的排查手段。 - 30 秒查询超时:任何单查询超过 30 秒会被终止,优化不力的全表扫描正是超时主因(见 d1/README.md 的 Platform Limits 表)。
- 批量的取舍:
batch()适合互不依赖的查询;若查询之间存在依赖(如先 INSERT 再读取刚写入的行),应串行执行,避免逻辑错乱。
七、多租户 SaaS 模式(Multi-Tenant SaaS):每租户独立数据库
这是 D1 架构哲学的直接体现(见 d1/README.md:D1 专为 per-user/per-tenant/per-entity 数据库模式优化)。实现方式是按租户 ID 动态解析 binding,而非所有租户共享一张大表:
// Each tenant gets own database export default { async fetch(request: Request, env: { [key: `TENANT_${string}`]: D1Database }) { const tenantId = request.headers.get('X-Tenant-ID'); const data = await env[`TENANT_${tenantId}`].prepare('SELECT * FROM records').all(); return Response.json(data.results); } }关键机制与注意点:
- 动态 binding 访问:TypeScript 通过模板字面量类型
`TENANT_${string}`表达"一组以 TENANT_ 为前缀的 D1 binding",运行时用计算属性名env[\TENANT_${tenantId}`]` 取到对应数据库实例; - 租户隔离:每个租户拥有独立 schema 与数据空间,天然规避了"单库大表 + 行级租户过滤"的性能与安全边界问题;
- 创建租户库:租户开通时用
wrangler d1 create <tenant-db>创建独立库,并把新 binding 加入 configuration.md 中的d1_databases数组; - 租户上限:付费计划单库 10 GB,意味着"每个租户一个库"可支撑大量租户横向扩展;免费计划 500 MB/库,适合起步验证;
- 安全提醒:务必对
X-Tenant-ID做白名单/格式校验,防止构造恶意 key 命中不存在的 binding。
八、会话存储模式(Session Storage):带过期的 JOIN 验证
Web 应用中会话是典型的关系型数据:sessions表存 token 与过期时间,users表存用户主体,二者通过外键关联。模式提供建会话与验会话两个互补函数:
async function createSession(userId: number, token: string, env: Env) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); return await env.DB.prepare('INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, ?)').bind(userId, token, expiresAt).run(); } async function validateSession(token: string, env: Env) { return await env.DB.prepare('SELECT s.*, u.email FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.token = ? AND s.expires_at > CURRENT_TIMESTAMP').bind(token).first(); }细节说明:
- 过期时间的存储:
new Date(...).toISOString()生成 ISO 8601 文本,符合 SQLite 无原生 DATE/TIME 类型、建议用 TEXT(ISO 8601)或 INTEGER(unix 时间戳)存储的约定(见 d1/gotchas.md 的 "Date/Time Type Issues"); - 验证即 JOIN:一次查询同时校验 token 存在、未过期(
expires_at > CURRENT_TIMESTAMP),并顺带带出u.email,返回first()为null即视为会话失效; run()的返回值:INSERT/UPDATE 类语句用.run(),可通过result.meta.last_row_id取回自增主键(见 d1/api.md);- 定时清理:过期会话行可通过 Cron Trigger 定期执行
DELETE FROM sessions WHERE expires_at < CURRENT_TIMESTAMP清理,保持表体积可控。
九、分析与事件模式(Analytics/Events):JSON 元数据 + 分组聚合
埋点/行为日志类数据非常适合 D1:结构化字段 + 可序列化的 metadata + 分组聚合统计。模式给出写入与统计一对函数:
async function logEvent(event: { type: string; userId?: number; metadata: object }, env: Env) { return await env.DB.prepare('INSERT INTO events (type, user_id, metadata) VALUES (?, ?, ?)').bind(event.type, event.userId || null, JSON.stringify(event.metadata)).run(); } async function getEventStats(startDate: string, endDate: string, env: Env) { return await env.DB.prepare('SELECT type, COUNT(*) as count FROM events WHERE timestamp BETWEEN ? AND ? GROUP BY type ORDER BY count DESC').bind(startDate, endDate).all(); }要点:
- metadata 用 JSON 文本:
JSON.stringify(event.metadata)把任意对象压成 TEXT 列,天然适配 SQLite 无原生 JSON 类型的现实(SQLite 的 JSON 函数可按需在 SQL 中解析,但此模式选择最简方案); - GROUP BY + ORDER BY count DESC直接产出"事件类型热度排行",
BETWEEN ? AND ?完成时间窗口过滤; - 索引建议:为
events(type)、events(timestamp)或复合索引(timestamp, type)建索引,可显著加速统计查询; - 适用边界:D1 适合中等量级事件表;仓库中另有 analytics-engine 用于自定义指标点(
writeDataPoint),两者定位不同——事件明细与关系聚合用 D1,超高频指标计数可考虑 Analytics Engine。
十、读复制模式(Read Replication Pattern,付费计划):读写分离
读复制(付费附加项)让 Worker 自动路由到最近副本以降低读延迟,写入始终走主库。由于复制存在 100ms~2s 的延迟,读后写(read-after-write)场景必须回到主库读取以保证一致性:
interface Env { DB: D1Database; DB_REPLICA: D1Database; } export default { async fetch(request: Request, env: Env) { if (request.method === 'GET') { // Reads: use replica for lower latency const users = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE active = 1').all(); return Response.json(users.results); } if (request.method === 'POST') { const { name, email } = await request.json(); const result = await env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email).run(); // Read-after-write: use primary for consistency (replication lag <100ms-2s) const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(result.meta.last_row_id).first(); return Response.json(user, { status: 201 }); } } }配置与选型依据(见 d1/configuration.md 与 d1/README.md):
wrangler.jsonc中为同一database_id配置两个 binding:DB(主)+DB_REPLICA(副本);- 原文档明确给出分流原则:
- 副本适合:分析仪表盘、搜索结果、公开查询(允许最终一致性);
- 主库适合:读后写、金融交易、身份认证(要求强一致);
- 免费计划无读复制,代码需保证
DB_REPLICA未配置时能回退到主库(如可选链 + 回退逻辑)。
十一、Sessions API 模式(付费计划):突破 30 秒的长期任务
普通查询有 30 秒超时限制,但迁移、建索引、ANALYZE、大批量数据转换等任务必然超过该窗口。付费计划的 Sessions API 提供最长 15 分钟(900 秒)的长会话,且会话内语句共享快照视图。原文档给出两个典型场景:
场景一:带索引创建的迁移
// Migration with long-running session (up to 15 min) async function runMigration(env: Env) { const session = env.DB.withSession({ timeout: 600 }); // 10 min try { await session.prepare('CREATE INDEX idx_users_email ON users(email)').run(); await session.prepare('CREATE INDEX idx_posts_user ON posts(user_id)').run(); await session.prepare('ANALYZE').run(); } finally { session.close(); // Always close to prevent leaks } }场景二:分批游标式全量数据转换
// Bulk transformation with batching async function transformLargeDataset(env: Env) { const session = env.DB.withSession({ timeout: 900 }); // 15 min max try { const BATCH_SIZE = 1000; let offset = 0; while (true) { const rows = await session.prepare('SELECT id, data FROM legacy LIMIT ? OFFSET ?').bind(BATCH_SIZE, offset).all(); if (rows.results.length === 0) break; const updates = rows.results.map(row => session.prepare('UPDATE legacy SET new_data = ? WHERE id = ?').bind(transform(row.data), row.id) ); await session.batch(updates); offset += BATCH_SIZE; } } finally { session.close(); } }使用纪律(与 d1/api.md 的 Sessions API 一节一致):
- 超时范围:
withSession({ timeout })取值 1~900 秒; - 必须关闭:
session.close()必须放在finally中,否则造成资源泄漏(d1/gotchas.md 中 "Session not closed / resource leak" 专门警示); - 分页配合:大数据转换用
LIMIT ? OFFSET ?游标逐批取出,每批 1000 条再session.batch(updates),避免一次性加载全表; - 适用清单:迁移、
ANALYZE、大索引创建、批量变换;免费计划不可用。
十二、时间旅行与备份(Time Travel & Backups):一键恢复到任意时间点
D1 内建灾难恢复能力:Time Travel提供时间点恢复,免费计划保留 7 天恢复点,付费计划 30 天。同时支持整库/仅数据导出与导入,构成完整的备份-恢复闭环:
wrangler d1 time-travel restore <db-name> --timestamp="2024-01-15T14:30:00Z" # Point-in-time wrangler d1 time-travel info <db-name> # List restore points (7 days free, 30 days paid) wrangler d1 export <db-name> --remote --output=./backup.sql # Full export wrangler d1 export <db-name> --remote --no-schema --output=./data.sql # Data only wrangler d1 execute <db-name> --remote --file=./backup.sql # Import命令语义逐条说明:
time-travel restore --timestamp:把数据库恢复到指定 UTC 时间点的快照,注意时间戳需落在保留窗口内;time-travel info:先列出可用恢复点再决定恢复时间,避免盲目操作;export:--remote导出线上库;默认带 schema,--no-schema只导出数据;--output指定落盘文件;execute --file:将 SQL 文件导入线上库,实现恢复/迁移数据的灌入。
结合 d1/configuration.md 的 Import & Export 一节,还有三条重要边界:
- BLOB 二进制:导出可能损坏 BLOB,二进制文件应放 R2,D1 只存 URL/key(见 d1/gotchas.md 的 "BLOB data corrupted on export");
- 超大导出:超过 1 GB 的导出可能超时,需拆分处理;
- 导入非原子:
execute --file导入不具备原子性,需要事务保证的导入应在 Worker 内用batch()完成。
十三、模式选型速查与常见坑
模式速查表
| 场景 | 首选模式 | 核心 API |
|---|---|---|
| 列表分页 | Pagination | batch()+COUNT(*)+LIMIT/OFFSET |
| 动态筛选 | Conditional Queries | 条件数组 +bind(...) |
| 批量导入 | Bulk Insert | 同一prepare+batch() |
| 热点读 | KV Cache | KV.get/put+expirationTtl |
| 关联查询 | Query Optimization | JOIN/batch()/ 索引 |
| 租户隔离 | Multi-Tenant | 动态 bindingenv[`TENANT_${id}`] |
| 登录态 | Session Storage | expires_at+ JOIN 校验 |
| 行为埋点 | Analytics/Events | JSON metadata +GROUP BY |
| 低延迟读(付费) | Read Replication | DB_REPLICA读 /DB写 |
| 长任务(付费) | Sessions API | withSession+close() |
| 灾备恢复 | Time Travel & Backups | wrangler d1 time-travel/export/execute |
高频坑位(源自 d1/gotchas.md)
- SQL 注入:永远
prepare().bind(),禁止字符串插值拼 SQL; - no such table:迁移未执行(漏
--remote)或 binding 名不匹配; - UNIQUE constraint failed:捕获异常并返回 409;
- 布尔/日期类型:布尔用 0/1,时间用 ISO 8601 TEXT 或 unix 时间戳;
- N+1 与缺索引:用 JOIN/batch 替代循环查询,用
EXPLAIN QUERY PLAN验证索引命中; - 批次超限:免费 1,000 / 付费 10,000 条 batch 上限,超出需分块;
- 复制延迟:读后写一律走主库,否则可能读到旧数据。
本地开发与生产一致性
本地开发使用wrangler dev --persist-to=./.wrangler/state(d1/configuration.md),本地库文件位于.wrangler/state/v3/d1/<database-id>.sqlite,可用sqlite3直接检查。注意本地是 SQLite 单文件、生产是分布式 D1,行为与限制存在差异(见 d1/gotchas.md 的 "Local dev vs production behavior differs"),上线前务必在--remote上实测迁移与关键查询。
结语
本文以 patterns.md 的 11 个模式为骨架,逐一带入 D1 的 API 语义、平台限制与排错经验:免费/付费计划的批次与保留窗口差异、batch()的原子往返、Sessions API 的 15 分钟窗口、读复制的延迟权衡、Time Travel 的恢复窗口——每一条都能在 d1/api.md、d1/configuration.md、d1/gotchas.md 与 d1/README.md 中找到对应依据。把这套模式直接落入你的 Worker 数据层,即可在安全、性能与成本之间取得均衡。
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考