前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >PostgreSQL 事务读取行 不使用行锁 真的? 利弊双刃剑

PostgreSQL 事务读取行 不使用行锁 真的? 利弊双刃剑

作者头像
AustinDatabases
发布2024-11-25 11:03:57
发布2024-11-25 11:03:57
730
举报
文章被收录于专栏:AustinDatabases

最近一个同学问我一个问题,这也是练习题上的问题,对于答案,他提出了一个异议,为什么最终的答案是PostgreSQL 读取数据不需要锁? 其实也不奇怪,其他部分常用的数据库在读取数据会产生行的读锁(行读的共享锁),为什么PostgreSQL不会产生读锁。

同时随着越来越多的人熟悉了 PostgreSQL 一部分人对PG 的MVCC的设计问题提出了一些观点,希望PG 和ORACLE ,MySQL 一样,能通过undo 集中式管理方法,来解决Vacuum的问题。我个人看法,任何事务都有利弊,PG这样设计对于数据的提取是有利的,降低了在读取数据时产生锁的开销,对于读取数据非常有利,通过事务号和表中每行数据的特殊标注,完成了读取数据中,判断那些行是否可读取,那些行时再本事务不可读取的,这是一种大智慧体现,(弊端不说了)。

咱们分两个部分说

1 原理

2 代码 代码在下面给出查看源代码的链接位置

原理:PostgreSQL MVCC 原理

我们调整好工具 pageinspect,通过PostgreSQL提供的页面检索的工具,把这个事情说清楚,同时还有一些其他的工具会在下面说明。

代码语言:javascript
复制
postgres=# \c dvdrental
You are now connected to database "dvdrental" as user "postgres".
dvdrental=# select lp,lp_off,t_xmin,t_xmax,t_ctid,t_oid,t_data from heap_page_items(get_raw_page('actor', 0));
 lp  | lp_off | t_xmin | t_xmax | t_ctid  | t_oid |                                       t_data                                      
 
-----+--------+--------+--------+---------+-------+-----------------------------------------------------------------------------------
-
   1 |   8136 |    824 |      0 | (0,1)   |       | \x010000001350656e656c6f7065114775696e6573730000002037f5e29e800100
   2 |   8080 |    824 |      0 | (0,2)   |       | \x020000000b4e69636b135761686c626572670000000000002037f5e29e800100
   3 |   8032 |    824 |      0 | (0,3)   |       | \x030000000745640d43686173650000002037f5e29e800100
   4 |   7976 |    824 |      0 | (0,4)   |       | \x04000000134a656e6e696665720d446176697300000000002037f5e29e800100
   5 |   7920 |    824 |      0 | (0,5)   |       | \x050000000f4a6f686e6e791b4c6f6c6c6f627269676964612037f5e29e800100
   6 |   7864 |    824 |      0 | (0,6)   |       | \x060000000d4265747465154e6963686f6c736f6e000000002037f5e29e800100
   7 |   7808 |    824 |      0 | (0,7)   |       | \x070000000d47726163650f4d6f7374656c000000000000002037f5e29e800100
   8 |   7752 |    824 |      0 | (0,8)   |       | \x08000000114d617474686577154a6f68616e73736f6e00002037f5e29e800100
   9 |   7704 |    824 |      0 | (0,9)   |       | \x09000000094a6f650d5377616e6b00002037f5e29e800100
  10 |   7648 |    824 |      0 | (0,10)  |       | \x0a0000001543687269737469616e0d4761626c65000000002037f5e29e800100

先说结论:

1 PostgreSQL 的表为当前数据和历史数据的存放地,一张表中需要能够存储整体数据操作中的所有历史数据和当前事务操作中的最新数据。

2 数据的读取根据的是事务的快照,基于PG的表中包含,他历史的行+现在事务正在操作的行=整体的表。其中通过事务快照的视图,将该事务在其生命周期内可以看到的所有数据库版本标明。事务快照的关键是包含了当前事务能看到所有已经提交得事务ID(Transaction ID)。

在做下面的实验和说明读取行,并不需要锁的实验中,我们要说明我们要使用的一些命令、函数。

1 heap_page_items 函数

2 get_raw_page 函数

3 txid_current 函数

4 txid_current 函数

5 txid_current_snapshot 函数

6 语句 (通过下面的两个语句来完成一些信息的获取)

