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
从文档能看出,该方法会把所有批次的更新包裹在单个事务中,这应该是导致长时间数据库锁的核心原因。
我现在需要了解两个点:
- Django原生
bulk_update的batch_size参数工作机制 - 针对“分块调用
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通常是表级锁)。
具体实现步骤
- 拆分数据:把
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" ) - 关键注意事项
- 事务独立性:每个分块是独立事务,某一块更新失败不会回滚之前完成的块。如果业务要求全量成功或全量失败,这个方案不适用;如果允许部分失败后手动重试,这个方案更合适
- 分块大小调整:不要设置过大(比如还是10000),也不要过小(比如10,会增加请求次数)。建议根据数据库锁等待超时时间、业务容忍的锁时长调整,几百条一个批次比较稳妥
- 性能平衡:分块会增加数据库请求次数,但减少了单事务锁持有时间,能提升系统并发能力,避免阻塞其他正常业务
- 原代码修正:你原来的代码里
bulk_update_list只初始化了空列表,但循环中没有把修改后的chart添加进去,会导致更新无效,上面的代码已补上append操作
其他补充优化建议
- 缩小更新字段范围:如果本次更新没有修改
fields参数中的部分字段(比如comments、reason_id),直接从参数中移除,减少数据库锁的影响范围 - 检查锁类型根源:
lock:relation如果是表级锁,可能和数据库隔离级别、更新语句的过滤条件有关。如果能通过索引定位更新行,尽量触发行级锁而非表级锁(不过bulk_update批量更新可能还是会升级表锁,分块能有效缓解) - 避开业务高峰:如果允许,把批量更新操作放在业务低峰期执行,降低对正常业务的影响
内容的提问来源于stack exchange,提问作者Azima
相关产品推荐
相关产品推荐

