Express API中TypeORM.save()并发调用重复插入问题求解
解决TypeORM并发Save主键冲突的方案
针对你遇到的并发请求下save()因主键重复报错的问题,有几个靠谱的解决方案,不用搞延迟请求这种临时方案:
1. 用TypeORM的upsert替代save(推荐)
TypeORM提供了专门的upsert方法,直接告诉数据库:如果主键存在就更新,不存在就插入,从根源上避免冲突。
调用方式很简单,把原来的save换成:
await getManager().upsert(User, userEntity, ['id']);
第三个参数['id']指定用id作为冲突判断的唯一键,TypeORM会根据你使用的数据库自动生成对应的SQL(比如MySQL的INSERT ... ON DUPLICATE KEY UPDATE,PostgreSQL的INSERT ... ON CONFLICT (id) DO UPDATE)。不管多少并发请求,数据库都会原子性处理冲突,不会报错。
2. 数据库悲观锁控制并发查询
如果必须保留save逻辑,可以在查询用户是否存在的时候加悲观写锁,让第二个请求等待第一个请求完成后再执行:
// 查询时加悲观写锁 const existingUser = await getManager().findOne(User, { where: { id: userId }, lock: { mode: 'pessimistic_write' } }); if (!existingUser) { // 从外部服务获取数据并创建实体 const user = new User(); user.id = userId; // ...赋值其他字段 await getManager().save(user); }
第一个请求拿到锁后,后续请求会等待锁释放,确保只有第一个请求会执行插入操作,后续请求直接读取已存在的用户。缺点是会增加请求等待时间,适合对一致性要求高但并发量不大的场景。
3. 应用层加内存锁避免重复逻辑
在getUser函数里加一个内存级别的锁,让相同userId的并发请求复用同一个异步操作,既避免重复调用外部服务,也避免并发插入:
// 用Map存正在处理的用户请求Promise const pendingUserRequests = new Map<string, Promise<User>>(); async function getUser(userId: string): Promise<User> { // 如果已有相同userId的请求在处理,直接复用其Promise if (pendingUserRequests.has(userId)) { return pendingUserRequests.get(userId)!; } const requestPromise = (async () => { try { let user = await getManager().findOne(User, { id: userId }); if (user) return user; // 从外部服务获取数据 const externalData = await fetchUserFromExternalService(userId); user = new User(); user.id = userId; // ...赋值其他字段 // 这里也可以用upsert进一步保险 await getManager().upsert(User, user, ['id']); return user; } finally { // 处理完成后移除锁 pendingUserRequests.delete(userId); } })(); pendingUserRequests.set(userId, requestPromise); return requestPromise; }
这个方案在应用层拦截了重复请求,既减少了外部服务调用次数,也避免了数据库层面的冲突,性能最优,但要注意如果应用是多实例部署,内存锁只在单个实例生效,这种情况还是得结合数据库层面的方案。
内容的提问来源于stack exchange,提问作者johannchopin
相关产品推荐
相关产品推荐

