PostgreSQL并发删除插入同一条记录时的奇怪现象及分析

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
简介: 用sysbench 0.4.12对PostgreSQL进行压测时遇到了主键约束违反的错误。然后发现原因在于PostgreSQL在并发执行依次更新删除插入同一条记录的事务时(比如就像下面的事务),就可能会报主键约束违反的错误。
用sysbench 0.4.12对PostgreSQL进行压测时遇到了主键约束违反的错误
然后发现原因在于PostgreSQL在并发执行依次更新删除插入同一条记录的事务时(比如就像下面的事务),就可能会报主键约束违反的错误。
  1. begin;
  2. update tb1 set c=2 where id=1;
  3. delete from tb1 where id=1;
  4. insert into tb1 values(1,2);
  5. commit;

完整的再现方法是这样的:
1. 建表
  1. create table tb1(id int primary key,c int);
  2. insert into tb1 values(1,1);

2. 准备SQL脚本
test.sql:
  1. begin;
  2. update tb1 set c=2 where id=1
  3. delete from tb1 where id=1;
  4. insert into tb1 values(1,2);
  5. commit;

3. 执行测试

  1. [postgres(at)localhost ~]$ pgbench -n -f test.sql -c 2 -j 2 -t 2
  2. client 1 aborted in state 3: ERROR: duplicate key value violates unique
  3. constraint "tb1_pkey"
  4. DETAIL: Key (id)=(1) already exists.
  5. transaction type: Custom query
  6. scaling factor: 1
  7. query mode: simple
  8. number of clients: 2
  9. number of threads: 2
  10. number of transactions per client: 2
  11. number of transactions actually processed: 2/4
  12. latency average: 0.000 ms
  13. tps = 130.047467 (including connections establishing)
  14. tps = 225.060485 (excluding connections establishing)

4. 查看日志
事先已经配置PostgreSQL打印所有SQL

点击(此处)折叠或打开

  1. [postgres(at)localhost ~]$ cat pg95data/pg_log/postgresql-2015-10-25_141648.log
  2. 2015-10-25 14:16:48.144 EDT 57177 0 LOG: database system was shut down at 2015-10-25 14:16:47 EDT
  3. 2015-10-25 14:16:48.146 EDT 57177 0 LOG: MultiXact member wraparound protections are now enabled
  4. 2015-10-25 14:16:48.149 EDT 57175 0 LOG: database system is ready to accept connections
  5. 2015-10-25 14:16:48.150 EDT 57181 0 LOG: autovacuum launcher started
  6. 2015-10-25 14:16:57.960 EDT 57184 0 LOG: connection received: host=[local]
  7. 2015-10-25 14:16:57.961 EDT 57184 0 LOG: connection authorized: user=postgres database=postgres
  8. 2015-10-25 14:16:57.971 EDT 57186 0 LOG: connection received: host=[local]
  9. 2015-10-25 14:16:57.971 EDT 57187 0 LOG: connection received: host=[local]
  10. 2015-10-25 14:16:57.972 EDT 57186 0 LOG: connection authorized: user=postgres database=postgres
  11. 2015-10-25 14:16:57.972 EDT 57187 0 LOG: connection authorized: user=postgres database=postgres
  12. 2015-10-25 14:16:57.975 EDT 57186 0 LOG: statement: begin;
  13. 2015-10-25 14:16:57.975 EDT 57186 0 LOG: statement: update tb1 set c=2 where id=1
  14. 2015-10-25 14:16:57.975 EDT 57187 0 LOG: statement: begin;
  15. 2015-10-25 14:16:57.976 EDT 57187 0 LOG: statement: update tb1 set c=2 where id=1
  16. 2015-10-25 14:16:57.978 EDT 57186 39682 LOG: statement: delete from tb1 where id=1;
  17. 2015-10-25 14:16:57.979 EDT 57186 39682 LOG: statement: insert into tb1 values(1,2);
  18. 2015-10-25 14:16:57.979 EDT 57186 39682 LOG: statement: commit;
  19. 2015-10-25 14:16:57.980 EDT 57186 0 LOG: statement: begin;
  20. 2015-10-25 14:16:57.981 EDT 57186 0 LOG: statement: update tb1 set c=2 where id=1
  21. 2015-10-25 14:16:57.981 EDT 57187 39683 LOG: statement: delete from tb1 where id=1;
  22. 2015-10-25 14:16:57.981 EDT 57186 39684 LOG: statement: delete from tb1 where id=1;
  23. 2015-10-25 14:16:57.981 EDT 57186 39684 LOG: statement: insert into tb1 values(1,2);
  24. 2015-10-25 14:16:57.981 EDT 57186 39684 LOG: statement: commit;
  25. 2015-10-25 14:16:57.983 EDT 57187 39683 LOG: statement: insert into tb1 values(1,2);
  26. 2015-10-25 14:16:57.983 EDT 57187 39683 ERROR: duplicate key value violates unique constraint "tb1_pkey"
  27. 2015-10-25 14:16:57.983 EDT 57187 39683 DETAIL: Key (id)=(1) already exists.
  28. 2015-10-25 14:16:57.983 EDT 57187 39683 STATEMENT: insert into tb1 values(1,2);

