赞
踩
事务(transaction):
是用户定义的一组数据库操作,要么全做要么全不做,失败即回滚。
事务是恢复和并发控制的基本单元。
保存点(savePoint)
在一个大的事务中,可以把操作过程分成几个部分,第一个部分执行成功后可以建一个保存点,若后面的部分执行失败,则回滚到此保存点,而不必回滚整个事务。
事务的实现即:RDBMS采取何种技术确保事务的ACID特性?
回退(rollback):
撤销sql执行过程。事务管理可以管理insert、update、delete语句;不能回退create、drop操作。
RDBMS(Relational Database Management System,关系数据库管理系统)
是指包括相互联系的逻辑组织和存取这些数据的一套程序 (数据库管理系统软件)。
关系数据库管理系统就是管理关系数据库,并将数据逻辑组织的系统。
事务是数据库的逻辑工作单位。要么都做,要么都不做。==》通过MVCC保证
事务完成时,数据必须处于一致状态,数据的完整性约束没有被破坏,事务在执行过程中发生错误,会被回滚到事务开始前的状态。
事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。==》用锁和MVCC来保证。
读读不存在并发问题;
读写通过MVCC来解决并发问题;
写写通过加锁来解决并发问题。
事务一旦提交,所做修改会永久的保存在数据库中。==》保证了db的可靠性,用WAL日志来实现
其中一致性是事务的最终目的,为了达到一致性需要保证原子性、隔离性、永久性。
那么pg是怎么完成ACID的呢?
在标准SQL规范中,定义了4个事务隔离级别(由低到高):
读未提交(RU级别、read uncommitted) | 读已提交(read committed) | 可重复读(repeatable read) | 序列化(serializable) | |
---|---|---|---|---|
允许操作 | 允许事务读取未被其他事务提交的变更 | 允许事务读取已经被其他事务提交的变更 | 事务读取数据时,禁止其他事务对这个字段进行更新 | 所有事务都一个接一个地串行执行 |
可能存在问题 | 脏读、不可重复读、幻读 | 不可重复读、幻读 | 幻读==》添加间隙锁解决此类问题 | 数据安全。但是添加大量行锁会导致大量超时和锁竞争问题。 |
避免问题 | ||||
举例 | 事务读到了其他事务未提交的数据。其他事务回滚导致脏读。 | 同一个事务读了两次数据,分别读取了其他事务提交的内容,但是两次结果不一致。这就是不可重复读。 | 事务读了两次数据,不管数据怎么修改,都只读第一次的数据。==》导致幻读:每次select时,mvcc的read view不会变化。但是其他事务做了新增操作,真实的数据和当前的read view不同。 | |
数据库 | oracle、pg默认隔离级别 | mysql默认隔离级别 |
pg仅支持2种隔离级别:读已提交(默认)、可串行化。
事务隔离级别的实现:
每个命令都是从一个新的快照开始执行的。
为什么使用RC级别而不使用RR级别?
提高并发度并降低死锁概率。
每个命令都是从事务开始时的快照开始执行的。
MVCC是数据库并发访问时,保证数据一致性的一种方法。实现MVCC的方法有以下两种:
事务ID:XID、txid(transaction id)
pg中每个事务开始时,事务管理器都会分配一个唯一id,从3开始递增。
32位无符号整数,取值空间:2^32-1;如果超过范围从头开始算,称为事务回卷。
pg中表中有以下4个内置表字段,每个tuple的更新时是先del旧的再insert新的tuple。
字段 | 说明 | 默认值 | 举例 |
---|---|---|---|
xmin | insert tuple 时的 xid | ||
xmax | del tuple 时的 xid | 0,表示未删除 | |
cmin | 事务内部 insert 的命令ID | 0,递增 | |
cmax | 事务内部 del 的命令ID | 0,递增 | |
ctid | 磁盘上的物理位置,格式:(page,offset) | (0,1)表示0号page的第1个位置。如果xmax=0,表示最新版本;如果xmax!=0,ctid指向更新后的元组,形成了版本链。 |
xmin=事务id xmax=0 ctid=(0,1),指向当前元组
xmax=事务id
tupleOld的xmax=事务id ctid指向新的元组,tupleNew的xmin=事务id ctid指向当前元组
原子性:通过当前事务id对tuple进行标记,不管是commit还是rollback操作都可以通过xmin和xmax保证事务的原子性。
在不同事务中,可以根据xmin和xmax判断事务可见性。
快照(SnapshotData)
维护了以下一些信息:
- TransactionId xmin; // 记录了未提交并活跃的事务最小xid,如果t_xid < xmin则元组数据已提交:可见
- TransactionId xmax; //记录了已提交事务最大xid+1,如果t_xid >= xmax 则元组未提交:不可见
- TransactionId *xip; // 活动事务id列表
对于t_xid在[xmin, xmax)之间数据,需要结合clog日志判断其修改的数据是否可见
每次select获取当前db SnapshotData,判断数据的可见性。
区分元组t_xmin和快照s_xmin,对于当前元组数据:
cmin、cmax 用于同一个事务中实现版本可见性判断
clog(commit log):
pg记录事务状态。包括以下四种:
transaction_status_in_progress =0x00:表示事务正在进行中
transaction_status_committed =0x01:表示事务已提交
transaction_status_aborted =0x02:表示事务已回滚
transaction_status_sub_committed =0x03:表示子事务已提交
结构:数组,由缓存(SLRU Buffer Pool )中一系列的8K页面组成。
数组下标对应事务txid,数组内容则为事务状态。每个事务状态2bit,一个块8KB可以存储8KB*8/2 = 32K个事务的状态。
当shutdown pg或Checkpoint运行时,CLOG数据会由内存写入pg_clog(pg 10后叫pg_xact)目录中的文件。这些文件被命名为0000,0001,最大256KB。当pg启动时,会加载这些文件用于初始化CLOG。
CLOG数据会不断增长,但并非所有数据都是必要的,清理过程也会定期清理掉不再需要的CLOG页面和文件。
pg可以通过调用三个内部函数——TransactionIdIsInProcess、TransactionIdDidCommit和TransactionIdDidAbort,读取CLOG返回所请求事务状态。
判断元组的可见性非常频繁,每次从缓存或者磁盘读取clog信息依然不够高效,引入了Hint Bits概念。t_informask中存储的一些标志位保存了插入/删除该元组的事务的状态。
元组中的 Hint Bits采用延迟更新策略,并不会在事务提交或者回滚时主动更新所有操作过的元组Hint Bits。
等到第一次访问(可能是VACUUM,DML或SELECT)该元组并进行可见性判断时:
pg在事务提交前,只需要访问原来的数据;提交后,系统更新元组的存储标识,直到Vaccum进程回收为止。
相比InnoDB和Oracle,pg多版本优势在于:
劣势在于:
官方文档:Routine Vacuuming
Visibility Map中标记了哪些page中是没有dead tuple的,数据量很小可以cache到内存中。这有两个好处:
VACUUM寻找不再被别的任何事务任何人看到的行。这些行可能是页的中间几行。
一般pg会有个异步任务自动执行,如果突然有大量数据执行update全表等操作,会让磁盘空间瞬间翻倍,需要手动执行vacuum,但是这个操作会锁表,用的时候慎重。
--加表名指定表 不加表名表示全局处理
vacuum t_lxs;
--获取表空间大小
--vacuum允许 pg重用该空间,但是,它不会将该空间返回给操作系统。
SELECT pg_relation_size('t_lxs');--8192
DELETE FROM t_lxs;
--如果从表中的某个位置开始,ALL rows are dead,VACUUM可以截断表。
VACUUM t_lxs;
SELECT pg_relation_size('t_lxs');--0
--但是大表末尾总有那么几行数据,靠VACUUM几乎很难释放空间。通过使用VACUUM FULL重排数据的磁盘位置,可以解决表膨胀的问题。但是这个操作会直接锁表。一定要在业务低频使用时进行。
VACUUM FULL t_lxs;
0 1 2是系统预留ID,这三个ID比任何普通xid都要旧。
xid一直递增达到2^32为最大值,然后继续从3开始,以前的xid比新的xid大,回卷后无法按xid大小判断数据可见性。
回卷问题导致事务id可见性的判断怎么解决?
diff = (int32) (id1 - id2);
来判断id1<id2举例:txid=100的事务,[101,231+100] 均为不可见事务;[231+101, 99] 均为可见事务。
代码解析:
/*
* TransactionIdPrecedes --- is id1 logically < id2?
*/
bool TransactionIdPrecedes(TransactionId id1, TransactionId id2) // 结果返回一个bool值
{
int32 diff;
if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) //若其中一个不是普通id,则其一定较新(较大)
return (id1 < id2);
diff = (int32) (id1 - id2);
return (diff < 0);
}
普通xid比较:diff = (int32) (id1 - id2);
注意:int 32带符号,第一位表示符号位,取值范围为 [-2(n-1), 2(n-1)-1],即[-231,231-1]。
当两个txid(32位)相减 (id1 - id2)>231时,发生回卷,转换位int32时符号位从0变成了1,(int 32) (id1 - id2)
是个负数。此时id1<id2==true,id1事务更小更旧,id1对id2是可见的。
举例:id1=231+101,id2=100。id1-id2=231+1,int32转换后这个值是-1,小于0,因此id1的事务反而要旧,id2可见id1,id1不可见id2。
但是如果xid=100确实是很久很久的事务,那么对于231+101这个事务看见,此时上面的判断就是错的。为了避免这种问题,pg必须保证一个数据库中两个有效的事务之间的年龄最多是231(同一个数据库中,存在的最旧和最新两个事务txid相差不得超过231)。这时靠事务冻结来保证。
当超过231时,就把旧的事务换成一个FrozenTransactionId=2的特殊事务,当正常事务ID与冻结事务ID比较时,会认为正常xid比FrozenTransactionId更新。
freeze是被动触发的,可以调节pg的一些参数优化freeze,更多时候提倡用户进行主动预测需要freeze的时机,选择合适的时间(比如pg负载较低的时间)主动执行vacuum freeze命令。
目前已经有很多实现好的开源PostgreSQL vacuum freeze监控管理工具,比如flexible-freeze,能够:确定数据库的高峰和低峰期;在数据库低峰期创建一个cron job执行flexible_freeze.py;flexible_freeze.py会自动对具有最老XID的表进行vacuum freeze。
PG目前有19个查询算子。
每个执行计划都是一颗树,执行时自顶向下开始,自底向上计算,最终得到查询结果。
输入:1个结果集;
场景:order by、Unique算子、
原理:
原理:
场景:DISTINCT
输入:已排序的结果集;
输出:消除值重复的行;
原理:Sort->Seq Scan;
场景:limit、limit offset
输入:1个结果集;
输出:结果集的前n行,直接丢掉剩余的行
场景:GROUP BY
原理:Sort->Seq Scan
场景:聚合函数:AVG(), COUNT(), MAX(), MIN(), STDDEV(), SUM(), and VARIANCE()
原理:
场景:UNION、表继承
输入:2+个结果集
原理:返回第一个输入集的所有行,然后拼接第二个输入集的所有行。
性能影响不大。
场景:
select timeofday();
where 1==1;
SELECT * FROM customers, rentals
WHERE customers.customer_id = rentals.customer_id;
两个算子一起工作。
--所有等值连接都可以转换为inner joins
SELECT * FROM customers, rentals
WHERE customers.customer_id = rentals.customer_id;
Subquery Scan用于union子句;
Subplan用于子选择。
场景:用于内部标记,将输入集中每一行加入结果集。
用于事务中检索元组id(ctid),很少用到。
场景:Merge join算子对于内部输入集不可重新定位的情况,认为物化一个子选择比重复选择代价更高。
select的别名是否可以作为where查询条件?
不能,因为执行计划中where在select之前。
shared_buffers所代表的内存区域可以看成是一个以8KB的block为单位的数组,即最小的分配单位是8KB。这正好是一个page的大小,每个page以page内部的元数据(Page Header)互相区分。
这样,当Postgres想要从disk获取(主要是table和index)数据(page)时,他会(根据page的元数据)先搜索shared_buffers,确认该page是否在shared_buffers中,如果存在,则直接命中,返回缓存的数据以避免I/O。
如果不存在,Postgres才会通过I/O访问disk获取数据(显然要比从shared_buffers中获取慢得多)。
以页为单位,cache满的时候,会淘汰不常用的页。淘汰后的数据则进行刷盘,但是一般数据都是通过WAL+Checkpointer保证修改的数据刷盘,而不用等到cache满了才进行刷盘。
使用explain时,Shared read表示来自disk,Shared hit则是已经在cache中。
启动pg,主进程为Postmaster(pg的bin目录下,是一个指向Postgres的链接)。Postmaster是整个数据库实例的总控进程,负责启动和关闭数据库实例,同时fork出一些与数据库实例相关的辅助进程,并对其进行管理。
辅助进程 | 作用 | 配置 |
---|---|---|
Logger | 系统日志 | 参数logging_collect设置为on时启动该辅助进程 |
每次客户端与数据库建立连接时,pg数据库都会启动一个服务进程来为该连接服务,故而是进程架构模型,而MySQL是线程架构模型。当某个服务进程报错时,Postmaster主进程会自动完成系统恢复,恢复过程中停掉所有的服务进程,然后进行数据的一致性恢复,恢复完成后数据库才能接受新的连接。
autovacuum 是 postgresql 里非常重要的一个服务端进程,能够自动运行,在一定条件下自动触发对 dead tuples 进行清理并对表进行分析。
在pg中更新、删除行后,数据行并不会马上从数据块中清理掉,而是需要等VACUUM时时清理。为了加快VACUUM速度并降低对系统I/O性能的影响,pg8.4.1之后为每个数据块文件加了一个后缀为“_vm”的文件(可见性映射表文件,VM文件)。这个文件为每个数据块存储了一个标志位,标记数据块中是否存在要清理的tuple。
VACUUM有两种方式:
vacuum相关的配置:
参数名 | 说明 | 优化思路 |
---|---|---|
autovacuum | 默认为on,表示是否开起autovacuum。当需要冻结xid时,尽管此值为off,PG也会进行vacuum。 | |
autovacuum_naptime | 下一次vacuum的时间,默认1min | 通过缩短实际,调整回收频率,减少每次回收量,可以减小wal压力 |
log_autovacuum_min_duration | 向日志打印autovacuum的统计信息(以及资源消耗),大于阈值,输出这次autovacuum触发的事件的统计信息 。 “-1”表示不记录。“0”表示每次都记录。 | |
autovacuum_max_workers | 最大同时运行的worker数量,不包含launcher本身。 | CPU核多、IO优秀时,当DELETE\UPDATE非常频繁时适量调多点。注意最多可能消耗这么多内存: # autovacuum_max_workers * autovacuum mem(autovacuum_work_mem) |
autovacuum_vacuum_threshold | 默认50。与autovacuum_vacuum_scale_factor(默认值为20%)配合使用。当update,delete的tuples数量超过autovacuum_vacuum_scale_factor*table_size+autovacuum_vacuum_threshold时,进行vacuum。 | 改小可以降低vacuum触发条件,提高vacuum频率 |
autovacuum_analyze_threshold | 默认50。与autovacuum_analyze_scale_factor(默认10%)配合使用。当update,insert,delete的tuples数量超过autovacuum_analyze_scale_factor*table_size+autovacuum_analyze_threshold时,进行analyze。 | 改小可以降低vacuum触发条件,提高vacuum频率 |
autovacuum_freeze_max_age和autovacuum_multixact_freeze_max_age | 前者200 million,后者400 million。离下一次进行xid冻结的最大事务数。 如果表的事务ID年龄大于该值, 即使未开启autovacuum也会强制触发FREEZE,并告警Preventing Transaction ID Wraparound Failures。 | 设置较大值,减少因事务id消耗造成全表扫描的频率。(1000million、1200million) |
autovacuum_vacuum_cost_delay | 如果为-1,取vacuum_cost_delay值。autovacuum触发的vacuum、freeze、analyze的平滑化调度。 | 设置过大,会导致AUTOVACUUM launcher触发的vacuum耗时过长。特别是大表,耗时会非常长,可能导致膨胀等问题。可以调小一点,0. |
autovacuum_vacuum_cost_limit | 如果为-1,到vacuum_cost_limit的值,这个值是所有worker的累加值。 | |
vacuum_freeze_table_age | 当表的年龄大于vacuum_freeze_table_age,则自动转换成vacuum freeze | 调高可以降低vacuum freeze的频率 |
vacuum_multixact_freeze_table_age | 当表的年龄大于autovacuum_freeze_max_age,也会强制触发vacuum freeze | 调高可以降低vacuum freeze的频率 |
Relation: 表(table)或索引(Index)。
Page:磁盘中的数据块 。
Buffer:内存中的数据块。
$PGDATA/base/oid
下的一个子目录。#查看各个数据库的oid
testdb=# select oid, datname from pg_database;
oid | datname
-------+-----------
13025 | postgres
16384 | testdb
1 | template1
13024 | template0
(4 rows)
$PGDATA/base/{dboid}/{relfilenode}
$PGDATA/base/{dboid}
中通常会包含三种文件:文件类型 | 举例 | 说明 |
---|---|---|
表数据及索引文件 | 16385 | 文件最大1GB,超大文件会划分成1GB大小的段:filenode、filenode.1、filenode.2 |
空闲空间映射文件 | 16385_fsm | 每一个堆和索引关系(除了哈希索引)都有一个空闲空间映射(FSM)来保持对关系中可用空间的跟踪。 |
可见性映射文件 | 16385_vm | 提供当前所有page与活动事务可见的元组 |
--查询表所在磁盘的位置
select pg_relation_filepath('表名')
-- out
base/16384/50947
--查看表可见的block
testdb=# SELECT * FROM pg_visibility('testtb2');
blkno | all_visible | all_frozen | pd_all_visible
-------+-------------+------------+----------------
0 | t | f | t
(1 row)
数据块(页)大小为 BLCKSZ,默认 8K,最大为32K。
块的结构:
-- 查询表页数、行数
SELECT relpages as 页个数, reltuples as 行个数 FROM pg_class WHERE relname = '表名'
-- 查询占空间大小(排名前20) - 待考证
SELECT table_schema || '.' || table_name AS table_full_name,
pg_size_pretty(pg_total_relation_size('"' || table_schema || '"."' || table_name || '"')) AS size
FROM information_schema.tables
ORDER BY pg_total_relation_size('"' || table_schema || '"."' || table_name || '"') DESC
limit 20;
pg中元组由三部分组成——元组头结点、空值位图、用户数据。
HeapTupleHeaderData结构:Database Page Layout,pg提供了pageinspect插件,可查看指定表对应的page header内容。
相关字段说明:
字段 | 说明 | 备注 |
---|---|---|
t_xmin | 保存插入该元组的事务txid(该元组由哪个事务插入) | 对应pgclass内置字段xmin |
t_xmax | 保存更新或删除该元组的事务txid。若该元组尚未被删除或更新,则t_xmax=0,即invalid | 对应pgclass内置字段xmax |
t_cid | 保存命令标识(command id,cid),指在该事务中,执行当前命令之前还执行过几条sql命令(从0开始计算) | 对应pgclass内置字段cmin cmax |
t_ctid | 一个指针,保存指向自身或新元组的元组的标识符(tid) | 对应pgclass内置字段ctid |
t_xvac | 存储的是VACUUM FULL 命令的事务ID | |
t_infomask2 | number of attributes, plus various flag bits | |
t_infomask | 各种标志位,标识元组的属性、状态 | 比如:是否具有空属性、是否具有变长的属性、是否包含外部存储的字段、事务提交状态 |
t_hoff | offset to user data,行header的长度 |
使用多版本并发控制(MVCC)比锁定模型(mysql的锁机制)的优势:读锁和写锁不再互斥。
Postgres 有 3 种锁机制:表级锁,行级锁和建议性锁。
表锁由内置的 SQL 命令获得的,同时可以通过锁命令来明确获取。
表锁 | 获得情况 | 冲突对象 | 备注 |
---|---|---|---|
访问共享(ACCESS SHARE) | SELECT读表 | ACCESS EXCLUSIVE | |
行共享(ROW SHARE | SELECT FOR UPDATE 和 SELECT FOR SHARE 读表 | EXCLUSIVE、 ACCESS EXCLUSIVE | |
行独占(ROW EXCLUSIVE) | UPDATE、INSERT 和 DELETE 读表 | SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE | |
访问独占(ACCESS EXCLUSIVE) | ALTER TABLE,DROP TABLE,TRUNCATE,REINDEX,CLUSTER 和 VACUUM FULL 命令读表时获得 | 与所有模式冲突(包括其自身) | LOCK 命令的默认模式。保证事务是可以访问该表的唯一事务。 |
共享更新独占(SHARE UPDATE EXCLUSIVE) | VACUUM(不含FULL),ANALYZE,CREATE INDEX CONCURRENTLY,和一些 ALTER TABLE 的命令获得该锁 | ||
共享(SHARE) | CREATE INDEX 命令在查询中引用的表上获得该锁。 | ||
共享行独占(SHARE ROW EXCLUSIVE) | 不被任何命令隐式获取。 | ||
排他(EXCLUSIVE) | 这个锁模式在事务获得此锁时只允许读取操作并行。它不能由任何命令隐式获取。 |
行级锁不影响对数据的查询,它们只阻塞对同一行的写入。
行锁容易发生死锁。==》 pg自动侦测死锁条件,然后退出其中一个事务从而允许其它事务完成来解决死锁问题。
9.1版本的行锁主要分为两类,后续版本有基于此的扩充,但是大差不差,都是功能的细分。
锁分类 | 获得方式 | 备注 | 版本 |
---|---|---|---|
排他锁 | SELECT FOR UPDATE | 即使没有实际的修改,也会获取到锁 | |
共享锁 | SELECT FOR SHARE | 允许其它事务同时获取共享锁,此时,事务的更新、删除或排他锁都不被允许。 |
用于控制共享缓冲池中表页面的读/写。这些锁在抓取或者更新一行后马上被释放。不需要过多关注
谓词锁定:
禁止当前事务插入另一个并发事务的where条件,直到其它任务提交
--隐患:当前事务的select中的where条件与另一个并发事务的insert内容相同,导致并发事务:A和B的执行顺序不同结果不同。
--事务1
SELECT ... WHERE class = 1;
Insert class = 10 ...
--事务1
SELECT ... WHERE class = 10;
Insert class = 1 ...
如何处理这种情况?
--查死锁
select pid from pg_locks where relation= (
select oid from pg_class where relname='表名'
);
--释放锁
--方式1:
select pg_cancel_backend('上面查询到的pid');
--方式2:
select pg_terminate_backend('上面查询到的pid');
通过show 参数
sql语句可以查看当前设置值,修改后需要重启计算机才可以生效。
参数 | 默认值 | 说明 | 调优 |
---|---|---|---|
deadlock_timeout | 1s | 进行死锁检测之前在一个锁上等待的总时间(以毫秒计)。增加这个值就减少了浪费在无用的死锁检测上的时间,但是减慢了报告真正死锁错误的速度。 | 在一个高负载的服务器上,可能需要增大它。这个值的理想设置应该超过通常的事务时间 |
log_lock_waits | off | 如果一个会话等待某个类型的锁的时间超过deadlock_timeout的值,该参数决定是否在数据库日志中记录这个信息。 | |
max_locks_per_transaction | 64 | 共享锁表跟踪在 max_locks_per_transaction * ( max_connections + max_prepared_transactions ) 个对象(如表)上的锁。因此,在任何一个时刻,只有不超过这么多个可区分对象能够被锁住,超过这个数量会报错。 | 如果想在一个事务中使用很多不同表的查询(例如查询一个有很多子表的父表),则需要提高这个值。 |
max_connections | 151 | 表示允许客户端并发连接的最大数量 | 最小值为1,最大值为100000 |
max_prepared_transactions | 设置可以同时处于prepared状态的事务的最大数目 | 设置为0表示不使用预备事务;否则max_prepared_transactions至少与max_connections一样大,以便每个会话都可以有一个待处理的预备事务。 |
PgSQL · 特性分析 · MVCC机制浅析
PgSQL · 特性分析 · 事务ID回卷问题
PgSQL · 引擎特性 · PostgreSQL Hint Bits 简介
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。