Oracle 事务

文章详细对比了Oracle和PostgreSQL在事务处理上的差异,特别是多版本控制(MVCC)的实现方式。Oracle使用独立的undo段存储历史版本,而PostgreSQL在每个tuple上携带事务信息。文章通过实例分析了事务的可见性判断,探讨了两种数据库在并发读写和版本清理上的优缺点。
摘要由CSDN通过智能技术生成

Oracle 事务(对比PG)

Oracle没有开源,只有一些paper的设计思路。Gaussdb v6 技术选型。
下面是设计伴随流程梳理

设计:

Postgresql 为了提高读写并发,采用mvcc,也就是多版本控制。即每个tuple都有一个版本,对应到一个生存时间。事务只能看到在他开始之前的版本信息,也就是只能看到在其生成之前生成的tuple,这个信息存储在每个tuple的xmin和xmax字段,xmin代表这个tuple被生成的“时间”,xmax代表这个tuple被删除的“时间”,所以一个事务判断当前这个tuple是否可见,只用判断自己是否在xmin和xmax中间,如果是则可见,否则不可见。当然,这个“时间(xmin和xmax)”本质是事务的唯一标识id号,也就是xid,这是因为事务(xid)本身在全局上有明确的先后顺序,由全局的事务管理器统一分配。这样设计的缺点是:1. 首先page上的每个tuple都携带事务信息(xmin,xmax)占用空间;2. 其次各个tuple的新老版本都按照产生的顺序存在一个page上的,也就是说一个page上存在多个tuple的多个版本,每个tuple的新旧版本通过自身维护的链串起来,这为清理造成困难,比如要清理一个时间点之前的tuple,就需要根据新旧版本链一条一条在page上查找并删除,pg为此增加vacuum进程后台清理。优点是访问各个版本的数据会快,因为大概率各个版本都存在一个page上(版本多了会跨页),查找方便。更多设计参考 PostgreSQL 事务上PostgreSQL 事务下

MVCC的核心就是版本对事务的可见性,以及如何维护版本信息。

Oracle 同样也是多版本,但是和pg不同的设计,首先对于每一个tuple的版本信息有独立的管理。即这个tuple的仅最新版本存在page上(heap page),该tuple的所有的老版本都存在另一个空间上(undo 段),这样在清理老版本的时候直接对undo段进行操作。其次,不同于pg每个heap page上的每个tuple有事务信息(xmin和xmax),oracle只在heap page上保留一些槽位(TD)用于记录操作tuple的事务信息(in-progress,commited),每个(读写)事务操作tuple的时候会被分配到一个TD的槽位,如果所有TD槽位都在in-progress中,TD槽位会扩增;事务commit之后其使用的TD槽位可以被复用。同时tuple中也会有一个最后操作这个tuple保存的td信息(一个byte,很小),根据tuple中的td信息和TD槽内存储的信息可以判断当前事务对这个最新版本的tuple的可见性(这里需要非常注意的一点是,最后操作这个tuple的事务可能晚于即将要操作这个tuple的事务,见下面场景的 X & D)。这一块有点复杂但也没有那么复杂,下面通过一个全面的场景来讨论tuple的对事务的可见性。


构造一个场景:A-F都操作相同 tuple,且操作该tuple的只有事务A-F
在这里插入图片描述

从X的视角进行分析,因为其他事务相对于X来说是全面的。


A & X:A结束于X开始之前。

A相对于X:A commit 之后,X拿到page的排他锁通过Ctid找到 tuple。

此时tuple上的状态分为两种情况:

  1. tuple指向的td事务槽(tdid)未被其他事务复用。此时的lockerTdId为INVALID_TD_SLOT,tuple上的td状态是ATTACH_TD_AS_NEW_OWNER(这是由事务A设置的)。通过TD槽内存的事务A的xid去事务管理器判断A已经commit,此时判断结果为该tuple没有被其他事务改变(TupleIsChanged的step4),事务X直接进行删除操作。

  2. tuple指向的td事务槽(tdid)被其他事务复用。此时tuple上的td状态为ATTACH_TD_AS_HISTORY_OWNER,既然被复用,生成这个tuple的xid(事务A)一定commit了,这是在复用TD的时候判断和操作的,同时如果选择这个TD分配给新事务,会将指向这个TD的tuple的td状态从ATTACH_TD_AS_NEW_OWNER刷新成ATTACH_TD_AS_HISTORY_OWNER。此时X默认A commit了,根据X的快照判断tuple可见性,A开始于X之前,此时可见,事务X继续删除操作。