分析这段日志,发现和我的认识不符,我一直认为事务里的第一条UPDATE会获得一个行锁,没有得到锁的事务会等到得到锁的事务提交后把锁释放,这样的话之后的操作就变成了串行操作,不会出现冲突。

于是,我把这个问题作为BUG提交到社区的Bug邮件列表。
http://www.postgresql.org/message-id/20151025110136.3017.39398@wrigleys.postgresql.org

结果社区不认为这是Bug,而与PG实现MVCC的机制有关。并且手册中确实也有说明。虽然UPDATE仍然是阻塞的,在持有行锁的那个事务提交后 读已提交隔离级别下,被解除阻塞的事务会再次进行更新操作。但是这次更新操作可能会看到不一致的数据快照。
From http://www.postgresql.org/docs/current/static/transaction-iso.html > Because of the above rule, it is possible for an updating command to see an
> inconsistent snapshot: it can see the effects of concurrent updating
> commands on the same rows it is trying to update, but it does not see
> effects of those commands on other rows in the database. This behavior
> makes Read Committed mode unsuitable for commands that involve complex
> search conditions;

然而,不光是UPDATE,SELECT ... FOR UPDATE也可能看到不一致的快照,实验如下:

1. SQL脚本
test10.sql:

点击(此处)折叠或打开

  1. begin;
  2. select * from tb1 where id=1 for update;
    update tb1 set c=2 where id=1;
    delete from tb1 where id=1;
    insert into tb1 values(1,2);
    commit;
    begin;
    select * from tb1 where id=1 for update;
    update tb1 set c=2 where id=1;
    delete from tb1 where id=1;
    insert into tb1 values(1,2);
    commit;
    ...

以上内容重复多次

2. 执行测试

  1. [postgres@localhost ~]$ psql -f test10.sql >b1.log 2>&1 &
  2. [postgres@localhost ~]$ psql -f test10.sql >b2.log 2>&1 &

3. 查看日志
b1.log:

点击(此处)折叠或打开

  1. ...
  2. BEGIN
  3. id | c
  4. ----+---
  5. (0 rows)
  6. UPDATE 0
  7. DELETE 0
  8. psql:test10.sql:29: ERROR: duplicate key value violates unique constraint "tb1_pkey"
  9. DETAIL: Key (id)=(1) already exists.
  10. ROLLBACK
  11. ...

从日志可以看出,“ select * from tb1 where id=1 for update”看到了一个不一致的状态。这不就是“脏读”吗!

解释
那么,怎么解释这个事情?
PostgreSQL的处理逻辑是这样的(手册也有说明):
两个事务并发 更新同一条记录时会导致一个事务被锁住,持有锁的事务提交后,被解除阻塞的事务的隔离级别如果是“读已提交”则对更新对象行再次进行where条件评估,如果仍然满足原来的where条件这执行更新否则不更新。

需要注意的是,where条件的再评估是针对初始检索筛选出的行而不是对整个表重新执行检索,所以如果这期间有insert过来的新行也满足where条件,或者某个被更新的行从原来不满足where条件变成了满足where条件,是不会被处理的。
另外,被insert的行总被认为是新行,哪怕它的主键和之前刚刚删除的一行相同(我之前没有意识到这一点,所以老在纠结)。

关于这个问题的详细解释,如下
参考PG的MVCC实现原理,逻辑上的行由1个或多个行版本(tuple)构成,这些tuple通过内部的t_ctid指向最新版本的tuple。像下面这样.
开始时,逻辑行上只有1个tuple,它的t_ctid指向自己(0,1) 。

点击(此处)折叠或打开

  1. postgres=# begin;
  2. BEGIN
  3. postgres=# select * from tb1;
  4.  id | c
  5. ----+---
  6.   1 | 2
  7. (1 row)

  8. postgres=# SELECT * FROM heap_page_items(get_raw_page('tb1', 0));
  9.  lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid
  10. ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
  11.   1 | 8160 | 1 | 32 | 148302 | 0 | 0 | (0,1) | 2 | 11008 | 24 | |
  12. (1 row)

UPDATE后,出现了2个tuple, 第2个tuple是新版, 所以这两个tuple的 t_ctid都指向(0,2 ) 。

