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

启用migrationsRun时多服务器并发执行TypeORM迁移的问题处理

解决多Pod并发执行TypeORM迁移的方案

1. 用PostgreSQL Advisory Lock实现迁移互斥

在迁移代码中加入数据库级别的排他锁,确保同一时间只有一个Pod能执行该迁移操作。修改迁移类的up方法:

public async up(queryRunner: QueryRunner): Promise<void> {
  // 用迁移的时间戳ID作为唯一锁标识,避免和其他迁移冲突
  const lockId = 1662924574480;
  // 获取advisory锁,其他请求会阻塞直到锁释放
  await queryRunner.query(`SELECT pg_advisory_lock(${lockId})`);

  try {
    await queryRunner.query(`ALTER TYPE "item_type_enum" RENAME TO "item_type_enum_old"`);
    await queryRunner.query(
      `CREATE TYPE "item_type_enum" AS ENUM('CLOTHING', 'FOOD', 'JEWELLERY', 'SPORTS')`,
    );

    await queryRunner.query(`ALTER TABLE "orders" ALTER COLUMN "item_type"
      SET DATA TYPE "item_type_enum"
        USING (
          CASE "item_type"::text
            WHEN '1' THEN 'CLOTHING'
            WHEN '2' THEN 'FOOD'
            WHEN '3' THEN 'JEWELLERY'
            WHEN '4' THEN 'SPORTS'
          END
        )::"item_type_enum";`);
    await queryRunner.query(`DROP TYPE "item_type_enum_old"`);
  } finally {
    // 无论迁移成功失败,都要释放锁
    await queryRunner.query(`SELECT pg_advisory_unlock(${lockId})`);
  }
}

Advisory Lock是PostgreSQL提供的会话级锁,同一锁ID只能被一个进程持有,从根源上避免并发修改枚举类型的冲突。

2. 关闭自动迁移,改为手动条件执行

关闭TypeORM的migrationsRun自动执行开关,在NestJS启动时手动判断迁移状态,结合锁机制执行迁移:
首先修改DataSource配置:

export default new DataSource({
  ...
  migrationsRun: false, // 关闭自动迁移
});

然后在main.ts中添加迁移逻辑:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const dataSource = await AppDataSource.initialize();
  
  const migrationRepo = dataSource.getRepository(Migration);
  const targetMigration = await migrationRepo.findOne({
    where: { name: 'changeItemTypeEnum1662924574480' }
  });

  if (!targetMigration) {
    const lockId = 1662924574480;
    await dataSource.query(`SELECT pg_advisory_lock(${lockId})`);
    
    try {
      // 二次检查,防止等待锁期间其他Pod已完成迁移
      const recheckMigration = await migrationRepo.findOne({
        where: { name: 'changeItemTypeEnum1662924574480' }
      });
      if (!recheckMigration) {
        await dataSource.runMigrations();
      }
    } finally {
      await dataSource.query(`SELECT pg_advisory_unlock(${lockId})`);
    }
  }

  await app.listen(3000);
}
bootstrap();

这种方式先判断迁移是否已执行,再通过锁避免并发,减少不必要的迁移执行。

3. 优化枚举修改的迁移逻辑

换一种枚举更新方式,避免直接重命名枚举类型的高冲突操作:

public async up(queryRunner: QueryRunner): Promise<void> {
  // 1. 添加临时文本列存储转换后的值
  await queryRunner.query(`ALTER TABLE "orders" ADD COLUMN "item_type_temp" VARCHAR`);
  // 2. 将旧枚举值转换为目标文本存入临时列
  await queryRunner.query(`UPDATE "orders" SET "item_type_temp" = 
    CASE "item_type"::text
      WHEN '1' THEN 'CLOTHING'
      WHEN '2' THEN 'FOOD'
      WHEN '3' THEN 'JEWELLERY'
      WHEN '4' THEN 'SPORTS'
    END`);
  // 3. 删除旧枚举列
  await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "item_type"`);
  // 4. 创建新枚举并添加列
  await queryRunner.query(`CREATE TYPE "item_type_enum" AS ENUM('CLOTHING', 'FOOD', 'JEWELLERY', 'SPORTS')`);
  await queryRunner.query(`ALTER TABLE "orders" ADD COLUMN "item_type" "item_type_enum"`);
  // 5. 将临时列值同步到新枚举列
  await queryRunner.query(`UPDATE "orders" SET "item_type" = "item_type_temp"::"item_type_enum"`);
  // 6. 清理临时列和旧枚举
  await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "item_type_temp"`);
  await queryRunner.query(`DROP TYPE IF EXISTS "item_type_enum_old"`);
}

分步操作降低了对枚举类型的直接修改冲突,建议配合Advisory Lock一起使用。

4. Kubernetes层面控制Pod启动顺序

利用Kubernetes的podManagementPolicy: OrderedReady让Pod逐个启动,同时通过Init Container执行迁移:
在Deployment配置中添加:

spec:
  podManagementPolicy: OrderedReady
  replicas: 3
  template:
    spec:
      initContainers:
      - name: run-migrations
        image: your-backend-image
        command: ["node", "dist/main.js", "--migrate-only"]
        env:
        - name: DB_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url

然后在main.ts中处理--migrate-only参数,只执行迁移任务后退出:

if (process.argv.includes('--migrate-only')) {
  const dataSource = await AppDataSource.initialize();
  await dataSource.runMigrations();
  process.exit(0);
}

第一个Pod的Init Container完成迁移后,后续Pod的Init Container会因为迁移已存在而直接跳过,避免并发执行。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 09:57:17