
1,事务原理
事务(transaction):
是用户定义的一组数据库操作,要么全做要么全不做,失败即回滚。
事务是恢复和并发控制的基本单元。
保存点(savePoint)
在一个大的事务中,可以把操作过程分成几个部分,第一个部分执行成功后可以建一个保存点,若后面的部分执行失败,则回滚到此保存点,而不必回滚整个事务。
事务的实现即:RDBMS采取何种技术确保事务的ACID特性?
回退(rollback):
撤销sql执行过程。事务管理可以管理insert、update、delete语句;不能回退create、drop操作。
RDBMS(Relational Database Management System,关系数据库管理系统)
是指包括相互联系的逻辑组织和存取这些数据的一套程序 (数据库管理系统软件)。
关系数据库管理系统就是管理关系数据库,并将数据逻辑组织的系统。
1)事务特性(ACID)
1>原子性(Atomic)
事务是数据库的逻辑工作单位。要么都做,要么都不做。==》通过MVCC保证
2> 一致性(Consistency)==》最终目的
事务完成时,数据必须处于一致状态,数据的完整性约束没有被破坏,事务在执行过程中发生错误,会被回滚到事务开始前的状态。
3>隔离性(Isolation)
事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。==》用锁和MVCC来保证。
读读不存在并发问题;
读写通过MVCC来解决并发问题;
写写通过加锁来解决并发问题。
4>永久性(Durability)
事务一旦提交,所做修改会永久的保存在数据库中。==》保证了db的可靠性,用WAL日志来实现
其中一致性是事务的最终目的,为了达到一致性需要保证原子性、隔离性、永久性。
那么pg是怎么完成ACID的呢?
2)pg隔离级别
在标准SQL规范中,定义了4个事务隔离级别(由低到高):
| 读未提交(RU级别、read uncommitted) | 读已提交(read committed) | 可重复读(repeatable read) | 序列化(serializable) | |
|---|---|---|---|---|
| 允许操作 | 允许事务读取未被其他事务提交的变更 | 允许事务读取已经被其他事务提交的变更 | 事务读取数据时,禁止其他事务对这个字段进行更新 | 所有事务都一个接一个地串行执行 |
| 可能存在问题 | 脏读、不可重复读、幻读 | 不可重复读、幻读 | 幻读==》添加间隙锁解决此类问题 | 数据安全。但是添加大量行锁会导致大量超时和锁竞争问题。 |
| 避免问题 | ||||
| 举例 | 事务读到了其他事务未提交的数据。其他事务回滚导致脏读。 | 同一个事务读了两次数据,分别读取了其他事务提交的内容,但是两次结果不一致。这就是不可重复读。 | 事务读了两次数据,不管数据怎么修改,都只读第一次的数据。==》导致幻读:每次select时,mvcc的read view不会变化。但是其他事务做了新增操作,真实的数据和当前的read view不同。 | |
| 数据库 | oracle、pg默认隔离级别 | mysql默认隔离级别 |
pg仅支持2种隔离级别:读已提交(默认)、可串行化。
事务隔离级别的实现:
- 读未提交/读已提交:每个query都会获取最新的快照CurrentSnapshotData
- 重复读:所有的query 获取相同的快照都为第1个query获取的快照FirstXactSnapshot
- 串行化:使用锁系统来实现
1>读已提交隔离级别(默认)
每个命令都是从一个新的快照开始执行的。
- 当前事务看不见其它未提交事务的数据;
- 当前事务可以看到自己未提交的数据;
- 一个事务里两个select,两次select读到的数据可能不是同一个快照。第二次select的时候会看到其它事务已提交的数据。
- 同一个事务中,第一次更新之后,其它事务获得锁进行数据的更新,当前事务中的更新操作需要等到其它事务结束或者回滚再进行操作。
为什么使用RC级别而不使用RR级别?
提高并发度并降低死锁概率。
2>可串行化隔离级别(开销大)
每个命令都是从事务开始时的快照开始执行的。
- 当前事务看不见其它未提交事务的数据;
- 当前事务可以看到自己未提交的数据;
- 只读事务不存在冲突:一个事务里两个select,两次select读到的数据一致。
- 串行化冲突:更新事务与其它更新事务冲突需要重试。
同一个事务中,第一次更新之后,其它事务获得锁进行数据的更新,当前事务中的更新操作需要等到其它事务结束或者回滚再进行操作。如果其它事务回滚,当前事务正常进行;如果其它事务提交,那么当前事务回滚(ERROR: could not serialize access due to concurrent update),需要重试。
3)多版本并发控制(MVCC,Multi-Version Concurrency Control)
MVCC是数据库并发访问时,保证数据一致性的一种方法。实现MVCC的方法有以下两种:
- 写新数据时,把原数据移到一个单独的位置,如回滚段中,其它用户读数据时,从回滚段中把原数据读出来。(Oracle和Mysql数据库中的InnoDB引擎使用这种方法)
- 写新数据时,原数据不删除,而是把新数据插入进来。(pg使用这种方法)==》相当于每个事务看到的都是之前一小段时间的数据快照(某一个数据库版本)。
1>原子性保证
事务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指向更新后的元组,形成了版本链。 |
- insert tuple:
xmin=事务id xmax=0 ctid=(0,1),指向当前元组 - delete tuple:
xmax=事务id - update tuple,先delete,再insert:
tupleOld的xmax=事务id ctid指向新的元组,tupleNew的xmin=事务id ctid指向当前元组
原子性:通过当前事务id对tuple进行标记,不管是commit还是rollback操作都可以通过xmin和xmax保证事务的原子性。
2>事务隔离性保证
a)不同事务的可见性: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,对于当前元组数据:
- t_xmin<s_xmin && t_xmax == 0,元组插入且事务已提交,可见;
- t_xmin<s_xmin && t_xmax !=0 && t_xmax <s_xmin,元组已删除,不可见;
- t_xmin<s_xmin && t_xmax !=0 && t_xmax >s_xmax,元组删除但未提交,可见;
- 其它:需要结合clog进行判断。
b)同一事务的可见性:cmin cmax
cmin、cmax 用于同一个事务中实现版本可见性判断
3>事务持久性保证
a)clog(commit log)日志
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返回所请求事务状态。
b)Hint Bits
判断元组的可见性非常频繁,每次从缓存或者磁盘读取clog信息依然不够高效,引入了Hint Bits概念。t_informask中存储的一些标志位保存了插入/删除该元组的事务的状态。
元组中的 Hint Bits采用延迟更新策略,并不会在事务提交或者回滚时主动更新所有操作过的元组Hint Bits。
等到第一次访问(可能是VACUUM,DML或SELECT)该元组并进行可见性判断时:
- 如果Hint Bits已设置,直接读取Hint Bits的值。
- 如果Hint Bits未设置,则调用函数从CLOG中读取事务状态。如果事务状态为COMMITTED或ABORTED,则将Hint Bits设置到元组的t_informask字段。如果事务状态为INPROCESS,由于其状态还未到达终态,无需设置Hint Bits。
4>MVCC的优缺点
pg在事务提交前,只需要访问原来的数据;提交后,系统更新元组的存储标识,直到Vaccum进程回收为止。
相比InnoDB和Oracle,pg多版本优势在于:
- 事务回滚可以立即完成;
- 数据可以进行很多更新,不必像Oracle和InnoDB那样需要经常保证回滚段不会被用完,也不会像Oracle数据库那样,经常遇到ORA-1555错误的困扰。
劣势在于:
- 旧数据需要Vaccum清理。
- 旧版本数据的存在降低查询速率,需要扫描更多的数据块。
4)表膨胀问题
1>Visibility Map机制:
官方文档:Routine Vacuuming
Visibility Map中标记了哪些page中是没有dead tuple的,数据量很小可以cache到内存中。这有两个好处:
- 当vacuum时,可以直接跳过这些page
- 进行index-only scan时,可以先检查下Visibility Map。这样减少fetch tuple时的可见性判断,从而减少IO操作,提高性能
2>vacuum(表空间优化、收缩表)
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;
5)事务id回卷
1>系统预留事务ID:0 1 2
0 1 2是系统预留ID,这三个ID比任何普通xid都要旧。
- InvalidTransactionId=0 无效的事务ID;表示还未分配事务ID。
- BootstrapTransactionId=1 表示系统表初始化时的事务ID;表示Initdb服务正在初始化系统表。
- FrozenTransactionId=2 冻结的事务ID。

最低0.47元/天 解锁文章
491

被折叠的 条评论
为什么被折叠?