B & X:B开始于X开始之前,结束于X运行中。

B相对于X:
如果B是一个update的操作且B先拿到page的排他锁,那X只有在B提交的时刻X才能获得该page上的tuple的操作权利(其他时间在等待),即等待B结束。获得锁之后的事务X步骤与A & X情况相同。

有特殊情况,即B进行的途中X获得到了该page的锁:
B是一个select…for update操作,但select之后不需要更新。在B进行select操作的时候会设置lockerTdId的值,然后释放page的锁, 然后X拿到锁。事务B查看之后并不进行更新操作,事务B commit。X拿到锁之后判断B结束,将lockerTdId设置为INVALID_TD_SLOT。此时事务B已经commit,根据X的快照判断tuple可见性,B先于X开始,此时可见,事务X继续删除操作。

C & X: C开始于X之前,结束于X之后。

如果C是一个select…for update操作,且select之后需要更新。在C进行select操作的时候会设置lockerTdId的值,然后释放page的锁,此时X拿到锁,但是发现C还未结束(此时C在等待重新拿到该page的锁去update),事务X删除操作失败,回退,并释放该page的锁,C重新拿到锁继续进行update操作,结束于X之后。

D & X: D开始于X之后,结束于X运行中。

X虽然开始于D之前,但D先拿到page的排他锁并修改tuple然后commit。然后X拿到page的排他锁,开始判断是否可以修改。同样分为两种情况:

  1. tuple指向的TD未被复用,tuple中的td状态是 ATTACH_TD_AS_NEW_OWNER,这是td槽里的xid就是D的xid,根据xid查到D已经提交,这时候判断X和D的先后顺序(根据X快照中的csn判断:JudgeTupCommitBeforeSpecCsn(offset, transaction->GetSnapshotCsn(), isDirty))这里判断到D开始于X之后,X操作失败,退出。
  2. tuple指向的td被复用(因为D commited可以被复用),这时tuple中的td状态是ATTACH_TD_AS_HISTORY_OWNER,同情况1一样去判断tuple对X的可见性,同样发现D开始于X之后,X操作失败退出。
E & X: E开始于X之后,结束于X之后

X如果在E开始前拿到锁,那E只有等待X结束之后操作,此时同 F & X,见 F & X;
X如果在E开始后拿到锁,只有在E是select…for update情况下,select完之后lockerTdId填值然后释放page锁,X拿到锁,此时会查找到lockerXid还在运行中,X直接退出,E后退出。

F & X: F开始于X结束之后。

参考 A & X


事务X存在自身多次操作tuple的两种情况:

insert into … on duplicate key update 

衍生还有insert into … conflict key 等。

以insert into … on duplicate key update为例分析 :

第一种情况:第一次操作这个tuple先锁,第二次操作这个tuple做update操作

create table tu(a int primary key, b int);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "tu_pkey" for table "tu"
CREATE TABLE

insert into tu(a, b) VALUES(0, 1) on DUPLICATE KEY UPDATE b = 88;	Q1
INSERT 0 1
select * from tu;
 a | b 
---+---
 0 | 1
(1 row)

insert into tu(a, b) VALUES(0, 1) on DUPLICATE KEY UPDATE b = 88;	Q2
INSERT 0 1
select * from tu;
 a | b  
---+----
 0 | 88
(1 row)

我们看Q2,SQL的意思是插入数据(0, 1)如果key 重复(这里就是唯一索引,primary key默认生成的索引)则执行update语句。这个语句有insert + update两步操作。下面来看执行过程。

执行计划看不懂,占坑:

explain insert into tu(a, b) VALUES(0, 1) on DUPLICATE KEY UPDATE b = 88;
                   QUERY PLAN                   
------------------------------------------------
 Insert on tu  (cost=0.00..0.01 rows=1 width=0)
   Conflict Resolution: UPDATE
   Conflict Arbiter Indexes: tu_pkey
   ->  Result  (cost=0.00..0.01 rows=1 width=0)
(4 rows)

gdb跟Q2执行过程。
主要是走ExecUpsert调用ExecConflictUpdate的逻辑。流程是

  1. 查找到conflict的ctid;
  2. 然后锁住这个tuple;
  3. 更新。

这里的第2步锁住tuple(v6:LockUnchangedTuple)会分配TD事务槽,更改tuple的td事务信息。这里第3步更新的时候lockerTdId是自己的本身的xid。

第二种情况:

单条SQL:第一次操作这个tuple的时候做insert,第二次做update;
or
单条SQL:第一次操作这个tuple的时候做update,第二次做update;

对于单条SQL这两个操作的cid是一样的,如何保证他们的可见性?

首先构造场景:

建表:

CREATE TABLE t1_lock (
    c1 INT
) ;

INSERT INTO t1_lock VALUES (1);
INSERT INTO t1_lock VALUES (1);
SELECT * FROM t1_lock;

CREATE TABLE t2_lock (
    c1 INT PRIMARY KEY,	// 默认有一个唯一的主键索引
    c2 INT DEFAULT 1
) ;
  1. insert + update
INSERT INTO t2_lock (c1)
    SELECT c1 FROM t1_lock
    ON DUPLICATE KEY UPDATE c2 = c2 + 1;
select * from t2_lock;

行为说明:内部两次upsert操作,第一次是t2_lock 没有数据(这里走索引查找),做insert操作;第二次t2_lock存在conflict的数据(由第一次插入的,这里的conflict是conflict索引,所以会检查所有的索引,进行索引),然后做update操作;
结果:

select * from t2_lock;
 c1 | c2 
----+----
  1 |  2
(1 row)
  1. update + update
delete from t2_lock;

INSERT INTO t2_lock (c1)
    SELECT c1 FROM t1_lock
    ON DUPLICATE KEY UPDATE c2 = c2 + 1;
select * from t2_lock;

INSERT INTO t2_lock (c1)
    SELECT c1 FROM t1_lock
    ON DUPLICATE KEY UPDATE c2 = c2 + 1;
select * from t2_lock;

行为说明:
第二个insert into这条query之前,t2_lock的状态是:

gaussdb=# select * from t2_lock;
 c1 | c2 
----+----
  1 |  2
(1 row)

执行第二条query的时候,其实对t2_lock中的这条数据进行了两次upsert操作,这时候的结果是:

gaussdb=# select * from t2_lock;
 c1 | c2 
----+----
  1 |  4
(1 row)

这里需要解决的问题是,一条SQL对一个tuple做了两次dml操作,且两次dml操作的cid是一样的,如何保证第一次的dml操作的结果对第二次可见?(这里需要处理index 和 heap两部分)

GaussDB v5 的ustore增加了了两种状态:查看可见性的时候上面两种情况对应着两种状态 TM_selfCreated 和 TM_selfUpdated 这两种状态认为可见。

Gaussdb v6 的dstore在这里仍然会判断第一次在TD槽上存储的事务cid,和第二次dml操作该tuple的cid大小,这里是cid相等(同一个命令),这种情况v6做的兼容是从undo里找到上一个关于这个tuple的undo record,比较record了的cid和当前事务的cid,这里其实说不通,因为第一次dml操作也会产生undo的record,且cid和本事务的cid是一样的,照理说还是会认为不可见,但gdb跟的时候没有走到这里,原因是:

以insert + udpate为例,预期是这样的:select 判断是否满足index的约束,此时满足,做insert操作;select判断是否满足index的约束,此时不满足,做update操作。但实际是:第一部分没有问题,第二部分在select判断是否满足index的约束时:由于第一次insert数据也会在index插入1的数据,所以按理来说第二次和第一次插入数据会使index冲突,判定为不满足约束,但此时在查找的时候,构建了本地cr,cr页面构建的逻辑是按照当前的snapshot和cid构建此时能看到的这个page上的所有tuple,也就是说当前cid = 0,index page上这条数据指向的TD槽内的cid = 0,该tuple做回滚操作,回滚到上一个事务commit之后tuple,这里没有上一个事务,tuple为空。所以select的时候在cr页面查找查不到1的数据,认为满足约束,做insert操作,但heap insert 完之后做index insert时给之前那条数据重复,error。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值