D1 读复制通过在全球各区域更接近客户端的位置添加称为读副本(read replica)的只读数据库副本,降低读取查询延迟并扩展读取吞吐量。
要使用读复制,您必须使用 D1 Sessions API,否则所有查询将继续仅由主数据库执行。
会话封装了应用程序一个逻辑会话中的所有查询。例如,会话可能对应来自特定 Web 浏览器会话的所有查询。会话内的所有查询从满足您查询时效性需求的数据库实例读取。Sessions API 确保会话中所有查询的顺序一致性。
要体验 D1 读复制,使用 Sessions API 部署以下 Worker 代码,系统将提示您创建 D1 数据库并在该数据库上启用读复制。
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// A. Create the Session.
// When we create a D1 Session, we can continue where we left off from a previous
// Session if we have that Session's last bookmark or use a constraint.
const bookmark =
request.headers.get("x-d1-bookmark") ?? "first-unconstrained";
const session = env.DB01.withSession(bookmark);
try {
// Use this Session for all our Workers' routes.
const response = await withTablesInitialized(
request,
session,
handleRequest,
);
// B. Return the bookmark so we can continue the Session in another request.
response.headers.set("x-d1-bookmark", session.getBookmark() ?? "");
return response;
} catch (e) {
console.error({
message: "Failed to handle request",
error: String(e),
errorProps: e,
url,
bookmark,
});
return Response.json(
{ error: String(e), errorDetails: e },
{ status: 500 },
);
}
},
};export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
// A. Create the Session.
// When we create a D1 Session, we can continue where we left off from a previous
// Session if we have that Session's last bookmark or use a constraint.
const bookmark =
request.headers.get("x-d1-bookmark") ?? "first-unconstrained";
const session = env.DB01.withSession(bookmark);
try {
// Use this Session for all our Workers' routes.
const response = await withTablesInitialized(
request,
session,
handleRequest,
);
// B. Return the bookmark so we can continue the Session in another request.
response.headers.set("x-d1-bookmark", session.getBookmark() ?? "");
return response;
} catch (e) {
console.error({
message: "Failed to handle request",
error: String(e),
errorProps: e,
url,
bookmark,
});
return Response.json(
{ error: String(e), errorDetails: e },
{ status: 500 },
);
}
},
} satisfies ExportedHandler<Env>;
未使用读复制时,D1 将所有查询(读取和写入)路由到全球某一位置的特定数据库实例,称为主数据库实例。D1 请求延迟取决于用户与主数据库实例的物理距离。距离主数据库实例较远的用户由于网络往返时间 ↗会经历更长的请求延迟。
使用读复制时,D1 创建主数据库实例的多个异步复制副本,仅处理读取请求,称为读副本。D1 在 Cloudflare 网络的多个区域创建读副本。
即使用户可能距离主数据库实例很远,他们也可能靠近读副本。当 D1 将读取请求路由到读副本而非主数据库实例时,用户的读取查询响应更快。
D1 异步将主数据库实例的更改复制到所有读副本。这意味着在任何给定时间,读副本可能任意过时。主数据库实例中最新已提交数据复制到读副本所需的时间称为副本延迟(replica lag)。副本延迟和对单个副本的非确定性路由可能导致应用数据一致性问题。 D1 Sessions API 通过确保顺序一致性来解决此问题。 有关更多信息,请参阅副本延迟和一致性模型。
| 数据库实例类型 | 描述 | 如何处理写入查询 | 如何处理读取查询 |
|---|---|---|---|
| Primary database instance(主数据库实例) | 包含数据库“原始”副本的数据库实例 | 可以处理写入查询 | 可以处理读取查询 |
| Read replica database instance(读副本数据库实例) | 包含原始数据库副本的数据库实例,异步接收来自主数据库实例的更新 | 将任何写入查询转发到主数据库实例 | 使用自身的数据库副本处理读取查询 |
在全球各地拥有多个读副本的系统可提升数据库性能:
- 靠近读副本的用户查询延迟降低。通过缩短数据库实例与用户之间的物理距离,读取查询延迟降低,应用响应更快。
- 通过在多个副本间分配负载,读取吞吐量增加。由于多个数据库实例能够处理只读请求,您的应用可以在任何给定时间处理更多查询。
通过将 Sessions API 用于读复制,来自单个会话的所有查询从确保顺序一致性的数据库版本读取。这确保即使查询由不同读副本处理,您读取的数据库版本在逻辑上也是一致的。
D1 读复制通过为会话内每个查询附加书签(bookmark)来实现这一点。有关更多信息,请参阅书签(bookmark)。
读复制可在 Cloudflare 仪表板的数据库级别启用。检查 D1 数据库的 **Settings(设置)**以查看是否启用了读复制。
-
在 Cloudflare 仪表板中,前往 D1 页面。
Go to D1 SQL database ↗ -
选择现有数据库 > Settings(设置)> Enable Read Replication(启用读复制)。
要从任何可用数据库版本创建会话,使用不带任何参数的 withSession(),这将把第一个查询路由到任何数据库实例,主数据库实例或读副本均可。
const session = env.DB.withSession() // synchronous
// query executes on either primary database or a read replica
const result = await session
.prepare(`SELECT * FROM Customers WHERE CompanyName = 'Bs Beverages'`)
.run()withSession()与withSession("first-unconstrained")相同- 当应用不需要最新数据库版本时,此方法最佳。会话中的所有查询确保顺序一致性。
- 请参阅 D1 Workers Binding API 文档。
要从最新数据库版本创建会话,使用 withSession("first-primary"),这将把第一个查询路由到主数据库实例。
const session = env.DB.withSession(`first-primary`) // synchronous
// query executes on primary database
const result = await session
.prepare(`SELECT * FROM Customers WHERE CompanyName = 'Bs Beverages'`)
.run()- 当应用需要最新数据库版本时,此方法最佳。会话中的所有查询确保顺序一致性。
- 请参阅 D1 Workers Binding API 文档。
要从先前会话的上下文创建新会话,传递 bookmark 参数以保证会话从至少与提供的 bookmark 一样新的数据库版本开始。
// 从存储在 HTTP 头中的先前会话检索 bookmark
const bookmark = request.headers.get('x-d1-bookmark') ?? 'first-unconstrained';
const session = env.DB.withSession(bookmark)
const result = await session
.prepare(`SELECT * FROM Customers WHERE CompanyName = 'Bs Beverages'`)
.run()
// 为将来会话存储 bookmark
response.headers.set('x-d1-bookmark', session.getBookmark() ?? "")- 使用
bookmark启动会话确保新会话至少与生成给定bookmark的先前会话一样新。 - 请参阅 D1 Workers Binding API 文档。
要查看读副本的添加如何影响 D1 请求的处理,served_by_region 和 served_by_primary 字段在 D1 Result 的 meta 对象中返回。
const result = await env.DB.withSession()
.prepare(`SELECT * FROM Customers WHERE CompanyName = 'Bs Beverages'`)
.run();
console.log({
servedByRegion: result.meta.served_by_region ?? "",
servedByPrimary: result.meta.served_by_primary ?? "",
});- 所有 D1 远程请求都包含
served_by_region和served_by_primary字段,无论是否启用读复制或使用 Sessions API。在本地开发npx wrangler dev中,这些字段为undefined。
使用 REST API,设置 read_replication.mode: auto 以在 D1 数据库上启用读复制。
对于此 REST 端点,您需要具有 D1:Edit 权限的 API 令牌。如果您没有 API 令牌,请按照指南:创建 API 令牌。
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/d1/database/{database_id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"read_replication": {"mode": "auto"}}'const headers = new Headers({
"Authorization": `Bearer ${TOKEN}`
});
await fetch ("/v4/accounts/{account_id}/d1/database/{database_id}", {
method: "PUT",
headers: headers,
body: JSON.stringify(
{ "read_replication": { "mode": "auto" } }
)
}
)使用 REST API,设置 read_replication.mode: disabled 以在 D1 数据库上禁用读复制。
对于此 REST 端点,您需要具有 D1:Edit 权限的 API 令牌。如果您没有 API 令牌,请按照指南:创建 API 令牌。
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/d1/database/{database_id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"read_replication": {"mode": "disabled"}}'const headers = new Headers({
"Authorization": `Bearer ${TOKEN}`
});
await fetch ("/v4/accounts/{account_id}/d1/database/{database_id}", {
method: "PUT",
headers: headers,
body: JSON.stringify(
{ "read_replication": { "mode": "disabled" } }
)
}
)在 Cloudflare 仪表板上,检查 D1 数据库的 **Settings(设置)**以查看是否启用了读复制。
或者,GET D1 数据库 REST 端点返回读复制是启用还是禁用。
对于此 REST 端点,您需要具有 D1:Read 权限的 API 令牌。如果您没有 API 令牌,请按照指南:创建 API 令牌。
curl -X GET "https://api.cloudflare.com/client/v4/accounts/{account_id}/d1/database/{database_id}" \
-H "Authorization: Bearer $TOKEN"const headers = new Headers({
"Authorization": `Bearer ${TOKEN}`
});
const response = await fetch("/v4/accounts/{account_id}/d1/database/{database_id}", {
method: "GET",
headers: headers
});
const data = await response.json();
console.log(data.read_replication.mode);- 检查
result对象的read_replication属性"mode": "auto"表示已启用读复制"mode": "disabled"表示已禁用读复制
目前,D1 在每个支持的区域自动创建读副本,包括主数据库实例所在的区域。这些区域是:
- ENAM
- WNAM
- WEUR
- EEUR
- APAC
- OC
要查看读复制的影响并检查 D1 请求如何被额外数据库实例处理,您可以使用:
D1Result返回对象内的meta对象,包含新字段:served_by_regionserved_by_primary
- Cloudflare 仪表板,您可以在其中按处理 D1 请求的区域查看数据库指标明细。
D1 读复制内置于 D1,因此你无需为读副本支付额外的存储或计算费用。无论是否使用副本,你都会产生完全相同的 D1 用量计费,依据查询的 rows_read 和 rows_written。
D1 读复制有一些已知限制。
- Sessions API 仅通过 D1 Worker Binding 可用,尚无法通过 REST API 使用。
要考虑到副本延迟(replica lag),考虑 D1 的一致性模型很重要。一致性模型是一个逻辑框架,用于规范存在多个数据库实例时数据库系统如何服务用户查询(数据如何更新和访问)。不同模型在不同用例中有用。大多数数据库系统根据配置提供 read committed ↗、snapshot isolation ↗ 或 serializable ↗ 一致性模型。
考虑在分布式数据库系统中,没有明确框架来强制执行一致性模型时可能发生的情况。
- 您的 SQL 写入查询由主数据库实例处理。
- 您获得确认写入查询的响应。
- 您后续的 SQL 读取查询发送到读副本。
- 读副本尚未更新,因此不包含 SQL 写入查询的更改。从您的角度来看,返回的结果不一致。
使用 D1 Sessions API 时,您的查询获得书签,使读副本仅提供顺序一致的数据。
- SQL 写入查询由主数据库实例处理。
- 您获得确认写入查询的响应。您还会获得一个书签 (100),用于标识写入查询之后的数据库状态。
- 您后续的 SQL 读取查询发送到读副本,并提供书签 (100)。
- 读副本将等待更新到至少与提供的书签 (100) 一样新。
- 读副本更新后(bookmark 104),它处理您的读取查询,现在顺序一致。
在图中,返回的书签是 bookmark 104,与您读取查询中提供的 (bookmark 100) 不同。如果在您执行的两个写入/读取查询之间,来自其他客户端请求的其他写入也被复制到读副本,就可能发生这种情况。
D1 读复制提供顺序一致性 ↗。D1 创建数据库上所有操作的全局顺序,并使用书签(bookmark)识别查询已看到的最新数据库版本。然后,它使用至少与随查询传递的书签一样新的数据库实例来处理查询。
顺序一致性具有以下属性:
- 单调读(Monotonic reads):如果您连续执行两次读取(read-1,然后 read-2),read-2 不能读取 read-1 之前的数据库版本。
- 单调写(Monotonic writes):如果您执行 write-1 然后 write-2,所有进程都在 write-2 之前观察到 write-1。
- 写跟随读(Writes follow reads):如果您读取一个值,然后执行写入,后续写入必须基于刚读取的值。
- 读己之写(Read my own writes):如果您写入数据库,所有后续读取都会看到该写入。
您可能希望参考以下资源: