踩坑实录:D1 数据库的事务陷阱

踩坑实录:D1 数据库的事务陷阱

Cloudflare D1 很好用,但事务坑了我一次~

我写了个批量插入的脚本,以为 INSERT … SELECT 会自动 wrap 成事务~

结果跑到一半报错,插了一半数据,回滚不了~

教训:D1 的事务要显式开启~

错误的写法

// 我以为这是一条语句,会自动事务
await db.exec(`
  INSERT INTO logs (id, content)
  SELECT id, content FROM temp_logs
`);

D1 的 exec() 不保证事务~

如果 SELECT 返回 100 条,插到第 50 条报错,前 49 条已经写进去了~

正确的写法

// 显式开启事务
const batch = [];
const rows = await db.prepare('SELECT * FROM temp_logs').all();

for (const row of rows.results) {
  batch.push(
    db.prepare('INSERT INTO logs (id, content) VALUES (?, ?)')
      .bind(row.id, row.content)
  );
}

// 一次性提交
await db.batch(batch);

db.batch() 会把所有操作 wrap 成一个事务~

要么全成功,要么全回滚~

另一个坑:Batch 大小限制

D1 的 batch 最多 100 条语句~

我有次一次 batch 了 500 条,直接报错 batch too large~

解决方案:分批提交

const BATCH_SIZE = 100;
for (let i = 0; i < batch.length; i += BATCH_SIZE) {
  await db.batch(batch.slice(i, i + BATCH_SIZE));
}

第三个坑:Read-After-Write 不一致

D1 的读操作可能读到旧数据,因为它是最终一致性~

我写入一条记录后立即 SELECT,有时查不到~

解决方案:加延迟或用 RETURNING

// 写入后立即返回
const result = await db.prepare('INSERT INTO logs (content) VALUES (?) RETURNING *')
  .bind(content)
  .first();

我学到的

事务要显式:用 db.batch(),别指望 exec() 自动事务 batch 限制 100 条:多了要分批 读可能不一致:写后立即读用 RETURNING

这三个坑,我踩了两天才搞清楚~

希望你别踩~

核心观点

数据库操作,事务是第一位的~

没有事务保护,一次失败就能搞乱你的数据~


你踩过 D1 的坑吗,是什么~