Cloudflare D1 实战模式与最佳实践:从分页、缓存到多租户与备份恢复
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.mdD1 能力概览与平台限制、api.md查询方法 API、configuration.mdwrangler.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; // 读复制模式需要付费计划 }二、分页模式PaginationCOUNT 与数据一次往返列表接口最朴素的分页实现是先 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/1SQLite 没有原生布尔类型底层用 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 KVCache-Aside 读缓存热点数据如用户资料、配置项每次穿透 D1 会徒增读取计费与延迟。模式采用经典的Cache-Aside先查 KV未命中再查 D1 并回填 KV设置 TTLasync 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索引、限量与告别 N1原文档给出的优化对照非常直接此处逐条展开// ✅ 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 N1 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.mdD1 专为 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 类型、建议用 TEXTISO 8601或 INTEGERunix 时间戳存储的约定见 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/EventsJSON 元数据 分组聚合埋点/行为日志类数据非常适合 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 自动路由到最近副本以降低读延迟写入始终走主库。由于复制存在 100ms2s 的延迟读后写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.mdwrangler.jsonc中为同一database_id配置两个 bindingDB主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 })取值 1900 秒必须关闭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 --timestamp2024-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二进制文件应放 R2D1 只存 URL/key见 d1/gotchas.md 的 BLOB data corrupted on export超大导出超过 1 GB 的导出可能超时需拆分处理导入非原子execute --file导入不具备原子性需要事务保证的导入应在 Worker 内用batch()完成。十三、模式选型速查与常见坑模式速查表场景首选模式核心 API列表分页Paginationbatch()COUNT(*)LIMIT/OFFSET动态筛选Conditional Queries条件数组 bind(...)批量导入Bulk Insert同一preparebatch()热点读KV CacheKV.get/putexpirationTtl关联查询Query OptimizationJOIN/batch()/ 索引租户隔离Multi-Tenant动态 bindingenv[TENANT_${id}]登录态Session Storageexpires_at JOIN 校验行为埋点Analytics/EventsJSON metadata GROUP BY低延迟读付费Read ReplicationDB_REPLICA读 /DB写长任务付费Sessions APIwithSessionclose()灾备恢复Time Travel Backupswrangler d1 time-travel/export/execute高频坑位源自 d1/gotchas.mdSQL 注入永远prepare().bind()禁止字符串插值拼 SQLno such table迁移未执行漏--remote或 binding 名不匹配UNIQUE constraint failed捕获异常并返回 409布尔/日期类型布尔用 0/1时间用 ISO 8601 TEXT 或 unix 时间戳N1 与缺索引用 JOIN/batch 替代循环查询用EXPLAIN QUERY PLAN验证索引命中批次超限免费 1,000 / 付费 10,000 条 batch 上限超出需分块复制延迟读后写一律走主库否则可能读到旧数据。本地开发与生产一致性本地开发使用wrangler dev --persist-to./.wrangler/stated1/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),仅供参考