You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免多Node.js Lambda并发向PostgreSQL插入重复记录?

解决Lambda并发下PostgreSQL重复插入的更优方案

嘿,这个并发场景下的竞态条件问题太常见了!你现在用的唯一约束加重试方案确实能解决,但确实有点繁琐——其实PostgreSQL本身就提供了更优雅的原子性解决方案,结合Knex的API可以一步搞定,完全不用在应用层处理重试逻辑。

核心思路:用PostgreSQL的INSERT ... ON CONFLICT原子操作

PostgreSQL的INSERT ... ON CONFLICT语法(也叫"UPSERT")可以把「查询是否存在→插入/跳过」这两步合并成一条原子SQL语句,数据库会在内部原子性地处理冲突,从根源上避免竞态条件。不管多少Lambda实例同时发起请求,数据库只会保证同一external_id的记录最多存在一条。

方案1:仅确保记录存在,不更新现有数据

如果你不需要修改已存在的记录,只是想保证external_id对应的记录存在(不存在就插入,存在就跳过),可以用ON CONFLICT DO NOTHING结合Knex的.ignore()方法:

// 尝试插入,冲突则跳过,返回新插入的记录(成功)或undefined(冲突)
let location = await Location.query()
  .insert({ name, external_id })
  .onConflict('external_id')
  .ignore()
  .returning('*');

// 如果返回undefined,说明记录已存在,直接查询获取
if (!location) {
  location = await Location.query().where({ external_id }).first();
}

方案2:一步获取已存在/新插入的记录(更简洁)

如果想省掉后续的查询步骤,可以用ON CONFLICT DO UPDATE(即使不修改任何字段),这样不管插入成功还是冲突,都会返回对应的记录:

// 插入新记录,或冲突时返回已存在的记录(不修改任何字段)
let location = await Location.query()
  .insert({ name, external_id })
  .onConflict('external_id')
  .doUpdate({ 
    // 这里设置为原字段值,相当于不修改数据,只是触发返回逻辑
    name: knex.raw('??', ['name']) 
  })
  .returning('*');

如果需要在冲突时更新现有记录的字段(比如更新name),只需要把doUpdate里的字段改成新值即可:

// 插入新记录,或冲突时更新name字段
let location = await Location.query()
  .insert({ name, external_id })
  .onConflict('external_id')
  .doUpdate({ name: knex.raw('EXCLUDED.name') }) // EXCLUDED指代插入语句中的值
  .returning('*');

为什么这个方案更优?

  • 原子性保障:单条SQL语句由数据库原子处理,彻底消除竞态条件,不需要担心多个实例同时插入的问题。
  • 代码更简洁:省去了应用层的重试逻辑和二次判断,代码行数更少,可读性更高。
  • 性能更好:减少了一次数据库往返(原来的方案是「查询→插入」两次请求,现在是一次请求),在高并发场景下能降低数据库负载。

前提条件

和你原来的方案一样,必须先给external_id字段添加唯一约束,否则ON CONFLICT无法识别冲突:

ALTER TABLE locations ADD CONSTRAINT unique_external_id UNIQUE (external_id);

内容的提问来源于stack exchange,提问作者user2719094

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:27:39