PostgreSQL学习(2)-- mvcc

  1.PG事务隔离级别

  在数据库中,并发的操作进行读写数据时,则会遇到脏读、不可重复读、幻读、串行化异常等问题。

  数据库事务的特性: 

  1. 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行;
  2. 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束;
  3. 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
  4. 持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。
  数据库中存在4种事务隔离级别,读未提交、读已提交、可重复读和可序列化。
  PostgreSQL在9.3版本后,已经支持了这四种标准的事务隔离级别。可以通过SET TRANSACTION命令设置当前事务的隔离级别(Transaction Isolation)。
PostgreSQL事务隔离级别和对应数据库问题的关系
隔离级别 脏读  不可重复读 幻读 串行化异常
读未提交 允许,但pg不支持 可能 可能 可能
读已提交 不可能 可能 可能 可能
可重复读 不可能 不可能 允许,但pg不支持 可能
可序列化 不可能 不可能 不可能 不可能

 

  从上表中可以看到在PostgreSQL中,“读未提交”隔离级别,不允许脏读;“可重复读”隔离级别,不允许幻读。

  


 

2.MVCC(Multi-Version Concurrency Control)多版本并发控制

   在PostgreSQL中,MVCC的实现方法是:当插入或者更新一行数据时,旧数据不删除,而是插入一行新数据;通过使用事务id进行标记,把旧数据标记为过期,并保留在数据库直到垃圾收集器回收掉。

 

MVCC优势

  • 使用MVCC,读操作不会阻塞写,写操作也不会阻塞读,提高了并发访问下的性能
  • 事务的回滚可立即完成,无论事务进行了多少操作
  • 数据可以进行大量更新,不段像MySQL和Innodb引擎和Oracle那样需要保证回滚段不会被耗尽

MVCC缺点

  • 事务ID个数有限制事务ID由32位数保存,而事务ID递增,当事务ID用完时,会出现wraparound问题。
  • 大量过期数据占用磁盘并降低查询性能

多版本元组存储结构

  在PostgreSQL中,使用元组头部信息(HeapTupleHeaderData)的字段来标示元组的版本号,元组头部信息的结构如下:

struct HeapTupleHeaderData
{
    union
    {
        HeapTupleFields t_heap;
        DatumTupleFields t_datum;
    }            t_choice;
 
    ItemPointerData t_ctid;        /* current TID of this or newer tuple (or a
                                 * speculative insertion token) */
 
    /* Fields below here must match MinimalTupleData! */
 
    uint16        t_infomask2;    /* number of attributes + various flags */
 
    uint16        t_infomask;        /* various flag bits, see below */
 
    uint8        t_hoff;            /* sizeof header incl. bitmap, padding */
 
    /* ^ - 23 bytes - ^ */
 
    bits8        t_bits[FLEXIBLE_ARRAY_MEMBER];    /* bitmap of NULLs */
 
    /* MORE DATA FOLLOWS AT END OF STRUCT */
};
typedef struct HeapTupleFields
{
TransactionId t_xmin;   /* inserting xact ID */
TransactionId t_xmax;   /* deleting or locking xact ID */
 
union
{
   CommandId t_cid;   /* inserting or deleting command ID, or both */
   TransactionId t_xvac; /* VACUUM FULL xact ID */
}    t_field3;
} HeapTupleFields;
  • t_xmin 存储的是产生这个元组的事务ID,可能是insert或者update语句
  • t_xmax 存储的是删除或者锁定这个元组的事务ID
  • t_cid 包含cmin和cmax两个字段,分别存储创建这个元组的Command ID和删除这个元组的Command ID
  • t_xvac 存储的是VACUUM FULL 命令的事务ID

  数据库中每一个事务中的查询仅能看到:该事务启动之前已经提交的事务所作出的数据更改;该事务之前启动的事务和该事务之后启动的事务修改的数据不可见。

  Postgres中元组版本对一个事务可见,其事务TransactionID要满足以下条件:1. t_xmin<TransactionID;2. t_xmax==0 || t_xmax>TransactionID 。


3.实践

  通过实际操作,观察元组头部信息中的t_xmin和t_xmax的变化。

  • 开启事务,查看事务id,创建表并插入一条记录;再查看该记录的t_xmin。
 1 [root@localhost ~]# su pguser
 2 [pguser@localhost root]$ psql -d test
 3 test=# create table t2(id int);
 4 CREATE TABLE
 5 test=# begin;
 6 BEGIN
 7 test=# select txid_current();
 8  txid_current 
 9 --------------
