Pg批量更新数据产生的死锁问题

本文描述了在SpringKafka中两个并发执行的批量更新操作导致PostgreSQL死锁的问题,分析了死锁产生的原因,涉及事务、行锁和索引的影响,以及提出了解决方案——确保按相同字段顺序更新以避免死锁。

两个服务同时批量更新相同Pg表的相同记录时,产生如下错误:

[2024-01-18 09:17:52,172  host: 173.16.34.10  ERROR  alarm  [org.springframework.kafka.KafkaListenerEndpointContainer#3-0-C-1]  com.zhongbao.alarm.service.impl.AlarmServiceImpl  - <ZG-COMMON-JS-ALARM>批量更新失败:]
org.springframework.dao.DeadlockLoserDataAccessException:
### Error updating database.  Cause: org.postgresql.util.PSQLException: ERROR: deadlock detected
  Detail: Process 2345 waits for ShareLock on transaction 177149283; blocked by process 30377.
Process 30377 waits for ShareLock on transaction 177149284; blocked by process 2345.
  Hint: See server log for query details.
  Where: while updating tuple (2672,3) in relation "alarm_data_20240117"
### The error may exist in class path resource [mapper/AlarmDataMapper.xml]
### The error may involve com.zhongbao.alarm.mapper.AlarmDataMapper.updateAlarmBatch-Inline
### The error occurred while setting parameters

死锁的产生原因

两个更新事务(如批量update)在已获取锁(行锁)的情况下,都尝试去获取对方的锁(行锁),会互相等待,形成死锁 !死锁的详细信息,出现在服务器日志中。

推测:
1、基于update …的批量更新,在提交给Pg后,Pg自动生成批量更新的事务;
2、更新事务欲修改行数据时,会获取锁来锁定该行,直到事务执行完毕后释放所有的行锁(Pg会自动为更新添加行锁);
3、多个更新事务因修改记录相互等待对方的释放锁时,即发生死锁;
4、死锁后,直到其中一个事务超期结束,其锁被释放,死锁才打开;

查看Pg所有锁: SELECT * FROM pg_locks;
查看Pg的行锁: CREATE EXTENSION pgrowlocks; SELECT * FROM pgrowlocks('表名');

死锁的解决办法

The correct way to perform such operations is to lock resources in the same order.
For example: in this case, accounts(表) can be locked in ascending order of their numbers.即批量更改表数据,需按照相同字段的相同排序去批量修改,保证修改顺序即可。

   /// 顺序更新数据,以防止死锁
   oldAlarmDataList.sort(new Comparator<AlarmData>() {
       @Override
       public int compare(AlarmData o1, AlarmData o2) {
           return o1.getSid().compareTo(o2.getSid());
       }
   });
   startTime = System.currentTimeMillis();
   this.alarmAllMapper.updateAlarmAllBatch(oldAlarmDataList);
   log.info("更新消息数量:{}, 耗时:{}",
           oldAlarmDataList.size(), System.currentTimeMillis() - startTime);

死锁模拟(作者没模拟出来,MD…)

=> create table accounts (
	acc_no  integer,
	amount  integer
);
=> insert into accounts values(0,0),(1,1),(2,2),(3,3),(4,4);
=> create index on accounts(amount desc);
/// 自增函数,延迟1秒后加100
=> CREATE FUNCTION inc_slow(n numeric) RETURNS numeric AS $$
  SELECT pg_sleep(1);
  SELECT n + 100.00;
$$ LANGUAGE SQL;
/// 安装扩展函数
=> CREATE EXTENSION pgrowlocks;
///// 第一会话 顺序修改表记录
/// Since tuples on the table page are located in ascending order of the amount (exactly how we added them),
/// they will also be updated in the same order. 
/// explain命令输出结果中的"Seq Scan"表示顺序扫描,即全表扫描。
=> EXPLAIN (costs off) UPDATE accounts SET amount = inc_slow(amount);

|           QUERY PLAN         
|  ----------------------------
|   Update on accounts
|     ->  Seq Scan on accounts
|  (2 rows)
///// 第二会话 逆序修改表记录
/// 禁止顺序扫描
=> SET enable_seqscan = off;
/// 注意:这里的100如果写成100.00会因数据类型不一致导致索引失效 !!!
/// explain命令输出结果中的"Index Scan"表示索引扫描。
=> EXPLAIN (costs off)
UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100; 

|                         QUERY PLAN                       
|  --------------------------------------------------------
|   Update on accounts
|     ->  Index Scan using accounts_amount_idx on accounts
|           Index Cond: (amount > 100.00)
|  (3 rows)
/// 查看sql线程(pid)对记录的加锁情况
=> SELECT * FROM pgrowlocks('accounts');

当两个sql线程同时运行,其更新的记录"碰头"后,会产生死锁 !

其他

特别注意: 在执行"SET enable_seqscan = off;“之前,虽然已添加索引,但执行"UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100;”
时候,Pg依然进行的全表扫描!因此推测:对应较简单的查询,即使建立并使用索引过滤,Pg依然会进行全表扫描。

参考文档
https://blog.youkuaiyun.com/pg_hgdb/article/details/100653715

<think>嗯,用户需要排查PostgreSQL批量处理数据速度慢的问题。这个问题很实际,批量处理性能优化在数据库运维中很常见。用户可能是个开发或DBA,正在处理数据迁移或ETL任务时遇到了瓶颈。 从引用内容看,用户之前接触过Python批量插入脚本[^1]和锁问题排查[^2][^3],说明用户有一定技术基础。不过这次聚焦在速度而非死锁,需要更全面的性能分析视角。 首先想到几个关键排查方向:硬件资源、SQL执行效率、事务管理、批量操作方式。硬件是基础,但用户可能更关注数据库层面的优化。特别要注意的是,引用[1]提到用copy_from做批量插入,这比逐条INSERT快得多,如果用户没采用这种方式可能就是症结所在。 具体排查步骤应该由浅入深:先看系统负载排除硬件瓶颈,再分析慢查询。索引缺失是常见问题,比如批量更新时全表扫描。引用[3]提到大数据量时排查困难,所以需要教用户精准定位慢查询的方法。 事务管理很关键,大事务会导致WAL膨胀和锁竞争。引用[2]的锁排查指南在这里也适用,长时间持有锁会阻塞其他操作。建议用户拆分为小事务,这点在引用[3]的死锁分析中也有印证。 最后要检查批量操作方式。如果用户用循环执行单条INSERT,速度会比COPY慢百倍。引用[1]的Python脚本示例很好,应该重点推荐copy_from方法。表结构优化如调整fillfactor也能提升更新速度。 生成的问题要覆盖不同场景:有人可能想深入EXPLAIN分析,有人需要参数调优指导,还有应用场景的疑问。所以设计了从执行计划到vacuum优化的递进问题链。</think>排查 PostgreSQL 批量处理数据速度慢的问题,需要系统性地检查多个环节。以下是关键排查步骤和优化方向: --- ### **一、核心排查步骤** 1. **定位瓶颈源头** - **监控系统资源**:使用 `top`、`htop` 或 `vmstat` 检查 CPU、内存、I/O 使用率。I/O 瓶颈(如磁盘 100% 利用率)是常见拖慢批量操作的主因。 - **检查 PostgreSQL 活动**: ```sql SELECT * FROM pg_stat_activity WHERE state = 'active'; -- 查看当前运行中的查询 SELECT query, total_time FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10; -- 需启用 pg_stat_statements 扩展 ``` 2. **分析慢查询** - 对批量操作中的关键 SQL 使用 `EXPLAIN (ANALYZE, BUFFERS)`: ```sql EXPLAIN (ANALYZE, BUFFERS) INSERT INTO target_table SELECT * FROM source_table; ``` - 关注:`Seq Scan`(全表扫描)、`Sort`(排序开销)、`Buffers: shared hit/read`(缓存命中率低需优化)。 3. **检查锁竞争** 批量操作可能阻塞于锁(如 `AccessExclusiveLock`): ```sql SELECT locktype, relation::regclass, mode, granted FROM pg_locks WHERE relation = 'your_table'::regclass; -- 替换 your_table ``` 结合引用[2][3]:长事务或未提交事务会持有锁,导致后续操作排队[^2][^3]。 4. **评估事务和日志** - **大事务问题**:单次提交过多数据会填满 WAL 日志,增加恢复点开销。 - **检查 WAL 配置**: ```sql SHOW wal_level; SHOW checkpoint_timeout; SHOW max_wal_size; ``` 过小的 `max_wal_size` 或频繁的检查点(checkpoint)会拖慢写入。 --- ### **二、关键优化方法** 1. **提升批量写入效率** - **使用 `COPY` 代替 `INSERT`**: ```python # Python 示例(引用[1]的优化) with open('data.csv', 'r') as f: cursor.copy_expert("COPY table_name FROM STDIN WITH (FORMAT CSV)", f) ``` `COPY` 比单条 `INSERT` 快 **10-100 倍**[^1]。 - **调整事务提交**: ```sql BEGIN; -- 批量操作 INSERT ...; -- 多次操作 COMMIT; -- 集中提交 ``` 避免逐条提交,但单事务数据量不宜过大(建议每批 10k-100k 行)。 2. **索引与表优化** - **暂停/重建索引**: ```sql DROP INDEX CONCURRENTLY idx_name; -- 删除索引 -- 执行批量插入 CREATE INDEX CONCURRENTLY idx_name ON table_name(column); -- 重建 ``` - **调整表参数**: ```sql ALTER TABLE table_name SET (fillfactor = 70); -- 提高更新效率 ``` 3. **配置调优** - **关键参数**(修改 `postgresql.conf`): ```ini shared_buffers = 25% RAM -- 通常 10-25% 总内存 work_mem = 64MB -- 每个排序/哈希操作可用内存 max_parallel_workers = 8 -- 并行进程数 maintenance_work_mem = 1GB -- 用于 VACUUM 等操作 wal_buffers = 16MB -- 增加 WAL 缓冲区 ``` - **并行查询**:确保查询计划中出现 `Parallel Seq Scan`。 4. **避免锁冲突** - **缩短事务时间**:拆分大批量操作为多个小事务。 - **监控长事务**: ```sql SELECT pid, now() - xact_start AS duration, query FROM pg_stat_activity WHERE state = 'active' AND now() - xact_start > interval '5 minutes'; ``` 及时终止阻塞操作(引用[2][3])[^2][^3]。 --- ### **三、高级工具** - **`pg_stat_statements`**:分析历史查询性能。 - **`pgBadger`**:解析日志生成性能报告。 - **`auto_explain`**:自动记录慢查询执行计划。 > **典型优化案例**:某用户通过 `COPY` 替换循环 `INSERT`,同时暂停索引,使 100 万行插入从 **120 秒**降至 **8 秒**。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值