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

Django批量更新引发数据库锁:求batch_size机制解析及解决方案

批量更新数据引发数据库锁的问题与解决方案咨询

问题场景与代码

我用以下代码批量更新数据:

from simple_history.utils import bulk_update_with_history
from django.utils.timezone import now

bulk_update_list = []
for chart in updated_chart_records:
    chart.coder_assignment_sequence = count
    chart.queue_id = work_queue_pk
    chart.level_id = role_id
updated_num = bulk_update_with_history(bulk_update_list, Chart, ["coder_assignment_sequence", "l1_auditor_assignment_sequence", "l2_auditor_assignment_sequence", "l3_auditor_assignment_sequence","queue_id", "level_id", "comments", "reason_id"],
                                                           batch_size=10000, default_change_reason="work_queue")

当待更新数据量达数千行时,数据库会进入lock:relation状态,持续2-3分钟无法正常操作。

查看simple_history包中bulk_update_with_history的文档:

def bulk_update_with_history(
    objs,
    model,
    fields,
    batch_size=None,
    default_user=None,
    default_change_reason=None,
    default_date=None,
    manager=None,
):
    """
    Bulk update the objects specified by objs while also bulk creating
    their history (all in one transaction).
    :param objs: List of objs of type model to be updated
    :param model: Model class that should be updated
    :param fields: The fields that are updated
    :param batch_size: Number of objects that should be updated in each batch

从文档能看出,该方法会把所有批次的更新包裹在单个事务中,这应该是导致长时间数据库锁的核心原因。

我现在需要了解两个点:

  1. Django原生bulk_update的batch_size参数工作机制
  2. 针对“分块调用bulk_update_with_history拆分事务缓解锁问题”的具体建议与思路

一、Django bulk_update的batch_size工作机制

  • batch_size用来指定单次数据库请求处理的对象数量,比如设置为1000,10000条数据会被拆成10次数据库请求执行更新
  • 注意:原生bulk_update不会自动拆分事务——如果没有手动控制事务边界,所有批次的更新仍会在同一个事务中完成
  • 每个批次生成一条批量UPDATE语句,能减少单条更新的网络和连接开销,但事务层面还是整体绑定的

二、分块调用bulk_update_with_history的优化思路

核心逻辑

既然bulk_update_with_history会把所有批次塞进单个事务,那手动将待更新数据拆成若干小批次,每个批次单独调用该方法,就能让每一批的更新和历史记录创建都在独立事务中完成,缩短单事务的锁持有时间,避免长时间表级锁(lock:relation通常是表级锁)。

具体实现步骤

  1. 拆分数据:把updated_chart_records分成多个子列表,建议每个子列表包含200-500条数据(具体大小根据数据库性能调整,从小值测试)
    from itertools import islice
    
    def chunked(iterable, size):
        iterator = iter(iterable)
        while chunk := list(islice(iterator, size)):
            yield chunk
    
    # 按每500条分块处理
    for chunk in chunked(updated_chart_records, 500):
        bulk_update_list = []
        for chart in chunk:
            chart.coder_assignment_sequence = count
            chart.queue_id = work_queue_pk
            chart.level_id = role_id
            bulk_update_list.append(chart)  # 原代码漏了添加对象到列表,这里补上
        # 逐块调用批量更新方法
        bulk_update_with_history(
            bulk_update_list, 
            Chart, 
            ["coder_assignment_sequence", "l1_auditor_assignment_sequence", "l2_auditor_assignment_sequence", "l3_auditor_assignment_sequence","queue_id", "level_id", "comments", "reason_id"],
            batch_size=500,  # 批次大小与分块大小保持一致即可
            default_change_reason="work_queue"
        )
    
  2. 关键注意事项
    • 事务独立性:每个分块是独立事务,某一块更新失败不会回滚之前完成的块。如果业务要求全量成功或全量失败,这个方案不适用;如果允许部分失败后手动重试,这个方案更合适
    • 分块大小调整:不要设置过大(比如还是10000),也不要过小(比如10,会增加请求次数)。建议根据数据库锁等待超时时间、业务容忍的锁时长调整,几百条一个批次比较稳妥
    • 性能平衡:分块会增加数据库请求次数,但减少了单事务锁持有时间,能提升系统并发能力,避免阻塞其他正常业务
    • 原代码修正:你原来的代码里bulk_update_list只初始化了空列表,但循环中没有把修改后的chart添加进去,会导致更新无效,上面的代码已补上append操作

其他补充优化建议

  • 缩小更新字段范围:如果本次更新没有修改fields参数中的部分字段(比如comments、reason_id),直接从参数中移除,减少数据库锁的影响范围
  • 检查锁类型根源:lock:relation如果是表级锁,可能和数据库隔离级别、更新语句的过滤条件有关。如果能通过索引定位更新行,尽量触发行级锁而非表级锁(不过bulk_update批量更新可能还是会升级表锁,分块能有效缓解)
  • 避开业务高峰:如果允许,把批量更新操作放在业务低峰期执行,降低对正常业务的影响

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 03:40:11