Django+PostgreSQL项目级自增字段并发请求触发504超时问题排查
并发场景下django-sequences锁库导致Apigee 504超时的问题排查与解决
你遇到的这个情况我经手过好几个类似案例——用django-sequences实现按Project维度的Item自增ID,单请求跑起来顺得很,一到并发场景就触发数据库锁,直接被Apigee的10秒超时卡出504。咱们先把根因拆透,再给你几个落地可行的解决办法。
问题原因拆解
要搞懂为什么并发会炸,得先摸清楚django-sequences的底层逻辑:
- 这个包会在你的PostgreSQL库中自动创建一张
sequences_sequence表,专门存每个自定义序列的当前最大值 - 当你调用
get_next_value()时,它默认会开启事务并对整张sequences_sequence表加排他锁(因为你设置了nowait=False),然后找到对应Project的序列记录,递增后返回新值 - 并发请求涌进来时,所有请求都会排队等这把表级锁释放,而Apigee的超时只有10秒,一旦排队时间超了,直接返回504
- 再加上市面上你的代码逻辑:只有当
instance.identifier < 0时才会触发序列生成,多个请求同时命中这个分支的话,锁等待的冲突会更集中
可行解决方案
1. 把表级锁改成行级锁(最推荐,根治问题)
django-sequences默认的表级锁是罪魁祸首,咱们可以自己实现一个行级锁的序列逻辑,只锁当前Project对应的那条记录,不影响其他Project的请求。
先建一个专门的序列表:
from django.db import models class ProjectItemSequence(models.Model): project = models.OneToOneField(Project, on_delete=models.CASCADE, unique=True) current_id = models.IntegerField(default=0) class Meta: db_table = "project_item_sequence"
然后写一个原子操作的函数来获取下一个ID:
from django.db import transaction def get_next_project_item_id(project): with transaction.atomic(): # 先获取或创建对应Project的序列记录 sequence, _ = ProjectItemSequence.objects.get_or_create(project=project) # 只锁定当前Project的这一行,其他Project的请求不受影响 locked_sequence = ProjectItemSequence.objects.select_for_update().get(id=sequence.id) locked_sequence.current_id += 1 locked_sequence.save() return locked_sequence.current_id
最后把你的业务代码改成调用这个函数:
if instance.identifier < 0: instance.identifier = get_next_project_item_id(instance.project)
这样一来,不同Project的并发请求完全不会互相阻塞,锁的粒度被压缩到最小,超时问题基本就能解决。
2. 给django-sequences加重试逻辑(快速改代码)
如果不想重新实现序列逻辑,可以调整get_next_value()的参数,让请求遇到锁时直接抛出异常,然后在代码里加重试机制,避免长时间等待锁释放:
from django.db import OperationalError import time if instance.identifier < 0: max_retries = 3 retry_count = 0 success = False while retry_count < max_retries: try: with transaction.atomic(): instance.identifier = get_next_value( f'project__{instance.project.id}__item__identifier', initial_value=1, nowait=True # 遇到锁直接抛异常,不等待 ) success = True break except OperationalError: retry_count += 1 time.sleep(0.1) # 间隔100ms重试,避免频繁请求 if not success: raise ValueError("无法生成Item标识,请稍后重试")
这种方式适合并发量中等的场景,能快速降低超时概率,但如果并发量特别大,重试可能还是会失败,只能作为过渡方案。
3. 预生成序列值(高并发场景终极方案)
如果你的系统并发量特别高,比如每秒几百个请求,那最好的办法是把序列生成逻辑从数据库搬到缓存(比如Redis)里,预生成一批ID存在缓存中,请求过来直接取缓存里的值:
- 用Redis的原子操作
INCR来维护每个Project的当前ID - 每次预取一批(比如100个)ID存在Redis的列表里
- 请求过来时,从列表里弹出一个ID,如果列表为空,就批量生成100个新的存入列表
这种方式完全绕开了数据库锁的问题,性能拉满,但需要引入Redis,增加了系统复杂度,适合高并发场景。
4. 临时调整超时时间(应急用)
如果上面的方案都来不及上线,可以临时把Apigee的超时时间从10秒调到30秒,给锁等待多留一点时间,但这只是权宜之计,不能从根本上解决并发阻塞的问题,长期来看还是要优化锁的粒度。
内容的提问来源于stack exchange,提问作者user2880391
相关产品推荐
相关产品推荐