点击(此处)折叠或打开

  1. postgres=# update tb1 set c=2 where id=1;
  2. UPDATE 1
  3. postgres=# SELECT * FROM heap_page_items(get_raw_page('tb1', 0));
  4.  lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid
  5. ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
  6.   1 | 8160 | 1 | 32 | 148302 | 148304 | 0 | (0,2) | 16386 | 8960 | 24 | |
  7.   2 | 8128 | 1 | 32 | 148304 | 0 | 0 | (0,2) | 32770 | 10240 | 24 | |
  8. (2 rows)

DELETE后,最新的tuple的t_xmax被标记上了删除它的事务的事务ID。

点击(此处)折叠或打开

  1. postgres=# delete from tb1 where id=1;
  2. DELETE 1
  3. postgres=# SELECT * FROM heap_page_items(get_raw_page('tb1', 0));
  4.  lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid
  5. ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
  6.   1 | 8160 | 1 | 32 | 148302 | 148304 | 0 | (0,2) | 16386 | 8960 | 24 | |
  7.   2 | 8128 | 1 | 32 | 148304 | 148304 | 0 | (0,2) | 40962 | 8224 | 24 | |
  8. (2 rows)

到目前为止,UPDATE和DELETE都作用的tuple都可以用t_ctid串起来,我们可以姑且称之为“tuple链”。但是INSERT操作是不一样的,它开始了一个新的tuple链。

点击(此处)折叠或打开

  1. postgres=# insert into tb1 values(1,2);
  2. INSERT 0 1
  3. postgres=# SELECT * FROM heap_page_items(get_raw_page('tb1', 0));
  4.  lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid
  5. ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
  6.   1 | 8160 | 1 | 32 | 148302 | 148304 | 0 | (0,2) | 16386 | 8960 | 24 | |
  7.   2 | 8128 | 1 | 32 | 148304 | 148304 | 0 | (0,2) | 40962 | 8224 | 24 | |
  8.   3 | 8096 | 1 | 32 | 148304 | 0 | 2 | (0,3) | 2 | 2048 | 24 | |
  9. (3 rows)

在我们的例子中,由于并发更新而被阻塞的UPDATE操作在阻塞解除后,根据它操作对象的tuple(第一个tuple)的t_ctid找到这个tuple链上最新的tuple,即第2个tuple(它没有在整个表上再次执行检索,所以他没有发现第3个tuple)。由于第2个tuple已经被删除了,所以UPDATE的影响行数是0。这个事务中后面的DELETE遇到了和UPDATE同样的问题,开始时它没有抢过其他并发事务,等其他并发事务执行完了,它同样没有看到新插入的行,所以DELETE的影响行数也是0。因为这个原因,执行INSERT操作时,表中已经有了相同key的记录了,作为报主键约束违反的错误。
关于PostgreSQL MVCC的原理,可以参考    http://blog.chinaunix.net/uid-20726500-id-4040024.html

分析到这里,我们可以知道,即使没有第一个update,就像下面这样,问题也能再现。

点击(此处)折叠或打开

  1. [postgres@localhost ~]$ cat test0.sql
  2. begin;
  3. delete from tb1 where id=1;
  4. insert into tb1 values(1,2);
  5. commit;

  6. [postgres@localhost ~]$ pgbench -n -f test0.sql -c 2 -j 2 -t 1
  7. client 0 aborted in state 2: ERROR: duplicate key value violates unique constraint "tb1_pkey"
  8. DETAIL: Key (id)=(1) already exists.
  9. transaction type: Custom query
  10. scaling factor: 1
  11. query mode: simple
  12. number of clients: 2
  13. number of threads: 2
  14. number of transactions per client: 1
  15. number of transactions actually processed: 1/2
  16. latency average: 0.000 ms
  17. tps = 94.393053 (including connections establishing)
  18. tps = 223.788743 (excluding connections establishing)
所以关键在于,在一个读已提交事务中,delete + insert同一个key就可能出现问题。

如果没有主键会发生什么?
如果没有主键,你会发现事情会变得更糟。
先把主键去掉

点击(此处)折叠或打开

  1. drop table tb1;
  2. create table tb1(id int,c int);
  3. insert into tb1 values(1,1);
再次测试,没有报错。

点击(此处)折叠或打开

  1. [postgres@localhost ~]$ pgbench -n -f test0.sql -c 2 -j 2 -t 1
  2. transaction type: Custom query
  3. scaling factor: 1
  4. query mode: simple
  5. number of clients: 2
  6. number of threads: 2
  7. number of transactions per client: 1
  8. number of transactions actually processed: 2/2
  9. latency average: 0.000 ms
  10. tps = 169.047418 (including connections establishing)
  11. tps = 338.094836 (excluding connections establishing)
你会发现插入了2条记录

点击(此处)折叠或打开

  1. postgres=# select * from tb1;
  2.  id | c
  3. ----+---
  4.   1 | 2
  5.   1 | 2
  6. (2 rows)