代码语言:javascript
复制
SELECT pl.pid, pl.locktype, pl.relation::regclass, pl.page, pl.tuple, pl.virtualxid, pl.transactionid, pl.mode, pl.granted, pl.fastpath
FROM pg_locks pl
LEFT JOIN pg_stat_activity psa ON pl.pid = psa.pid
ORDER BY pl.pid;

select lp,lp_off,t_xmin,t_xmax,t_ctid,t_oid,t_data from heap_page_items(get_raw_page('test', 0));

实验的主题

通过事务和非事务中对一个表中的行进行查看不同的状态,来分析当前的读取数据的语句是否,会对表,或行产生锁。

我们先在测试机上,打开两个进程,且创建一张表,并写入两条数据。

代码语言:javascript
复制
postgres=# \c dvdrental 
You are now connected to database "dvdrental" as user "postgres".
dvdrental=# create table test (id int,name varhcar(20));
dvdrental=# insert into test (id,name) values (1,'Tom'),(2,'Jim');
INSERT 0 2

dvdrental=# \d test
                       Table "public.test"
 Column |         Type          | Collation | Nullable | Default 
--------+-----------------------+-----------+----------+---------
 id     | integer               |           |          | 
 name   | character varying(20) |           |          | 

dvdrental=# select * from test;
 id | name 
----+------
  1 | Tom
  2 | Jim
(2 rows)

我们多次对上述两个进程中执行同样的命令查看结果

好我们已经有表和两条数据,我们将基于这两条数据来进行下面的实验和证明。

我们通过这张图中可以看到如下的内容

1 当前事务的ID,在事务内和事务外是不同的 2 当前事务snapshot 可见的范围

从第二张图,我们看到,当前的t_xmin,都为980,此时无论是事务内,还是事务外,对于test表中的数据行都是可见的。

下一步,我们将对事务中数据行进行update操作

我们参见上图,外部的当前事务ID已经是983,但事务内部的事务ID依然是982。

到这里从实际的情况上,事务外部看不到,事务内部的变化后的数据,事务内只能看到自己变化后的数据。

代码语言:javascript
复制
dvdrental=# SELECT pl.pid, pl.locktype, pl.relation::regclass, pl.page, pl.tuple, pl.virtualxid, pl.transactionid, pl.mode, pl.granted, pl.fastpath
FROM pg_locks pl
LEFT JOIN pg_stat_activity psa ON pl.pid = psa.pid  ORDER BY pl.pid;
  pid  |   locktype    |         relation          | page | tuple | virtualxid | transactionid |       mode       | granted | fastpath 
-------+---------------+---------------------------+------+-------+------------+---------------+------------------+---------+----------
 23395 | relation      | test                      |      |       |            |               | RowExclusiveLock | t       | t
 23395 | virtualxid    |                           |      |       | 4/3        |               | ExclusiveLock    | t       | t
 23395 | relation      | pg_database_oid_index     |      |       |            |               | AccessShareLock  | t       | f
 23395 | relation      | pg_authid_rolname_index   |      |       |            |               | AccessShareLock  | t       | f
 23395 | relation      | pg_authid_oid_index       |      |       |            |               | AccessShareLock  | t       | f
 23395 | relation      | pg_authid                 |      |       |            |               | AccessShareLock  | t       | f
 23395 | transactionid |                           |      |       |            |           982 | ExclusiveLock    | t       | f
 23395 | relation      | pg_database               |      |       |            |               | AccessShareLock  | t       | f
 23395 | relation      | pg_database_datname_index |      |       |            |               | AccessShareLock  | t       | f
 23395 | relation      | pg_stat_activity          |      |       |            |               | AccessShareLock  | t       | t
 23395 | relation      | test                      |      |       |            |               | AccessShareLock  | t       | t
 23594 | relation      | pg_database_datname_index |      |       |            |               | AccessShareLock  | t       | f
 23594 | relation      | pg_locks                  |      |       |            |               | AccessShareLock  | t       | t
 23594 | virtualxid    |                           |      |       | 6/13       |               | ExclusiveLock    | t       | t
 23594 | relation      | pg_authid_rolname_index   |      |       |            |               | AccessShareLock  | t       | f
 23594 | relation      | pg_stat_activity          |      |       |            |               | AccessShareLock  | t       | t
 23594 | relation      | pg_authid                 |      |       |            |               | AccessShareLock  | t       | f
 23594 | relation      | pg_database               |      |       |            |               | AccessShareLock  | t       | f
 23594 | relation      | pg_authid_oid_index       |      |       |            |               | AccessShareLock  | t       | f
 23594 | relation      | pg_database_oid_index     |      |       |            |               | AccessShareLock  | t       | f
