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

多并行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)。

原因分析

你的更新流程是:

  1. 查询数据库获取当前的vehiclePhotoCondition数据
  2. 在内存中修改info字段里的图片数组
  3. 将修改后的整个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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 18:35:15