如何避免多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
相关产品推荐
相关产品推荐