(20 rows)

dvdrental=# 

此时我们将查看,当前数据库中分配锁的语句执行,且将结果贴到上面。这里已经发现了行的排它锁,那个锁就是我们进行Update 操作的时候发生的锁。但我们并未发现我们读取语句是的行读锁。

为什么?结果可以肯定的是,在读取数据的时候,并未有行的读锁,这里我们可以在单独给出条件,对数据进行单行的读取,此时我们再次运行查看行锁的语句,依然没有对应的行的lock。

好了结论我们已经有了,的确在题目中给出的PG读数据不需要加行锁,是对的。


为什么,他是怎么解决这个问题的。

1 MVCC 多版本控制

2 基于PG将历史行和当前行存储在一个表的机制

3 丰富的数据行隐藏的字段如 xmin,xmax,ctid,infomask,infomask2等丰富的事务行标记

4 通过读取snapshot 镜像来保证事务的特性,哪些数据可以显示,哪些数据不能显示

5 在运行SQL的时候,通过判断程序来,完成判断读取的数据是否可见工作

所以在此基础上PostgreSQL 在我们实验中的 read committed 隔离模式下,不需要读行锁来对数据行在读取期间加锁。

读取事务的规则

t_xmin 为数据插入时的事务号

t_xmax 为数据删除或更新时的事务号

一个总结简单规则,通过当前事务快照 txid_current_snapshot()中的号范围来作为基准判断你是否可以看到某些行,或者看不到某些行。

1 我们以上图为例,在一个事务中,t_xmin是之前某个事务创建的,则我们可以看到他。典型的就是980,我们当前的事务号是982 ,则980是可以看见的。

2 t_max 中存在数字,则说明这个行已经被删除或被更新过,如果你的事务号和他的t_xmax是一致的,则我们不能看到他。

3 在行中可以看到,t_ctid,存储数据更新后的新的物理位置,并进行行物理位置的指示

所以根据上面部分的查看逻辑,我们能看到的行是 lp_off 数字为 8160,8096等两行数据。

代码语言:javascript
复制
dvdrental=*# select lp,lp_off,t_xmin,t_xmax,t_ctid,t_oid,t_data from heap_page_items(get_raw_page('test', 0));
 lp | lp_off | t_xmin | t_xmax | t_ctid | t_oid |       t_data       
----+--------+--------+--------+--------+-------+--------------------
  1 |   8160 |    980 |      0 | (0,1)  |       | \x0100000009546f6d
  2 |   8128 |    980 |    982 | (0,3)  |       | \x02000000094a696d
  3 |   8096 |    982 |      0 | (0,3)  |       | \x0200000009323232
(3 rows)

如果想了解更深的关于如何判断的代码方面的知识,可以通过下面的连接来查看源代码 https://doxygen.postgresql.org/heapam__visibility_8c.html

当然这里还有一些复杂的部分我们没有讲到,基于时间的原因。比如infomask infomask2 字段的含义,这两个字段尤其infomask 是行的标记信息

如他可以标记如下的功能,

1 行操作删除失败

2 行操作删除成功

3 行被锁定

4 行删除或更新是多事务操作

5 行已经提交

6 插入行无效

7 插入行被锁定

等等

总结:PostgreSQL 基于原理,在读取数据行的时候,不需要使用常见的只读行锁来对读取的数据加锁,而是通过snapshot+ 判断的方式来去解决读取数据时的行的可见性等问题。

这样操作的好处也是显而易见,在一些数据库上,如SQL SERVER上我们经常提到的其他的数据库都没有的锁升级。这证明锁本身开启是有开销的,(其实SQL SERVER的锁升级也可以讲讲,不过今天算了吧)MSSQL 正是因为无法承受大量行读取时的行锁的开销上涨(其中一个原因),而产生了一个解决方案,锁升级。这也是一种解决行锁过多的一种方案,但相对于PostgreSQL的方案,我觉得还是PostgreSQL 的方案更好。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AustinDatabases 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档