作者:崔鹏
并发控制技术
MVCC多版本并发控制,读不阻塞写,写不阻塞读。
PostgreSQL使用MVCC的一种变体——快照隔离技术SI
严格两阶段锁定S2PL,写操作进行时,会阻塞对象上的读操作。
乐观并发控制OCC
是一种用来解决写-写冲突的无锁并发控制,认为事务间争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有新提交改变,如果没有就提交,如果有就放弃并重试。
乐观并发控制类似自选锁。乐观并发控制适用于低数据争用,写冲突比较少的环境。
关系型数据库实现Si的区别
1.Oracle/innodb
undo回滚段
2.sqlserver
temp临时表空间
3.postgresql
表的数据页面
SI的实现差异
ANSI SQL-92中定义三种异常,脏读/不可重复读/幻读,SI都不会出现。
但是SI无法实现真正的可串行化,SI可能会出现串行化异常。
为了解决这个问题,PostgreSQL9.1之后添加了,可串行化快照隔离SSI。SSI可检测串行化异常。
目前Oralce只支持SI。
PostgreSQL中的事物隔离级别
事物标识
每当事务开始时,事务管理器都会分配一个唯一标识符,称为事务 ID (txid)。PostgreSQL 的 txid 是一个 32 位无符号整数,大约为 42 亿。如果在事务开始后执行内置的txid_current()函数,该函数将返回当前的 txid。
postgres=# begin;BEGINpostgres=# select txid_current(); txid_current -------------- 565(1 row)
PostgreSQL 保留了以下三个特殊的 txid:
0表示无效的事物ID。
1表示Bootstrap txid,仅用于数据库集群的初始化。
2表示Frozen txid,冻结ID。
假设 txid=100,大于100的txid是它的未来,在txid 100中是不可见的;小于 100 的 txid 是它的过去 是可见的。PostgreSQL将txid视为一个圆圈。
之前的 21 亿是“过去”,接下来的 21 亿是“未来”。
没有为 BEGIN 命令分配 txid。
在PostgreSQL中,当执行完BEGIN命令后执行第一个命令时,事务管理器会分配一个tixd,然后它的事务就开始了。如下图:
元组结构
一个堆元组由三部分组成,即 HeapTupleHeaderData 结构、NULL 位图和用户数据
元组的增、删、改
元组-新增
postgres=# CREATE EXTENSION pageinspect;postgres=# CREATE TABLE tbl(data text);CREATE TABLEpostgres=# INSERT INTO tbl VALUES('A'); INSERT 0 1postgres=# SELECT lp as tuple , t_xmin , t_xmax , t_field3 as t_cid , t_ctid FROM heap_page_items ( get_raw_page ( 'tbl' , 0 )); tuple | t_xmin | t_xmax | t_cid | t_ctid -------+--------+--------+-------+-------- 1 | 568 | 0 | 0 | (0,1)(1 row)元组-删除
元组-更新
空间映射
在插入堆或索引元组时,PostgreSQL 使用对应表或索引的FSM来选择可以插入它的页面。
所有表和索引都有各自的 FSM。每个 FSM 将有关每个页面的可用空间容量的信息存储在相应的表或索引文件中。
所有 FSM 都以后缀“fsm”存储,如有必要,它们会加载到共享内存中。
FSM文件产生于表被第一次vacuum时,如下:
pg_freespacemappostgres=# CREATE EXTENSION pg_freespacemap;CREATE EXTENSIONpostgres=# SELECT *, round(100 * avail/8192 ,2) as "freespace ratio" FROM pg_freespace('test'); blkno | avail | freespace ratio -------+-------+----------------- 0 | 7744 | 94.00(1 row)Commit Log
PostgreSQL 在Commit Log 中保存事务的状态。Commit Log,通常称为clog,分配给共享内存,并在整个事务处理过程中使用。
事务状态,PostgreSQL 定义了四种事务状态,
即IN_PROGRESS、COMMITTED、ABORTED 和 SUB_COMMITTED。
前三个状态是显而易见的。例如,当一个事务在进行中时,它的状态是 IN_PROGRESS 等。SUB_COMMITTED 用于子事务。
如何工作?
clog在逻辑上是一个数据,存储在共享内存区中,由一系列8KB页面组成。数组的序号,索引对应着相应事务的标识。其内容则是相应的事务状态。
T1:txid 200 提交;txid 200 的状态从 IN_PROGRESS 更改为 COMMITTED。
T2:txid 201 中止;txid 201 的状态从 IN_PROGRESS 更改为 ABORTED。
clog 维护
当 PostgreSQL 关闭或 checkpoint 进程运行时,clog 的数据被写入存储在pg_xact子目录下的文件中。这些文件被命名为0000、0001等。最大文件大小为 256 KB。
当clog使用8个页面(第一页到第八页;总大小为64 KB)时,将其数据写入0000(64 KB),当使用第37个页面时(296 KB)数据会写入 0000 和 0001两个文件中,其大小分别为 256 KB 和 40 KB。
当 PostgreSQL 启动时,存储在 pg_clog 的文件(pg_xact 的文件)中的数据被加载以初始化 clog。
事务快照是一个数据集,存储着某个特性事务,在某个特性的时间点,所看到的事务状态信息,哪些事务处于活跃状态活跃状态意味着事务正在进行中或还没有开始。
内置函数查询快照情况
SELECT txid_current_snapshot (); txid_current_snapshot ----------------------- 100 : 104 : 100 , 102
txid_current_snapshot 的文本表示为'xmin:xmax:xip_list',各部分描述如下。
xmin
最早仍然活跃的事务的txid,所有比它跟早的事务(txid < xmin),要么已经提交并可见,要么已经回滚并生成死元组。
xmax
第一个尚未分配的 txid。所有txid>xmax的事物在获取快照时尚未启动,因此其结果对当前事物不可见。
xip_list
获取快照时,活跃事物的txid列表,该列表仅包含xmin与xmax之间的txid。
例如在快照100:104:100,102中
xmin是100,xmax是104,而xip_list为100,102。
1.活跃的txid,正在运行中,或仍未开始的事务不可见。
2.不活跃的txid,已经提交或中止的事务,如果提交了就可见。
txid < 100的事务不活跃
txid > 104的事务是活跃的
txid等于100和102的事务是活跃的,因为它们在xip_list中,而txid等于101和103的事务不活跃。
可见性检查规则
可见性检查是一组规则,用于确定一条元祖是否对一个事务可见,可见性检查规则会用到元祖的t_xmin和t_xmax,提交日志Clog.
Status of t_xmin is ABORTED 元祖始终不可见 /* t_xmin status == ABORTED */Rule 1: IF t_xmin status is 'ABORTED' THEN RETURN 'Invisible' END IFStatus of t_xmin is IN_PROGRESS 元祖基本上不可见 /* t_xmin status == IN_PROGRESS */ IF t_xmin status is 'IN_PROGRESS' THEN IF t_xmin = current_txid THENRule 2: IF t_xmax = INVALID THEN RETURN 'Visible'Rule 3: ELSE /* this tuple has been deleted or updated by the current transaction itself. */ RETURN 'Invisible' END IFRule 4: ELSE /* t_xmin ≠ current_txid */ RETURN 'Invisible' END IF END
Status of t_xmin is COMMITTED 是可见的。 /* t_xmin status == COMMITTED */ IF t_xmin status is 'COMMITTED' THENRule 5: IF t_xmin is active in the obtained transaction snapshot THEN RETURN 'Invisible'Rule 6: ELSE IF t_xmax = INVALID OR status of t_xmax is 'ABORTED' THEN RETURN 'Visible' ELSE IF t_xmax status is 'IN_PROGRESS' THENRule 7: IF t_xmax = current_txid THEN RETURN 'Invisible'Rule 8: ELSE /* t_xmax ≠ current_txid */ RETURN 'Visible' END IF ELSE IF t_xmax status is 'COMMITTED' THENRule 9: IF t_xmax is active in the obtained transaction snapshot THEN RETURN 'Visible'Rule 10: ELSE RETURN 'Invisible' END IF END IF
Rule 1: If Status(t_xmin) = ABORTED ⇒ InvisibleRule 2: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD ⇒ VisibleRule 3: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax ≠ INVAILD ⇒ InvisibleRule 4: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin ≠ current_txid ⇒ InvisibleRule 5: If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active ⇒ InvisibleRule 6: If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) ⇒ VisibleRule 7: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid ⇒ InvisibleRule 8: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax ≠ current_txid ⇒ VisibleRule 9: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active ⇒ VisibleRule 10: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) ≠ active ⇒ Invisible
需要维护的过程
PostgreSQL 的并发控制机制需要以下维护流程。
1.删除死元组及指向死元组的索引元祖。
2.移除提交日志中非必要的部分。
3.冻结旧的事务标识。
4.更新FSM、VM及统计信息
冻结
假设元祖Tuple_1是由txid=100事务创建的,即Tuple_1的t_xmin=100,服务器运行了很长时间。
但是Tuple_1一直未曾被修改,假设txid已经前进到2^31+100,这时候正好执行一条select命令。
此时因为对当前事务而言txid=100的事务属于过去的事务,所以Tuple_1对当前事务可见,
然后再执行相同的select命令,此时txid前进至2^31+101,但对当前事务而言,txid=100的事务是属于未来的,因此Tuple_1不再可见,这就是事务回卷。
为了解决这个问题,PostgreSQL 引入了一个叫做freeze txid的概念,并实现了一个叫做FREEZE的过程。
在 PostgreSQL 中定义了一个冻结的 txid,它是一个特殊的保留 txid 2,被定义为总是比所有其他 txid 旧。
换句话说,冻结的 txid 始终处于非活动状态且可见。
vacuum过程会调用冻结过程。冻结的过程将扫描所有表文件,如果 t_xmin 值早于当前 txid 减去vacuum_freeze_min_age(默认值为 5000 万)更旧,则将该元祖的t_xmin重写为冻结事务标识。
pg_freespacemap
create table test(id int);insert into test(id) select generate_series(1,100000) as id;CREATE EXTENSION pg_freespacemap;\dx
SELECT count(*) as "number of pages", pg_size_pretty(cast(avg(avail) as bigint)) as "Av. freespace size", round(100 * avg(avail)/8192 ,2) as "Av. freespace ratio" FROM pg_freespace('test');SELECT *, round(100 * avail/8192 ,2) as "freespace ratio" FROM pg_freespace('test');--eg exec vacuumdelete from test where id % 10 !=0; SELECT count(*) as "number of pages", pg_size_pretty(cast(avg(avail) as bigint)) as "Av. freespace size", round(100 * avg(avail)/8192 ,2) as "Av. freespace ratio" FROM pg_freespace('test'); vacuum test; SELECT count(*) as "number of pages", pg_size_pretty(cast(avg(avail) as bigint)) as "Av. freespace size", round(100 * avg(avail)/8192 ,2) as "Av. freespace ratio" FROM pg_freespace('test');--eg exec vacuum full SELECT *, round(100 * avail/8192 ,2) as "freespace ratio" FROM pg_freespace('test'); vacuum full test; SELECT *, round(100 * avail/8192 ,2) as "freespace ratio" FROM pg_freespace('test');END
点击加载更多