开头还是介绍一下群,如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, OceanBase, Sql Server等有问题,有需求都可以加群群内有各大数据库行业大咖,可以解决你的问题。加群请联系 liuaustin3 ,(共2580人左右 1 + 2 + 3 + 4 +5 + 6 + 7+8) 新人进7群,开8群)
最近一个同学问我一个问题,这也是练习题上的问题,对于答案,他提出了一个异议,为什么最终的答案是PostgreSQL 读取数据不需要锁? 其实也不奇怪,其他部分常用的数据库在读取数据会产生行的读锁(行读的共享锁),为什么PostgreSQL不会产生读锁。
同时随着越来越多的人熟悉了 PostgreSQL 一部分人对PG 的MVCC的设计问题提出了一些观点,希望PG 和ORACLE ,MySQL 一样,能通过undo 集中式管理方法,来解决Vacuum的问题。我个人看法,任何事务都有利弊,PG这样设计对于数据的提取是有利的,降低了在读取数据时产生锁的开销,对于读取数据非常有利,通过事务号和表中每行数据的特殊标注,完成了读取数据中,判断那些行是否可读取,那些行时再本事务不可读取的,这是一种大智慧体现,(弊端不说了)。
咱们分两个部分说
1 原理
2 代码 代码在下面给出查看源代码的链接位置
原理:PostgreSQL MVCC 原理
我们调整好工具 pageinspect,通过PostgreSQL提供的页面检索的工具,把这个事情说清楚,同时还有一些其他的工具会在下面说明。
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 语句 (通过下面的两个语句来完成一些信息的获取)
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));
实验的主题
通过事务和非事务中对一个表中的行进行查看不同的状态,来分析当前的读取数据的语句是否,会对表,或行产生锁。
我们先在测试机上,打开两个进程,且创建一张表,并写入两条数据。
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。
到这里从实际的情况上,事务外部看不到,事务内部的变化后的数据,事务内只能看到自己变化后的数据。
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等两行数据。
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 的方案更好。
PostgreSQL 相关文章
PostgreSQL 稳定性平台 PG中文社区大会--杭州来去匆匆
PostgreSQL 分组查询可以不进行全表扫描吗?速度提高上千倍?
POSTGRESQL --Austindatabaes 历年文章整理
PostgreSQL 查询语句开发写不好是必然,不是PG的锅
OceanBase 相关文章
PolarDB 相关文章
PolarDB 并行黑科技--从百套MySQL撤下说起 (感谢8018个粉丝的支持)
PolarDB 杀疯了,Everywhere Everytime Everydatabase on Serverless
POLARDB 从一个使用者的角度来说说,POALRDB 怎么打败 MYSQL RDS
PolarDB 最近遇到加字段加不上的问题 与 使用PolarDB 三年感受与恳谈
PolarDB 从节点Down机后,引起的主从节点强一致的争论
PolarDB serverless 真敢搞,你出圈了你知道吗!!!!
PolarDB VS PostgreSQL "云上"性能与成本评测 -- PolarDB 比PostgreSQL 好?
临时工访谈:PolarDB Serverless 发现“大”问题了 之 灭妖记 续集
临时工访谈:庙小妖风大-PolarDB 组团镇妖 之 他们是第一
POLARDB -- Ausitndatabases 历年的文章集合
PolarDB for PostgreSQL 有意思吗?有意思呀
MongoDB 相关文章
数据库 《三体》“二向箔” 思维限制 !8个公众号联合抽奖送书 建立数据库设计新思维
MongoDB 是外星人,水瓶座,怎么和不按套路出牌的他沟通?
MySQL相关文章
阿里云系列
阿里云数据库产品权限设计缺陷 ,六个场景诠释问题,你可以做的更好?
阿里云数据库--市场营销聊胜于无--3年的使用感受与反馈系列
阿里云数据库产品 对内对外一样的卷 --3年阿里云数据库的使用感受与反馈系列
阿里云数据库使用感受--客户服务问题深入剖析与什么是廉价客户 --3年的使用感受与反馈系列
阿里云数据库使用感受--操作界面有点眼花缭乱 --3年的使用感受与反馈系列