最后
这个问题的危害还是有限的。
1,首先如果持有锁的事务回滚了,不会出现任何问题,它看到状态还是来自一个已提交的事务(只不过这个状态不是最终状态),所以不能算是“脏读”。
2 其次只有更新操作(包括select ... for update)可能会看到不一致状态,只读操作不会。
3, 在同一个事务中,先后删除再插入同一个key也没有什么意义,也就测试程序可能会这么干(如果你的应用也是这么写的,请改掉)。

如果非要在一个事务中 删除再插入同一个key(或者遇到其它的更新操作会看到不一致状态的场景),可以把隔离级别调高到可重复读或可串行化。但是调高以后,你会发现错误消息变成了“并发冲突”。但这个变化是有意义的, “并发冲突 ”代表一个可以重试的错误,应用捕获到这个错误后可以尝试再次执行,而“主键冲突”的错误没有这层含义。那么,看上去这个问题仅仅成了一个错误消息不当的问题了(实际上当然不是这么简单)。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
3月前
|
关系型数据库 MySQL Serverless
高顿教育:大数据抽数分析业务引入polardb mysql serverless
高顿教育通过使用polardb serverless形态进行数据汇总,然后统一进行数据同步到数仓,业务有明显高低峰期,灵活的弹性伸缩能力,大大降低了客户使用成本。
|
4月前
|
关系型数据库 BI 分布式数据库
PolarDB NL2BI解决方案,让你不懂SQL也能进行数据查询分析并生成BI报表
无需创建和开通资源,在预置环境中免费体验PolarDB MySQL及其NL2BI解决方案
PolarDB NL2BI解决方案,让你不懂SQL也能进行数据查询分析并生成BI报表
|
7月前
|
关系型数据库 物联网 PostgreSQL
沉浸式学习PostgreSQL|PolarDB 11: 物联网(IoT)、监控系统、应用日志、用户行为记录等场景 - 时序数据高吞吐存取分析
物联网场景, 通常有大量的传感器(例如水质监控、气象监测、新能源汽车上的大量传感器)不断探测最新数据并上报到数据库. 监控系统, 通常也会有采集程序不断的读取被监控指标(例如CPU、网络数据包转发、磁盘的IOPS和BW占用情况、内存的使用率等等), 同时将监控数据上报到数据库. 应用日志、用户行为日志, 也就有同样的特征, 不断产生并上报到数据库. 以上数据具有时序特征, 对数据库的关键能力要求如下: 数据高速写入 高速按时间区间读取和分析, 目的是发现异常, 分析规律. 尽量节省存储空间
600 1
|
1月前
|
存储 关系型数据库 MySQL
TiDB与MySQL、PostgreSQL等数据库的比较分析
【2月更文挑战第25天】本文将对TiDB、MySQL和PostgreSQL等数据库进行详细的比较分析,探讨它们各自的优势和劣势。TiDB作为一款分布式关系型数据库,在扩展性、并发性能等方面表现突出;MySQL以其易用性和成熟性受到广泛应用;PostgreSQL则在数据完整性、扩展性等方面具有优势。通过对比这些数据库的特点和适用场景,帮助企业更好地选择适合自己业务需求的数据库系统。
|
6月前
|
关系型数据库 定位技术 分布式数据库
沉浸式学习PostgreSQL|PolarDB 18: 通过GIS轨迹相似伴随|时态分析|轨迹驻点识别等技术对拐卖、诱骗场景进行侦查
本文主要教大家怎么用好数据库, 而不是怎么运维管理数据库、怎么开发数据库内核.
1066 1
|
2月前
|
关系型数据库 分布式数据库 PolarDB
电子书阅读分享《PolarDB开发者大会:PolarDB在线数据实时分析加速》
电子书阅读分享《PolarDB开发者大会:PolarDB在线数据实时分析加速》
85 3
|
2月前
|
关系型数据库 分布式数据库 PolarDB
电子书阅读分享《PolarDB开发者大会:PolarDB在线数据实时分析加速》
电子书阅读分享《PolarDB开发者大会:PolarDB在线数据实时分析加速》
76 1
|
2月前
|
关系型数据库 分布式数据库 PolarDB
电子书阅读分享《PolarDB开发者大会:PolarDB在线数据实时分析加速》
电子书阅读分享《PolarDB开发者大会:PolarDB在线数据实时分析加速》
87 1
|
3月前
|
存储 关系型数据库 分布式数据库
阿里云PolarDB解决乐麦多源数据存储性能问题
乐麦通过使用PolarDB数据库,使整个系统之间的数据查询分析更加高效
390 3
|
3月前
|
存储 关系型数据库 MySQL
美柚:消息2.0引入PolarDB-M支撑大表并发和存储
美柚旗下的移动互联网软件包括美柚、宝宝记、柚子街等丰富的产品矩阵,为广大女性用户提供全面的健康管理、知识科普、线上购物、互联网医疗等服务。