Django 3.2迁移PostgreSQL 13(Google Cloud SQL)时出现死锁问题求助
解决Django迁移时PostgreSQL死锁问题
我之前处理过类似的生产环境死锁问题,咱们先把你的情况拆解清楚,再一步步解决。
死锁根源分析
从你拿到的Cloud日志来看,死锁的核心是迁移操作的排他锁和实时业务的插入锁形成了循环等待:
- 迁移进程(6445)在执行
ALTER TABLE "custom_requestlog" DROP CONSTRAINT ...,这个操作需要获取custom_requestlog表的AccessExclusiveLock——这是PostgreSQL最严格的锁,会阻塞所有其他对该表的读写操作。但此时你的业务进程(9249)正在插入custom_requestlog,已经拿着该表的RowShareLock,直接堵住了迁移进程。 - 同时,业务进程插入时因为外键关联
auth_user,需要获取auth_user的RowShareLock,但迁移进程此时可能已经拿着auth_user的某个排他锁(比如ALTER操作间接触发的锁),反过来堵住了业务进程。 - 两边互相等对方释放锁,就形成了死锁。
紧急处理方案(临时解决当前死锁)
如果现在迁移卡着不动,你可以先登录PostgreSQL终端,终止其中一个死锁进程,让另一个完成:
-- 先查询死锁进程的PID SELECT pid, query FROM pg_stat_activity WHERE state = 'active' AND query LIKE '%custom_requestlog%'; -- 终止业务插入进程(替换成你查到的PID,比如9249) SELECT pg_terminate_backend(9249);
这样迁移就能继续执行,但这只是临时方案,下次迁移还会遇到同样问题,得用下面的长期解决方案。
长期解决方案(避免后续死锁)
1. 低峰期执行迁移,暂停相关业务流量
这是最稳妥的办法:
- 选凌晨或业务流量最低的时间段执行迁移。
- 临时暂停写入
custom_requestlog的业务:比如你的Google Stackdriver Uptime检查可以临时关掉,或者让应用在迁移期间跳过日志写入,迁移完成后再恢复。 - 如果是容器化部署(比如K8s),可以先把应用实例缩容到0,执行完迁移再扩回去;或者开启维护模式,让所有请求返回503直到迁移结束。
2. 调整迁移执行策略
- 单独执行这个DROP CONSTRAINT迁移:把这个操作从迁移集合里拆出来,单独运行,减少事务持有锁的时间。
- 关闭迁移的原子性:在对应的迁移文件里设置
atomic = False,这样DROP操作会直接执行,而不是包在一个大事务里(默认原子迁移会把所有操作放在一个事务中,锁持有时间更长)。注意:非原子迁移如果失败,需要手动回滚,所以一定要先备份数据库。
示例迁移文件修改:from django.db import migrations, models class Migration(migrations.Migration): atomic = False # 新增这一行 dependencies = [ # 你的依赖项 ] operations = [ migrations.AlterField( # 你的DROP CONSTRAINT操作 ), ] - 执行迁移时加上详细日志,方便排查:
python manage.py migrate --verbosity 2 --noinput
3. 优化业务逻辑,减少锁冲突
- 异步写入日志:把
custom_requestlog的插入改成异步操作,比如用Celery或者Google Cloud Tasks,实时请求只把日志数据放到队列里,后台异步写入数据库。这样实时请求不会直接和迁移抢锁。 - 临时修改代码跳过日志:迁移前临时注释掉写入
custom_requestlog的代码,迁移完成后再恢复,虽然麻烦但很有效。
4. 用PostgreSQL Advisory Lock协调迁移和业务
通过自定义锁让迁移和业务互斥:
- 在迁移文件里加锁:
from django.db import migrations, connection class Migration(migrations.Migration): atomic = False operations = [ migrations.RunSQL("SELECT pg_advisory_lock(12345);"), # 自定义锁ID,选一个唯一数字即可 migrations.AlterField( # 你的DROP CONSTRAINT操作 ), migrations.RunSQL("SELECT pg_advisory_unlock(12345);"), ] - 在应用插入日志的代码里加锁等待:
这样迁移期间,业务的插入请求会自动等待迁移完成后再执行,彻底避免锁冲突。from django.db import connection from yourapp.models import CustomRequestLog def log_request(request_data): with connection.cursor() as cursor: # 设置锁超时5秒,避免无限等待 cursor.execute("SELECT pg_advisory_lock_timeout(5000);") # 尝试获取锁,拿不到就等5秒 cursor.execute("SELECT pg_advisory_lock(12345);") # 执行插入操作 CustomRequestLog.objects.create(**request_data) with connection.cursor() as cursor: cursor.execute("SELECT pg_advisory_unlock(12345);")
注意事项
- 不管用哪种方案,迁移前一定要备份数据库,生产环境容不得半点马虎。
- 迁移完成后,记得恢复所有暂停的业务(比如Uptime检查)。
内容的提问来源于stack exchange,提问作者Maverick
相关产品推荐
相关产品推荐

