多并行API请求致PostgreSQL数据覆盖问题排查与解决
问题
我有一个用于上传图片并更新数据库表的接口,并发发送3个(或2个以上)请求时出现异常:
- 第一个请求成功上传图片并更新数据库表;
- 第二个请求上传图片后能看到第一个请求的数据库变更,完成更新;
- 第三个请求上传图片后无法看到第二个请求的变更,完成更新;
- 最终仅第一个和第三个请求的数据库变更生效,第二个请求的变更被覆盖或未生效。
使用pg npm包,请问该问题出在代码还是pg包?该如何解决?
Controller 代码
@UseStaffPermissionsGuards('upsert', 'VehicleCondition') @ApiBody({ type: VehiclePhotoConditionInfoImageDTO }) @ApiResponse({ status: 201 }) @Post(':id/photos/:photoConditionId/image') @ApiConsumes('multipart/form-data') @UseInterceptors(FilesInterceptor('images'), FilesToBodyInterceptor) async upsertImages( @Param('id') vehicleId: string, @Param('photoConditionId') photoConditionId: string, @Body() vehiclePhotoConditionInfoImages: VehiclePhotoConditionInfoImageDTO, ): Promise<void> { return this.vehiclePhotoConditionService.upsertImages( vehicleId, photoConditionId, vehiclePhotoConditionInfoImages, ); }
Service 代码
async upsertImages( vehicleId: string, vehiclePhotoConditionId: string, vehiclePhotoConditionImage: VehiclePhotoConditionInfoImageDTO, ): Promise<void> { await this.isVehicleExist(vehicleId); const vehiclePhotoCondition = await this.getOne(vehicleId, vehiclePhotoConditionId); if (!vehiclePhotoCondition) { throw new BadRequestException( `The vehicle photo condition ${vehiclePhotoConditionId} is not found`, ); } const imageKeys = await this.handleImages(vehiclePhotoConditionId, vehiclePhotoConditionImage); const updatedVehiclePhotoConditions = vehiclePhotoCondition.info.map((data) => { if (data.vehiclePart === vehiclePhotoConditionImage.vehiclePart) { data.uploadedImagesKeys.push(...imageKeys); } return data; }); const query = sql .update('vehicle_photo_condition', { info: JSON.stringify(updatedVehiclePhotoConditions), updated_at: sql('now()'), }) .where({ id: vehiclePhotoConditionId }); await this.db.query(query.toParams()); }
问题原因与解决办法
这个问题不是pg包的问题,是代码里的并发更新逻辑存在竞态条件(Race Condition)。
原因分析
你的更新流程是:
- 查询数据库获取当前的
vehiclePhotoCondition数据 - 在内存中修改
info字段里的图片数组 - 将修改后的整个
info写回数据库
当多个请求并发执行时,会出现以下情况:
- 请求1查询到初始数据,修改后写回;
- 请求2查询到请求1修改后的数据,修改后写回;
- 请求3在请求2写回之前,就已经查询到了请求1修改后的数据,所以它的修改是基于请求1的版本,最后写回时会覆盖请求2的修改。
本质是因为查询和更新不是原子操作,中间的时间窗口被其他请求抢占了。
解决办法
有两种可靠的解决方式:
1. 使用PostgreSQL的行级锁(SELECT ... FOR UPDATE)
在查询数据时加上行级锁,确保同一时间只有一个请求能修改这条数据。修改Service里的getOne方法,让查询语句带上FOR UPDATE:
// 修改getOne方法的查询逻辑,示例: async getOne(vehicleId: string, photoConditionId: string) { const query = sql` SELECT * FROM vehicle_photo_condition WHERE id = ${photoConditionId} AND vehicle_id = ${vehicleId} FOR UPDATE; -- 加上行级锁 `; const result = await this.db.query(query); return result.rows[0]; }
这样当一个请求查询到这条数据后,其他请求必须等待该事务提交后才能查询到最新数据,避免了竞态条件。
2. 使用PostgreSQL的JSONB字段直接更新(推荐)
因为你是修改JSON字段里的数组,可以直接用PostgreSQL的JSONB操作符在数据库层面完成更新,不需要先查询再修改,这样整个更新是原子操作。修改Service里的更新逻辑:
// 替换原来的查询+更新逻辑,直接用JSONB操作 async upsertImages( vehicleId: string, vehiclePhotoConditionId: string, vehiclePhotoConditionImage: VehiclePhotoConditionInfoImageDTO, ): Promise<void> { await this.isVehicleExist(vehicleId); // 先验证记录存在(可选,也可以在更新时判断) const exists = await this.db.query(sql` SELECT 1 FROM vehicle_photo_condition WHERE id = ${vehiclePhotoConditionId} AND vehicle_id = ${vehicleId} `); if (!exists.rows.length) { throw new BadRequestException(`The vehicle photo condition ${vehiclePhotoConditionId} is not found`); } const imageKeys = await this.handleImages(vehiclePhotoConditionId, vehiclePhotoConditionImage); // 直接在数据库中更新JSONB数组 const query = sql` UPDATE vehicle_photo_condition SET info = jsonb_set( info, '{${vehiclePhotoConditionImage.vehiclePart}, uploadedImagesKeys}', (info->>${vehiclePhotoConditionImage.vehiclePart}->'uploadedImagesKeys')::jsonb || ${JSON.stringify(imageKeys)}::jsonb, true ), updated_at = now() WHERE id = ${vehiclePhotoConditionId}; `; await this.db.query(query); }
这种方式不需要先查询数据,直接在数据库层面完成数组追加,完全避免了竞态问题,性能也更好。
内容的提问来源于stack exchange,提问作者Mümin Celal Pinar
相关产品推荐
相关产品推荐

