迁移前须创建InnoDB引擎的schema_lock表并实现抢占式锁,通过INSERT...SELECT原子操作加锁、shutdown函数释放,并在CI/CD中配置串行执行。
迁移执行前先检查
表是否存在
Laravel 本身不内置数据库迁移锁,多人同时运行
时,可能因并发写入
表或修改结构导致死锁、重复执行、表损坏。真正起作用的是你手动加的防护逻辑,不是框架默认行为。
常见错误现象:
,或者某次迁移只执行了一半、
表里多出两条相同
记录。
上线前务必确认数据库中是否存在
(或类似名称)表;没有就建一个:
该表必须是 InnoDB 引擎,MyISAM 不支持行级锁和事务,起不到保护作用
不要用
直接锁整张
表——它会被其他迁移命令绕过,且影响正常部署流程
在
的
里加轻量级抢占式锁
不能依赖 Artisan 命令启动后再判断,因为
命令初始化阶段就会读取迁移文件、连接数据库,锁必须在任何迁移操作前生效。
使用
实现原子抢占,比
两步更安全:
在
的
方法开头插入:
注意:MySQL 8.0+ 支持
,MariaDB 也兼容;低版本 MySQL 可改用
子查询替代
别用
或文件锁——部署环境常为无共享文件系统,Redis 缓存也可能跨实例失效
迁移成功后必须显式释放
锁不是自动消失的。如果进程崩溃、超时中断、或人为
,锁会一直挂着,后续所有迁移都会被拦住。
Laravel 13.2.0
PHP中文网提供Laravel 13.2.0版本下载,Laravel框架 是基于 PHP 8.3+ 的高性能框架,官方推荐通过 Composer 安装。它内置 AI SDK、JSON:API Resources 及原生向量搜索,支持属性驱动开发与队列路由,大幅提升开发效率。相比旧版,13.2.0 优化了缓存 TTL 管理与实时通信,无需 Redis 即可横向扩展。作为现代 Web 开发首选,它兼顾安全与极速体验,助您快速构建企业级应用。
下载
释放动作不能放在 migration 文件里(太晚),也不能只靠 finally(Artisan 异常退出时不一定触发)。最稳妥的方式是注册
:
在上面那段抢占逻辑之后追加:
这个函数会在脚本终止时执行,包括 fatal error、exit()、正常结束三种情况
不要用
包裹整个迁移流程——迁移命令本身已含事务控制,再套一层反而增加死锁风险
释放语句用
而非
,避免残留脏数据
CI/CD 环境要额外防重跑和跨节点冲突
GitLab CI、GitHub Actions 这类并行流水线,可能多个 job 同时拉代码、同时执行
,光靠单机锁没用。
关键点在于:锁必须是全局唯一的,且由调度层统一协调。
把迁移步骤设为「串行任务」:GitLab CI 中加
;GitHub Actions 用
键限制同一组 job 并发
禁止在多个 deploy 节点上都配置自动迁移——只允许主 deploy 节点执行,其他节点跳过:
若用 Kubernetes,可在 Job manifest 中加
和
,确保仅一个 Pod 执行
别信“加个 sleep 随机延迟就能错开”——概率性方案在高频率部署下迟早出事
真正难的不是写几行 SQL 加锁,而是让锁在各种异常路径下都不漏、不僵、不误杀。比如 SIGKILL 无法被捕获,这时候只能靠外部监控定期清理陈旧锁记录——这事得单独写个运维脚本,别指望 PHP 自己兜底。
schema_lockphp artisan migratemigrationsSQLSTATE[HY000]: General error: 1205 Deadlock found when trying to get lockmigrationsbatchschema_lockCREATE TABLE `schema_lock` (
`locked` TINYINT(1) NOT NULL DEFAULT '0',
`locked_at` TIMESTAMP NULL,
`locked_by` VARCHAR(255) NULL
) ENGINE=InnoDB;SELECT ... FOR UPDATEmigrationsAppServiceProviderboot()migrateINSERT ... SELECT ... FROM DUAL WHERE NOT EXISTSSELECT + INSERTapp/Providers/AppServiceProvider.phpboot()if (app()->runningInConsole() && in_array('migrate', $_SERVER['argv'] ?? [])) {
$locked = DB::insert("INSERT INTO schema_lock (locked, locked_at, locked_by)
SELECT 1, NOW(), ?
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM schema_lock WHERE locked = 1)", [gethostname()]);
if (!$locked) {
$this->warn('Migration lock held by another process. Aborting.');
exit(1);
}
}DUAL(SELECT 1)cacheschema_lockCtrl+Cregister_shutdown_functionregister_shutdown_function(function () {
if (app()->runningInConsole() && in_array('migrate', $_SERVER['argv'] ?? [])) {
DB::statement("DELETE FROM schema_lock WHERE locked = 1");
}
});DB::transaction()DELETEUPDATEmigrateresource_group: migrationsconcurrencyif [[ $(hostname) == "deploy-prod-01" ]]; then php artisan migrate; fiparallelism: 1completions: 1