本篇以Postgresql为例,探讨数据库的事务、并发控制和锁机制。
ACID
在关系型数据库中,一个事务必须具备以下特性,简称ACID:
在Postgresql中使用多版本并发控制(MVCC)来维护数据的一致性。相较于锁定模型,MVCC的主要优点是在MVCC里对读数据的锁请求与写数据的锁请求不冲突,读不会阻塞写,而写也从不阻塞读。
此外,PG与其他数据库最大的区别在于,在PG中大多数DDL可以包含在一个事务中,并支持回滚;此功能使得PG尤其适合作为sharding分布式数据库系统中的底层数据库。
事务隔离级别
数据库中存在以下四种隔离级别:
read uncommitted: 读未提交
read committed :读已提交(PostgreSQL中的默认隔离级别)
repeatable read: 重复读
serializable: 串行化
隔离级别 | 脏读 | 不可重复读 | 幻读 | 序列化异常 |
read uncommitted | 允许,但不在PG中 | 可能 | 可能 | 可能 |
read committed | 不可能 | 可能 | 可能 | 可能 |
repeatable read | 不可能 | 不可能 | 允许,但不在PG中 | 可能 |
serializable | 不可能 | 不可能 | 不可能 | 不可能 |
概念解释:
脏读
一个事务读取了另一个并行未提交事务写入的数据。
不可重复读
一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提交)修改。主要针对update
begin; select name from tab1 where id=111; #得到name是“张三” #此时另外一个事务对id=111的name更新成了“李四” select name from tab1 where id=111; #得到name是“李四”
幻读
一个事务重新执行一个返回符合一个搜索条件的行集合的查询, 发现满足条件的行集合因为另一个最近提交的事务而发生了改变。主要针对insert 、delete
begin; select name from tab1 where id=111; #得到name是“张三” #此时另外一个事务对id=111的记录增加一条“李四” select name from tab1 where id=111; #得到name是“李四”、“张三”
序列化异常
成功提交一组事务的结果与这些事务所有可能的串行执行结果都不一致。
在PostgreSQL中,你可以请求四种标准事务隔离级别中的任意一种,但是内部只实现了三种不同的隔离级别,即 PostgreSQL 的读未提交模式的行为和读已提交相同。这是因为把标准隔离级别映射到 PostgreSQL 的多版本并发控制架构的唯一合理的方法。
探讨:
从上面可以看出不可重复读和幻读的区别在于:
不可重复读重点在于更新,在数据库控制方面只需要锁住满足条件的记录(可以理解为行锁);
幻读重点在于删除和插入,在数据库控制方面需要锁住满足条件及其相近的记录(可以理解为表锁);
如果使用锁机制来实现这两种隔离级别,在可重复读中,该SQL第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。(悲观锁机制)
因此可以推断出不可重复读和幻读的最大区别在于数据库采用何种锁机制来解决他们产生的问题;
在PG中(mysql、oracle也是)为了更高的性能,乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。
悲观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处 于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机 制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
乐观锁
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如 果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
两阶段提交
两阶段提交是分布式系统中保持事务原子性的关键
Postgresql中两段式提交步骤如下:
<第一阶段>
(1)应用程序先调用各台数据库做一些操作,但不提交事务;然后调用事务协调器(这个协调器可能由应用自己实现)中的提交方法。
(2)事务协调器将联络事务中涉及的每台数据库,并通知它们准备提交事务,这是第一阶段的开始,PG中一般调用“PREAPARE TRANSACTION”命令。
(3)各台数据库接受到“PREPARE TRANSACTION”命令后,如果要返回成功,则数据库必须将自己置于以下状态:确保后续能在被要求提交事务时提交事务,或者在被要求回滚事务时能够回滚。因此PG会将已经准备好提交的信息写入持久存储区中。如果数据库无法完成此事务,它会直接返回失败给事务协调器
(4)事务协调器接收到所有数据库的响应
<第二阶段>
如果任一数据库在第一阶段返回失败,则事务协调器将会发出“ROLLBACK RREPARED”命令给各个数据库进行回滚;如果所有数据库的响应都是成功,则发送“COMMIT PREPARED”命令进行提交。
注:在实际操作中需要将PG的参数“max_prepared_transactions”设置为一个大于零的数字,否则会报错。
MVCC
MVCC是为了解决读写并发时数据不一致的问题,MVCC的方法是写数据时,旧的版本数据不删除,并发的读还能读到旧版本的数据,这样就避免了数据不一致。
实现MVCC的方法有两种:
Postgresql中使用的是第二种,而oracle和mysql中的innodb引擎使用的是第一种。
Postgresql中MVCC的实现
为了实现MVCC,每张表上都添加了四个系统字段:xmin、xmax、cmin、cmax
xmin:标记插入该行数据的事务ID。
xmax:标记删除该行数据的事务ID。
cmin:事务内部插入类操作的命令ID。
cmax:事务内部删除类操作的命令ID。
PG中的MVCC实现过程
PostgreSQL把事务状态记录在commit log(clog)中,事务的状态有以下四种。
事务ID在PG中用xid表示,是一个32bit的数字,有以下三个特殊的事务ID给系统内部使用:
事务ID会一直递增,当达到最大值后再从头开始,此时就会遇到事务ID回卷的问题。在PG中当事务ID达到2^31时,旧的事务ID就会变成一个特殊的事务ID,即FrozenTransactionId;当正常的事务ID与冻结的事务ID进行对比时,会认为正常事务ID比冻结事务ID新。
参考文献
Postgresql从小工到专家
原文:https://www.cnblogs.com/duanleiblog/p/13815534.html