本文主要介绍以PolarDB O引擎、ADB PG为代表的PG系数据库在某种特定事务场景下,其事务结果与Oracle、MySQL不同的现象,并分析该现象出现的原因。
一、数据初始化
Create table test(id int,money int);
insert into testtable values(1,80);
insert into testtable values(1,90);
insert into testtable values(1,100);
insert into testtable values(1,110);
insert into testtable values(1,120);
二、测试过程
1)事务A显式开启事务执行,暂不提交:
delete from testtable where id = 1;
insert into testtable values (1,100);
select * from testtable;
2)事务B自动提交执行:
delete from testtable where id = 1;
insert into testtable values(2,110);
select * from testtable;
3)由于事务A未提交导致事务B delete等锁,间隔几秒后提交事务A,之后事务B自动提交完成
三、测试结果
PS:经过验证,事务B的Delete操作无论在事务A的insert操作前后发起,对最终事务结果无影响
Oracle(Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production)
MySQL (5.7.32-log)
ADBPG(PostgreSQL 9.4.24 (Greenplum Database 6.3.0 build dev) on x86_64-unknown-linux-gnu, compiled by gcc (GCC) 6.5.1 20190307 (Alibaba 6.5.1-1 2.17), 64-bit compiled on Oct 25 2021 20:00:34)
PostgrelSQL(PostgreSQL 10.14)
PolarDB O引擎(PostgreSQL 11.9 (POLARDB Database Compatible with Oracle 11.9.18)
四、测试结论
该场景下MySQL处理逻辑和Oracle一致,PG系数据库均会出现数据冗余情况,类似Oracle业务场景做PG系替换有数据不一致风险,需要重点关注。
五、原因分析
-
分析原因前我们先介绍下事务型(TP)数据库常用的几种事务隔离级别:
-
Read Uncommitted:读未提交(脏读),一个事务可以读到另外一个事务未提交的数据,大多数关系型数据库不支持。
-
Read Committed:读已提交,一个事务可以读到其他事务已经提交的数据,大多数数据库的缺省模式。
-
Repeatable Read:可重复读,一个事务执行过程中访问的数据是一致的,也就是一个事务中多次读到的数据不会变化。
-
Serializable:序列化(串行化),事务串行化执行,避免不一致。代价很大,OLTP系统中很少使用。
-
Oracle、MySQL、PG系事务数据库目前都是读已提交(read committed)的隔离级别。
-
其次再介绍下数据库的MVCC、当前读、快照读等概念:
-
MVCC:Multi-version Concurrency Control (多版本并发控制),维持一个数据的多个版本,使得读写操作没有冲突
-
当前读:像select lock in share mode( 共享锁), select for update ; update, insert ,delete( 排他锁)这些操作都是一种当前读,为什么叫 当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
-
快照读:像 不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
基于上面的概念我们再来看Oracle/MySQL和PG系TP数据库在该事务场景下的差别时就可以作出如下的推测:
对于Oracle/MySQL结果只有事务B的新插入行:Oracle/MySQL的RC隔离级别下除了select操作都是当前读,因此当事务A对应行上X锁释放后,事务B重新获取X锁时发现行消失会重新索引扫描获取当前读状态,因而直接获取到事务A新插入的行并删除
从MySQL的官方文档我们也可以得到相应的佐证信息:
![](https://ucc.alicdn.com/d4fo45xeyzbbe_20240422_46d6952990d94c8e89159f1db9cb8177.png?x-oss-process=image/resize,w_1400/format,webp)
If you want to see the “freshest” state of the database, use either the
READ COMMITTED
isolation level or a locking read
原文如上,翻译过来就是如果始终希望获取数据的最新状态,建议使用Read Committed 的隔离级别或者使用带锁的读取操作(如select lock in share mode/delete隐式读/update隐式读等 );也就是MySQL/Oracle除了普通的select操作,其他操作时都会去获取数据的最新当前读状态
对于PG系结果同时含有事务A、B新插入的行:可能存在如下两种原因
-
Case1:PG内核逻辑可能是发现之前的行不存在时就判断事务不用执行直接提交了,不会去走重新索引扫描获取当前读状态,因此事务A新插入的那条数据会依旧存在,出现冗余。
-
Case2:PG内核在事务B发现行消失时也会重新索引扫描,但是PG系内核对于Delete的查询操作也使用了快照读,因此并不会读取到事务A 新插入的这行数据,因而事务B并不会删除该行数据,出现冗余
那么确切的原因什么,我们可以从PG的官方文档找到答案,如下图:
![](https://ucc.alicdn.com/d4fo45xeyzbbe_20240422_66260fff343f474d928dbfe0c7a70d37.png?x-oss-process=image/resize,w_1400/format,webp)
If the first updater commits, the second updater will ignore the row if the first updater deleted it, otherwise it will attempt to apply its operation to the updated version of the row.
前述这段英文是其中最关键的信息,简而言之就是如果事务A Delete了对应行,事务B提交时会直接忽略对应行,而不是重新获取当前读状态;而如果事务A Update了对应行,则此时事务B才会去重新获取当前读状态并基于该状态继续做更新操作。即我们推断的Case1是符合真实情况的
综上分析,PG系和Oracle/MySQL内核在Delete锁冲突时一点细微的处理逻辑差异,导致了在特定的事务场景下出现了数据不一致的情况。
六、参考文档
PostgreSQL官方文档:Read Committed Isolation Level
MySQL官方文档:Consistent Nonlocking Reads
一文读懂PostgreSQL事务隔离级别:链接
正确的理解MySQL的MVCC及实现原理:链接