10           688
11 (1 row)
12 
13 test=# insert into t2(id) values(1);
14 INSERT 0 1
15 test=# commit;
16 COMMIT
17 test=# select ctid, xmin,xmax,cmin,cmax,id from t2;
18  ctid  | xmin | xmax | cmin | cmax | id 
19 -------+------+------+------+------+----
20  (0,1) |  688 |    0 |    0 |    0 |  1
21 (1 row)
  • 先开启事务A,查看事务id。当事务B更新数据后,查看表数据信息,观察事务B更新数据前后的行数据信息。该行数据中t_xmax=763(事务Bid),表明该行被标记为过期,但是对该事务是可见的。
 1 test=# -- 启动事务A
 2 test=# begin;
 3 BEGIN
 4 test=# select txid_current();
 5  txid_current 
 6 --------------
 7           762
 8 (1 row)
 9 
10 test=# select ctid, xmin,xmax,cmin,cmax,id from t2;
11  ctid  | xmin | xmax | cmin | cmax | id 
12 -------+------+------+------+------+----
13  (0,1) |  688 |    0 |    0 |    0 |  1
14 (1 row)
15 test= -- 事务B update 后
16 test=# select ctid, xmin,xmax,cmin,cmax,id from t2;
17  ctid  | xmin | xmax | cmin | cmax | id 
18 -------+------+------+------+------+----
19  (0,1) |  688 |  763 |    0 |    0 |  1
20 (1 row)

 

  • 开启事务A后,再开启事务B,事务A的id比事务B的id小。在事务B中更新表数据,观察当前表的行数据信息,t_xmix=763。
 1 test=# -- 启动事务B
 2 test=# begin;
 3 BEGIN
 4 test=# select txid_current();
 5  txid_current 
 6 --------------
 7           763
 8 (1 row)
 9 
10 test=# select ctid, xmin,xmax,cmin,cmax,id from t2;
11  ctid  | xmin | xmax | cmin | cmax | id 
12 -------+------+------+------+------+----
13  (0,1) |  688 |    0 |    0 |    0 |  1
14 (1 row)
15 
16 test=# update t2 set id=2 where id=1;
17 UPDATE 1
18 test=# select ctid, xmin,xmax,cmin,cmax,id from t2;
19  ctid  | xmin | xmax | cmin | cmax | id 
20 -------+------+------+------+------+----
21  (0,2) |  763 |    0 |    0 |    0 |  2
22 (1 row)
  • 通过pageinspect的函数,查看page信息,发现表t2存在两条数据记录,与事务A、事务B的数据相对于。当事务A和事务B提交后,进行手动Vacuum清理后,只剩下一条最新的记录。
 1 test=# select * from heap_page_items(get_raw_page('t2',0));
 2  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 |   t_data   
 3 ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
 4   1 |   8160 |        1 |     28 |    688 |    763 |        0 | (0,2)  |       16385 |       8960 |     24 |        |       | \x01000000
 5   2 |   8128 |        1 |     28 |    763 |      0 |        0 | (0,2)  |       32769 |      10240 |     24 |        |       | \x02000000
 6 (2 rows)
 7 
 8 test=# -- commit 事务A 事务B
 9 test=# vacuum full;
10 VACUUM
11 test=# select * from heap_page_items(get_raw_page('t2',0));
12  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 |   t_data   
13 ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
14   1 |   8160 |        1 |     28 |    763 |      0 |        0 | (0,1)  |           1 |      11008 |     24 |        |       | \x02000000
15 (1 row)

 

3.总结

  PostgreSQL引入了MVCC多版本机制,保证了事务的原子性和隔离性,实现不同的事务隔离级别。

  PostgreSQL的MVCC实现方法有利有弊。从上面可以看到,多版本控制,会导致旧数据没有删除,最直接的问题就是导致表膨胀。PostgreSQL为了解决这个问题引入了AutoVacuum自动清理辅助进程,定时清理MVCC的过期数据。

 

posted @ 2018-07-27 17:45  游憩  阅读(497)  评论(0编辑  收藏  举报