启用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
相关产品推荐
相关产品推荐

