PostgreSQL 并发更新冲突与解决方案

在使用 PostgreSQL 时,遇到 ERROR: could not serialize access due to concurrent update 错误是并发更新场景中常见的问题。这篇文章将详细介绍该错误的原因及其解决方案,帮助开发者更好地应对高并发环境下的事务冲突。

错误背景

以下是一个典型的报错场景:

UPDATE "ir_config_parameter" 
SET "write_uid"=2776, "value"='5', "write_date"=(now() at time zone 'UTC') 
WHERE id IN (278);

报错信息:

ERROR: could not serialize access due to concurrent update

错误原因

该错误通常发生在 PostgreSQL 的高隔离级别(SERIALIZABLEREPEATABLE READ)下。当多个事务试图同时读取并更新同一行数据时,数据库检测到无法满足隔离级别的要求,就会中止其中一个事务以避免潜在的数据不一致。

可能的原因包括:

  1. 事务隔离级别过高SERIALIZABLE 模式会确保事务以完全隔离的方式执行,这在高并发场景下容易导致冲突。
  2. 并发更新相同数据:两个事务试图同时更新表 ir_config_parameterid=278 的记录。

解决方案

1. 降低事务隔离级别

如果当前事务隔离级别是 SERIALIZABLEREPEATABLE READ,可以改为 READ COMMITTED,从而降低冲突的概率。

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • 优点:降低隔离级别后,事务允许读取其他事务提交的数据,减少了锁冲突。
  • 缺点:可能会引入不可重复读的问题。

2. 使用乐观锁

通过检查数据版本号或时间戳,确保更新时数据未被其他事务修改。如果数据已更改,则当前事务不会更新。

示例:
UPDATE "ir_config_parameter" 
SET "write_uid"=2776, "value"='5', "write_date"=(now() at time zone 'UTC') 
WHERE id = 278 AND write_date = 'previous_write_date';
  • 优点:确保更新仅在数据未被其他事务更改时进行。
  • 缺点:需要额外的字段存储版本号或时间戳。

3. 实现重试机制

如果事务被中止,可以通过捕获异常并重试来保证更新成功。

示例代码(Python):
import psycopg2
from psycopg2.errors import SerializationFailure

def update_config():
    try:
        # 开始事务
        cursor.execute("""
            UPDATE "ir_config_parameter" 
            SET "write_uid"=2776, "value"='5', "write_date"=(now() at time zone 'UTC') 
            WHERE id = 278;
        """)
        connection.commit()
    except SerializationFailure:
        # 捕获并重试
        connection.rollback()
        update_config()
  • 优点:通过编程实现自动化处理,用户无感知。
  • 缺点:可能会增加事务完成时间。

4. 行级锁定

在事务中对需要更新的行提前加锁,避免其他事务对这些行进行操作。

示例:
BEGIN;
SELECT * FROM "ir_config_parameter" WHERE id = 278 FOR UPDATE;
UPDATE "ir_config_parameter" 
SET "write_uid"=2776, "value"='5', "write_date"=(now() at time zone 'UTC') 
WHERE id = 278;
COMMIT;
  • 优点:明确锁定目标行,避免并发冲突。
  • 缺点:可能会引发死锁,需要小心设计事务顺序。

5. 优化业务逻辑

分析业务逻辑,减少对同一行数据的频繁并发写入。

  • 避免不必要的更新:确保只有必要时才执行更新操作。
  • 分区数据更新:将数据分散到不同的逻辑分区,减少单行竞争。

示例分析

错误场景

假设表 ir_config_parameter 中有以下数据:

id

value

write_uid

write_date

278

3

123

2023-12-31 12:00:00

两个事务试图同时更新 id=278 的记录:

事务 A
BEGIN;
UPDATE "ir_config_parameter" 
SET "value" = '4', "write_date" = (now() at time zone 'UTC') 
WHERE id = 278;
COMMIT;
事务 B
BEGIN;
UPDATE "ir_config_parameter" 
SET "value" = '5', "write_date" = (now() at time zone 'UTC') 
WHERE id = 278;
COMMIT;

如果事务 A 和事务 B 同时执行,PostgreSQL 将中止其中一个事务(如事务 B)。

解决方法

通过重试机制,事务 B 可以在事务 A 提交后重新执行。

总结

当遇到 could not serialize access due to concurrent update 错误时,需要根据具体场景选择合适的解决方案:

  • 降低事务隔离级别以减少冲突概率。
  • 使用乐观锁确保更新安全。
  • 实现重试逻辑以提升用户体验。
  • 使用行级锁确保数据一致性。
  • 优化业务逻辑以减少并发操作。

合理的事务设计和并发控制策略可以有效提升 PostgreSQL 数据库的性能和可靠性。

上一篇:【C++】智能指针详解-二、为什么要用智能指针


下一篇:electron 主进程发生javascript错误