在《第三代分布式数据库(3)——一致性八仙图》中,对于PostgreSQL描述如下:
PostgreSQL基于SSI算法实现了可串行化隔离级别,消除了一致性八仙图中的Write Skew、Write Skew Committed和Step IAT三类数据异常(请注意,SSI消除的不仅仅是Write Skew)。但是,SSI算法对于PostgreSQL而言,却是多余的;PostgreSQL为实现可串行化化花费了巨大的力气却做了多余的无用功,非常可惜,这是数据库引擎研发人员的认知错误造成的,有效得展现了传统的事务知识认知的局限性。——这也是一个有趣的现象,背后的“洞察”亦非常深刻,后续单独成文讨论。
说PostgreSQL“比较拉胯”主要是从多余的SSI算法的角度说起的,SSI算法犹如画蛇添足,大幅度地增加了PostgreSQL代码的复杂性,但使得PostgreSQL的性能腰斩。这不是说可串行化隔离级别不重要,而是说没有必要采用SSI算法去实现。——究其根本原因,是对并发算法的本质不了解所致。
另外,MySQL和PostgreSQL的死锁检测算法,实现的都不好,甚至PostgreSQL弱于MySQL的实现,这点也使得PostgreSQL“比较拉胯”。
而基于PostgreSQL的Greenplum,则更为拉胯,尤其是分布式死锁检测的实现方式,同其他多数分布式数据库一样,更是不堪入目,使得该分布式下事务处理能力几乎不可用。Sigmod 2021有一篇论文,题为《Greenplum: A Hybrid Database for Transactional and Analytical Workloads》。该文在Greenplum分布式数据库内,提及了两个死锁问题,这两个死锁都是在并发事务执行的时候发生的分布式死锁。Greenplum用不优雅的、且非常差的算法解决了这样的数据异常,但高并发下性能堪忧,只能远离TP型应用、让自己不得不去向AP型应用靠拢。拉胯得非常可惜(不是没救,是本来有更好的算法可用的,欢迎来讨论)。
但是,在一致性八仙图所比较的数据库中,PostgreSQL算是王者。更好地有一个,是CockroachDB,作为一个分布式数据库,CockroachDB在事务处理方面的技术复杂度更高,而在分布式环境下实现可串行化更加富有挑战。其他分布式数据库则显得技术能力较弱。
如果有机会能测试、对比一下CockroachDB、TDSQL、OceanBase、TiDB、Greenplum的性能,再结合一致性八仙图,则可更加容易得出这几个分布式数据库的并发算法的优劣,期待后续有机会做一下这项工作,更欢迎读友能推荐相关测试数据。
PostgreSQL的数据正确性,是通过完整性约束(暂且不谈,后续另文讨论)和并发访问控制算法确保的。其中,并发算法的实现方式如下:
1.1 PostgreSQL实现可串行化隔离级别时,采用了SSI算法,该算法为事务加软读锁,使得构成的RW偏序关系能够被注册到一个有向图中(本质是一个只有RW冲突关系的等待图,PostgreSQL中定义为RWConflictPool的一个内存结构),之后会遍历该有向图(RWConflictPool),发现是否存在两个连续的RW,如果存在则回滚(与环检测算法的差异为:在该有向图中没有查找环是否存在,而是只找是否存在2两个连续的RW偏序关系)。
为了实现可串行化隔离级别而采用SSI算法,PostgreSQL可谓大费周章,定义了很多数据结构,具体可参考《数据库事务处理的艺术:事务管理与并发控制》一书的9第九章有详细介绍;其实,SSI算法是一个鸡肋,在PostgreSQL中不用此法亦可简易实现可串行化。
1.2 实现可串行化和可重复读这两个隔离级别,PostgreSQL和MySQL都会建立快照,但差别巨大:PostgreSQL没有实现SS2PL算法读操作不加锁,导致在死锁检测的范围内没有RW和WR偏序对,读操作受快照影响巨大;而MySQL则相反,因为实现了SS2PL算法读操作在可串行化隔离级别下加锁,所以快照被SS2PL算法掩盖(似乎是快照没有起作用或没有建立快照,但实际上会创建快照并让快照发挥作用),这个差异使得PostgreSQL的隔离级别的实现采用“层进式”的实现方式(“层进式”含义,参考《第三代分布式数据库(6)——没有价值的隔离级别》)。
1.3 实现读已提交隔离级别,PostgreSQL没有做任何特殊操作,只是把隔离级别的默认值设置为了XACT_READ_COMMITTED(读取数据时,除了本事务修改的数据可被本事务读取外,默认都是读取的已经提交的数据,该方式没有和读已提交隔离级别挂钩,即“读已提交”成为一个所有隔离级别都适用的规则,这使得PostgreSQL的RC和RU是一样的结果);
1.4 对于所有隔离级别,写操作都是要施加锁的,这是2PL算法中的一部分,和隔离级别“无关”;
1.5 封锁的作用,不只是让锁起到用户进程互斥的作用,同时构建waitOrders链表,标识出线程之间的等待关系,之后死锁检测将调用DeadLockCheck()函数从该链表中检测是否存在环。——同样是环检测(SSI算法对于RWConflictPool的检测、2PL算法对于waitOrders的检测),PostgreSQL构造了两套相似的数据结构,也启动了两套相似的遍历过程,耗费了内存和CPU资源,且SSI算法机制复杂、代码较多对系统的不良影响很大。这即是SSI算法为鸡肋之所在。
PostgreSQL对快照建立细分了多种场景,然后分别进行了优化,例如对于只读的事务在建立快照的时候进行优化,对于可串行化隔离级别也进行了优化:
2.1 这可以消除Dirty Read、Non-repeatable Read异常;也可消除幻读等异常。类似的还有Non-repeatable Reads、Intermediate Reads、Intermediate Reads Committed、Phantom、Read Skew、Predicated-based Read Skew、Read Skew 2等。可见采用快照的方式,PostgreSQL消除了较多的数据异常——MySQL却不能做到此点。
2.2 由于读操作不加锁(无论是表级锁还是行级锁,都不为读操作加锁),因此类似WR的偏序对,是通过快照来避免的(而不是死锁检测)。即能把WiRj冲突通过快照变为RjWi(快照创建在Wi操作之前,因此把WiRj变为了RjWi)类的冲突关系,可消除一部分WAT类异常,如Non-repeatable Read Committed在可串行化隔离级别下不发生(只一份快照),在读已提交隔离级别下却发生(用了两个快照)。MySQL的实现却不这样(创建了快照但又没有让快照发挥作用,优先了让SS2PL起作用,这点实现是混用多个并发算法需要特别注意的点),《第三代分布式数据库(5)——一无是处MySQL》讨论了MySQL的实现方式(采用了耗时的死锁检测),此点使得PostgreSQL占优。
2.3 快照对可串行化和可重复读隔离级别均有效,这使得快照可以消除的数据异常在可串行化隔离级别下依然能用与可重复读隔离级别相同的方式加以消除,从而使得SSI算法只集中在寻找RW关系上,从而有助于提高可串行化隔离级别下的SSI算法性能。
PostgreSQL没有直接禁止脏读的WW冲突发生(即没有采用2PL算法的No_wait策略,而是采用了Wait_die策略),而是允许WW冲突发生,此时,采用环检测的方式,构造等待图,消除诸如Dirty Write、Lost Update、Full-write Skew等类型的数据异常。
3.1 特例规则:对于Dirty Write、Lost Update、Lost Update Committed、Read-write Skew 1、Read-write Skew 2等,会采用特定的规则加以优化,此时PostgreSQL报告“ERROR: could not serialize access due to concurrent update”;即没有采用死锁检测的方式加以消除这些数据异常;但这几个数据异常只有在可重复读和可串行化隔离级别下被采用这样的方式消除。这点与MySQL不同。
3.2 PostgreSQL采用等待图实现死锁检测和消除诸如Full-write Skew、Full-write Skew Committed、Multi-edge WAT等异常;采用SSI消除Write Skew等异常;那么,请思考:PostgreSQL构造了两份等待图,分别实现不同类型的数据异常的检测,性能会何其低下?——说其拉胯,一点也不怨啊!
PostgreSQL处理幻读类的数据异常,是采用快照的方式加以消除的。
PostgreSQL提供两种加锁方式,一是常规符合2PL算法的加锁机制,一是SSI算法中的谓词锁。
在2PL算法背景下(非SS2PL算法,因为PostgreSQL对元组不加锁),对一个表或索引加锁:LockRelationOid()->LockAcquireExtended()->GrantLock(),加锁时指定锁的模式(lockmode),而lockmode的值源自对SQL语句解析而来的内容,例如一个SELECT语句在transformTableEntry()->addRangeTableEntry()时会通过LockRelationOid()调用LockAcquireExtended()在表级获取锁。例如SELECT语句中增加FOR UPDATE子句则会在如下代码中选择RowShareLock锁(题外话,该锁会转化为元组上的排它锁,严重影响性能;而且即使执行UPDATE语句也未必会在元组上施加排它锁,从代码角度可见SELECT语句中增加FOR UPDATE子句对数据库的性能影响会有多大,可见用户主动加锁的方式多么不可取)。
lockmode = isLockedRefname(pstate, refname) ? RowShareLock: AccessShareLock;rel = parserOpenTable(pstate, relation, lockmode);
在SSI算法范围内,对一个元组施加谓词锁,例如通过索引(IndexNext())获取元组时,会在元组一层施加谓词锁(PredicateLockTID()->PredicateLockAcquire())。
根据2PL算法,加锁的锁模式,是根据SQL语句确定的。如上述SELECT执行示例,会施加RowShareLock或AccessShareLock锁;DML则通过setTargetTable()施加诸如RowExclusiveLock排它锁与表/索引上。
而真正施加锁,是发生在打开表/索引的过程中,创建对应的锁并注册到锁表内。
是否施加行锁,可通过观察如下枚举值的使用而了解,或者查阅select_rowmark_type()函数。
typedef enum RowMarkType{ROW_MARK_EXCLUSIVE, /* obtain exclusive tuplelock */ROW_MARK_NOKEYEXCLUSIVE, /* obtain no-key exclusive tuple lock */ROW_MARK_SHARE, /* obtain sharedtuple lock */ROW_MARK_KEYSHARE, /* obtain keyshare tuplelock */ROW_MARK_REFERENCE, /* just fetch the TID,don't lock it */ROW_MARK_COPY /* physically copythe row value */} RowMarkType;
谓词锁,只在可串行化隔离级别下被施加。其发生在获取数据项的过程中。在执行阶段,也会尝试获取谓词锁,但发现之前已经获取到则不重复执行执行获取谓词锁的PredicateLockAcquire()函数。
PredicateLockAcquire()PredicateLockTID() //元组上施加谓词锁heap_hot_search_buffer()heapam_index_fetch_tuple()table_index_fetch_tuple()index_fetch_heap()index_getnext_slot()indexNext()ExecScanFetch()
在2PL算法背景下,PostgreSQL的死锁检测,在LockAcquireExtended()函数中进行冲突判断,如果存在写写冲突则调用WaitOnLock()->ProcSleep()->CheckDeadLock()进行死锁检测。
在可串行化隔离级别下,SSI算法进行两个RW的判断,发生在每个读写操作发生之时。当读操作进行时,检测是否存在与该读操作对应构成的RW(相同变量上);当写操作进行时,检测是否存在与该写操作对应构成的RW。如果有RW存在,则进一步检测是否有连续2个RW,有则回滚某个事务。
PostgreSQL采用快照,有助于带有较多的读操作的事务提高性能,这是因为读操作没有被锁阻塞。但这仅仅是理论层面的分析结论,在实践中该结果同MySQL一样,尚需用真实场景检测(可惜现有的benchmark鲜有能测试到这样的重要细节之处)。
为什么说在实践中需要检验真实效果呢?这是因为:快照的创建和使用,均有较大的代价。有的数据库如MySQL和PostgreSQL,采用活跃事务列表作为快照,而并发量很高、每个数据项存在较多版本的情况下,活跃事务列表会很大,导致其遍历速度会变慢,而且每个事务因冲突判断有多次遍历活跃事务的可能(创建快照时对其他事务造成的阻塞影响、尤其是冲突判断如果在每个操作执行时都进行的话),因此其代价可能会超过单纯采用SS2PL算法写阻塞读的代价,因此需要在实践中权衡,更需要在内核的设计中针对特定场景镜像设计和权衡。
PostgreSQL对于snapshot的创建,与隔离级别有密切关系:对于可重复读和可串行化隔离级别,无论事务中有多少条SELECT语句,快照只创建一次;而读已提交隔离级别,快照会随着SELECT语句的个数创建多次,这对并发性能的影响会较大。其具体实现的代码调用栈如下。
GetSnapshotData(SnapshotData* snapshot)GetTransactionSnapshot()exec_simple_query(constchar * query_string)PostgresMain(const char * dbname, constchar * username)或者:GetSnapshotData(SnapshotData * snapshot)GetSerializableTransactionSnapshotInt(SnapshotData* snapshot, …)GetSerializableTransactionSnapshot(SnapshotData * snapshot)GetTransactionSnapshot()exec_simple_query(const char * query_string)PostgresMain(const char * dbname, const char * username)
如下多数示例,是在可串行化隔离级别下测试验证,在每个客户端执行如下命令:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;SHOW transaction_isolation;
PostgreSQL只会对于Full Write Skew、Full Write Skew Committed、Step WAT等少数几个数据异常采用死锁检测的方式加以消除(报告“Deadlock found when trying to get lock”错误提示)。这一点非常不同于MySQL、SQL Server等。从并发算法的角度看,这点是优于其他数据库的地方,这样做会减少计算资源的消耗,提高系统并发性能。
表1 PostgreSQL对于Full Write Skew的执行过程(可串行化隔离级别)
时间 |
T1 |
T2 |
说明 |
t0 |
W1[x1] |
事务T1将施加两种锁,一是表/索引/Page上加RowExclusiveLock(读已提交隔离级别不施加该锁,且注意不是在元组上锁);二是在变量x所在表/索引/Page/元组上进行施加谓词的软写锁(注意软写锁不是2PL技术中的互斥写锁的写锁,只有可串行化隔离级别下施加该锁,其作用是用于在SSI算法的有向图中构造RW构成的偏序对,从辅助构成偏序对的角度看,软写锁的作用可等效于2PL中的写锁;谓词锁加锁对象是在元组还是页面还是表等对象上,与具体的SQL和优化器优化后的扫描方式有关,后不再赘述) |
|
t1 |
W2[y1] |
事务T2在变量y所在的表/索引/Page上施加RowExclusiveLock锁、在y所在元组上施加谓词软写锁; 注意此时会进行读写冲突的检查,耗费计算资源 |
|
t2 |
W2[x2] |
事务T2在变量x所在的表/索引/Page上施加RowExclusiveLock锁、在x所在的元组上施加谓词软写锁。在变量x上构成WW偏序对,事务T2会处于等待状态,此时会进行死锁检测、但因不存在死锁而不报告“deadlock detected” |
|
t3 |
W1[y2] |
事务T1在变量x所在表/索引/Page上施加RowExclusiveLock锁、在x所在元组上施加谓词软写锁;在变量y上构成WW偏序对,事务T1会处于等待状态,此时会进行死锁检测、存在死锁而报告“deadlock detected” |
对于Write Skew、Write Skew Committed、Double-write Skew 1 Committed、Write-read Skew、Write-read Skew Committed、Step IAT、Step RAT几类数据异常,PostgreSQL采用了SSI算法。
SSI原理是创建一个有向图,在该有向图中存储读操作所加的软锁、写操作所加的写锁到相应事务中,给并发事务赋予入边(写操作)和出边(读操作),当两个失误之间的出边和入边构成RW偏序对且有向图中存在两个RW偏序对时,即回滚一个事务(可能触发不必要的回滚)以确保消除Write Skew、Write Skew Commotted、Step IAT(这几个异常的特征是至少存在两个RW偏序对)。
在可串行化隔离级别下,写操作发生时,总需要做SSI规则的检测(CheckForSerializableConflictOut),效率会十分低下。
从一致性八仙图上看,能看到PostgreSQL有着大量的“蓝色R”和“绿色P”,表明PostgreSQL的死锁检测远少于其他数据库,直观上就能感知PostgreSQL的并发访问控制技术更强。
采用规则方式消除数据异常,是PostgreSQL的强项,在效率方面强于耗时的死锁检测(规则用一些简洁的判断即可完成,而死锁检测需要构造有向图、遍历有向图)。PostgreSQL提供了多种规则的方式来消除其余的数据异常,值得其他数据库学习。
3.1 “快照避免读类型异常”规则
用快照来避免数据异常,PostgreSQL强于MySQL。
例如,一些和读相关的异常,如Dirty Read、Intermediate Read、Non-repeatable Read、Read Skew(详细示例如表2)等异常,快照能够避免这些异常发生。
表2 PostgreSQL对于Read Skew的执行过程(可串行化隔离级别)
时间 |
T1 |
T2 |
说明 |
t0 |
R1[x0] |
事务T1在变量x上进行读操作,将施加两种锁,一是在x所在表/索引/Page上施加AccessShareLock,二是在x所在表/索引/Page上施加谓词的软读锁(注意软读锁不是2PL技术中的互斥写锁的读锁,只有可串行化隔离级别下施加该锁,其作用是用于在SSI算法的有向图中构造RW构成的偏序对,从辅助构成偏序对的角度看,软读锁的作用可等效于2PL中的读锁) |
|
t1 |
W2[y1] |
事务T2在变量y所在表/索引/Page上施加RowExclusiveLock锁(但也会施加谓词软写锁) |
|
t2 |
W2[x1] |
事务T2在变量x所在表/索引/Page上能得到RowExclusiveLock锁;但也会在x所在元组上施加谓词软写锁,这将在变量x上形成T1->T2的偏序对RW;之后做RW冲突检测(CheckForSerializableConflictIn,空白耗费计算元),不满足死锁检测条件,T2无处于等待的现象 |
|
t3 |
R1[y] |
事务T1在变量x所在表/索引/Page上进行读操作,将施加两种锁,一是AccessShareLock、二是在x所在表/索引/Page上谓词的软读锁,会在变量y上形成T2->T1的偏序对WR,但不符合SSI条件。 需要注意的是,在可串行化隔离级别下,事务T1使用同一个快照,则t3时刻R1[y]读到的应该是一个早于t1时刻事务T2写的y1的旧值,此时如基于时钟顺序本应可理解为偏序对应为R1W2[y]才对,这正是快照发挥价值之处,但PostgreSQL没这样处理,即没有视之为RW,这使得快照的本意被“违背”,但事实上事务T1在t3时刻读到的是一个旧值又符合快照本意(需要注意这种细微差异) |
3.2 “读已提交”规则
PostgreSQL的RU和RC隔离级别是一样的结果,其实这是因为PostgreSQL没有区分RC和RU,这主要是基于(Dirty Write除外,PostgreSQL和MySQL一样,在此点上都没有遵照ANSI/ISO-SQL标准标准)。
例如,PostgreSQL对于Non-repeatable Read和Non-repeatable Read Committed在RC隔离级别下的处理是存在差异的,这就是读已提交规则所起的作用。如表3和表4所示。
表3 PostgreSQL对于Non-repeatable Read的执行过程(RC)
时间 |
T1 |
T2 |
说明 |
t0 |
R1[x] |
事务T1在变量x所在表/索引/Page上施加互斥锁 |
|
t1 |
W2[x] |
事务T2在变量x所在表/索引/Page上获得写锁(更新成功),这在变量x上形成T1->T2的偏序对RW;没有触发死锁检测 |
|
t2 |
R1[x] |
事务T1在变量x上持有锁 |
|
t3 |
COMMIT |
事务T1提交成功,但没有读到事务T2所写的数据 |
表4 PostgreSQL对于Non-repeatable Read Committed的执行过程(RC)
时间 |
T1 |
T2 |
说明 |
t0 |
R1[x] |
事务T1在变量x所在表/索引/Page上施加互斥锁 |
|
t1 |
W2[x] |
事务T2在变量x所在表/索引/Page上获得写锁(更新成功),这在变量x上形成T1->T2的偏序对RW;没有触发死锁检测 |
|
t2 |
COMMIT |
先提交者获胜,事务T2被允许提交;事务T2提交成功后释放锁 |
|
t3 |
R1[x] |
事务T1在变量x所在表/索引/Page上持有锁,读已提交规则允许读到最新的写值,所以读到了事务T2所写的值 |
|
t4 |
COMMIT |
事务T1提交成功 |
3.3 “写写互斥”和“不可串行化访问”规则
对于WW类的偏序对,因为写锁与写锁互斥,所以后获取写锁的事务将被阻塞,因此先获取锁的事务一致被执行,因而没有受到其他并发事务的影响,所以不会形成环,因而也不会触发死锁检测。因此,“写写互斥”为一条并发规则,这隶属于封锁技术的范围。
“写写互斥”在PostgreSQL中又可以细化为两种情况:
一是RC隔离级别下,单纯的写写互斥,使得第二个并发事务被阻塞而不能完成写操作,致使该类数据异常不能真正发生。如Dirty Write、Full Write、Full Write Committed等存在WW偏序对的SDA类的一些数据异常,PostgreSQL在一致性八仙图中显示的结果是“绿色的P”,这表示其采用单纯的写写互斥规则。
二是SER和RR隔离级别下,尽管存在“写写互斥”规则,但是PostgreSQL又进一步进行了严格判断。该严格判断,又是一条规则,“不可串行化访问”规则,表现为PostgreSQL会报告“ERROR: could not serialize access due to concurrent update”错误。在一致性八仙图中显示的结果是“蓝色的R”,这表示其采用“不可串行化访问”规则加以消除数据异常。这包括Lost Update、Lost Update Committed、Lost Self Update、Lost Self Update Committed、Read-write Skew 1、Read-write Skew 1 Committed、Read-write Skew 2、Read-write Skew 2 Committed、Double-write Skew 1、Double-write Skew 2、Double-write Skew 2 Committed、Dirty Write、Full Write、Full Write Committed。这么多数据异常用规则直接加以消除,其效率远高于采用封锁的方式。
4. 出现数据不一致的现象
PostgreSQL在可重复读隔离级别下,不能处理Write Skew等,其过程如表5。
表5 PostgreSQL对于Write Skew的执行过程(可重复读)
时间 |
T1 |
T2 |
说明 |
t0 |
R1[x0] |
事务T1将施加两种锁,一是在变量x所在表/索引/Page上进行读操作AccessShareLock(读已提交隔离级别不施加该锁),二是x所在表/Page/元组上施加谓词的软读锁 |
|
t1 |
R2[y0] |
事务T2将施加两种锁,一是在变量y上所在表/索引/Page进行读操作AccessShareLock(读已提交隔离级别不施加该锁),二是y所在表/Page/元组上施加谓词的软读锁 |
|
t2 |
COMMIT |
先提交者获胜,事务T2被允许提交;事务T2提交成功后释放锁 |
|
t3 |
R1[x1] |
事务T1在变量x所在表/索引/Page/Page上持有锁,使用与t1时刻不同的快照,但在读已提交规则作用下,读到T2事务所写过的变量值x1,形成了数据异常 |
|
t4 |
COMMIT |
事务T1提交成功,但数据异常发生 |
如下是在表或元组上加不同类型的锁的一个例子(SIReadLock锁施加在元组和页面上):
relname | locktype | database | relation | page | tuple | virtualtransaction | pid | mode | granted------------+----------+----------+----------+------+-------+--------------------+-------+-----------------+--------parent_pkey | relation | 12401 | 16392 | | | 2/39 | 12028 | AccessShareLock | tparent| relation | 12401 | 16389 | | | 2/39 | 12028 | RowShareLock | tparent| tuple | 12401 | 16389 | 0 | 9 |2/39 | 12028 |SIReadLock | tparent_pkey | page | 12401 | 16392 | 1 | | 2/39 | 12028 |SIReadLock | t
从如上分析可以看到,采用死锁检测的数据异常个数较少,大部分数据异常被各种规则加以消除,这是PostgreSQL比诸如MySQL强悍之处,意味着从并发理论性能优于MySQL。
另外,对于谓词读锁和写锁,这是一种逻辑上的称呼方式便于理解,注意在代码实现的时候,是有细节需要注意的,这可以参考Write-read Skew Committeed。
Write-read Skew Committeed,其形式化表达式为:
W1[x1] W2[y1] R2[x] C2 W1[y2]
在分析该异常的时候,需要从几个层面来看:
第一:一致性八仙图背后的理论
该数据异常的形成,存在2个偏序对,一是在变量x上事务T1指向事务T2的WR偏序对,二是在变量y上事务T2指向事务T1的WW偏序对,因为两个事务互相指向故存在环WR-WW这样的环,所以存在异常。
该环和Write Skew的RW-RW环是不同的。这点需要特别注意。因为此点,会在PostgreSQL中被PostgreSQL的实现方式造成困扰。
第二:从PostgreSQL的执行结果来看
该数据库对上述形式化表达式的处理是按SSI算法执行的。而Write-read Skew Committeed中根本不存在两个RW,却被SSI算法处理,这难道是SSI算法的bug?
第三:从PostgreSQL的代码分析来看
在PostgreSQL中,当执行表达式中的第5个操作W1[y2]时,会在逻辑上施加谓词写锁,但是,该谓词写锁其实是通过获取变量y的过程中施加的,该过程其实是一个读的过程非写的过程,即施加的锁看起来和施加谓词读锁是“一样的”(区别是:在查询时调用CheckForSerializableConflictOut,在更新时调用CheckTargetForConflictsIn,注意这两个函数调用中参数的赋值方式)。此时,PostgreSQL即认为存在两个RW。
第四:从PostgreSQL的代码分析再来看
前述明明说理论层面是WR-WW环,怎么会把WR变为了RW呢?这又是一个令人疑惑的点。要想理解这个,还得看PostgreSQL对快照和谓词锁的实现方式。
事务T1和T2处于并发阶段,因此事务T2的快照是看不到事务T1的数据的,即W1[x1]R2[x]被快照变为了R2[x0]W1[x1],因此原始表达式中看似WR实则成为RW。

五 PostgreSQL的隔离级别
在上一篇《第三代分布式数据库(6)--没有价值的隔离级别》中,我们提出:
一个数据库的隔离级别的实现存在这样的问题,是MySQL自身的问题,还是所有数据库行业的问题?
“一个数据库的隔离级别的实现存在这样的问题”,这样的问题是指“隔离级别没有什么价值,弱隔离级别性能优于强隔离级别未必一定成立,并且隔离级别的概念不能有效起到指导数据库系统进行工程实现”。
该文对MySQL系统进行了分析,MySQL恰如其分地符合这一观点,这也是说MySQL的数据正确性处理技术方面一无是处的原因之一。
那么,这个问题对于其他数据库系统而言呢?
那些没有提供真正的可串行化隔离级别的数据库,事实上需要在多场景下进行测试验证,才能得出明确的结论(欢迎各位网友测试各个数据库,交流测试数据和结论)。
而理论层面分析,Oracle只有两个隔离级别(RC和假的可串行化),从一致性八仙图的测试结果上看,他们之间实现的差异还是较大的:
1. RC级别下有较多的“黄色背景A”表明有较多的数据异常不能消除,数据正确性存在较大的危险性;
2. 假的SER隔离级别下,相比RC存在较多的“蓝色背景R”表明该级别的实现方式较为高级,用了一些对性能影响小的低代价规则加以消除数据异常,这点是其进步之处,但也正是这点,表明该隔离级别下性能并不会弱于RC级别,因为RC级别下同类异常有两种情况:
2.1) 一是允许该数据异常发生(有“黄色背景A”,如Read-write Skew 1),数据库引擎未做处理,故弱隔离级别下代码少性能好(但强隔离级别采用对性能影响小的规则加以处理,故相比RC性能不会非常弱)。
2.2) 二是采用另外的规则加以消除(有“蓝色背景R”,如Double-write Skew 1),数据库引擎采用MVCC技术消除了数据异常。Oracle的MVCC技术未像PostgreSQL和MySQL一样采用活跃事务作为判断并发的依据,而是采用数字标识的一个范围段标识活跃事务会极大降低性能,性能损失会大幅度减少,因此其加的SER未必会比RC性能低很多。(除非实现存在重大缺陷)。
2.3) 如上两种情况,表明RC和SER理论性能应该是接近的,因此,弱隔离级别并不能有效提升并发性能。
2.4) 如果未来Oracle实现真的SER隔离级别,则性能可能会差也可能会与RC相近,这取决于其采用的算法。
如果了解了这些后,Oracle用户还敢用RC级别不?如果敢用,是不是还是准备SELECT…FOR UPDATE主动加锁?只可惜,通常主管不知道主动加锁后性能下降、并导致用机成本上升;于是乎,各位稀里糊涂地把锅甩给了宿主数据库…
不过各个数据库背锅也不冤。因为各家数据库都没有致力在确保“100%的数据正确性和同时最大化性能”上耗费真金白银(即前述文章所讨论的:理论欠缺,踢球给用户,把用户当做小白鼠来完善产品),而是踢球给用户让用户自己用SELECT…FOR UPDATE解决数据正确性问题。于是乎,死循环形成(正确性-性能-正确性-性能…),问题无解。
其他未提供可串行化隔离级别的数据库有着类似的情况,道理如上,不再一一例举。
只可叹,国外的数据库产品具有先发的诸多优势已经占领世人心智,而国产数据库如果在数据正确性和性能方面只是跟随模仿,则在用户的TP型应用中将举步维艰、寸步难行。
当年TDXXX在进军宇宙某大行时POC测试一致性不过关,差点惨遭淘汰,后把数年前开发的一致性功能融入生成版本才顺利通过银行的一致性验证,便是一例。
其他国产数据库,如TXXX估值超高、但也只能在AP或HTAP场景下发力,事务处理能力不过关、数据正确性难以较好保证且性能弱,是不能进军TP型应用的重要原因。
国产数据库,久久为功功尚未成,任重道远远无核心技术,TP型数据库的国产化道路尚远。
让我们回到PostgreSQL的隔离级别上来。
从一致性八仙图分析,RR于RC的差异,如同如上分析的Oracle的假的SER和RC的差异,我们可以得到这两个隔离级别的性能差异理论上不会太大,分析过程参考如上内容在此不再赘述。
而性能差异大的,在于可串行化隔离级别和RC之间的差异。
有测试数据表明,PostgreSQL的RC是会好于SER很多。例如,TPC-C场景下百核的机器上PostgreSQL的RC可到164万tpmC,而SER只能到67万tpmC(SELECT…FOR UPDATE时40万tpmC),PostgreSQL性能下降严重,归功于SSI算法(这也是我们反复在说SSI算法鸡肋不好的原因之一,MySQL的SER可没有比RC性能下降很多哈,这么说,MySQL也不是“一无是处”而是“有一是处”哈)。

PostgreSQL各个隔离级别的TPC-C场景的性能
虽然,PostgreSQL的性能测试数据,能够表明“若隔离级别的性能好于强隔离级别”,似乎能够验证SQL标准定义隔离级别的初衷,但是,这也只是PostgreSQL如此,因为PostgreSQL的SSI看似是一段灿烂的金花,似乎新颖而美丽,但其实SSI算法性能低下,这个世界上还有比SSI更好的算法,其将再度表明隔离级别没有什么价值。
另外,隔离级别没有什么价值,根本原因是在理上站不住脚。一是ANSI/ISO-SQL标准中的隔离级别的定义没有数据没有理论支撑,二是在数据异常尚不齐全的情况下根据若干个数据异常划分隔离级别是以偏概全的做法,于理不通。ANSI/ISO-SQL标准对隔离级别级别的定义只是基于当时可见的有限现象做出的一种应对措施,不是深思熟虑的结果。
时过境迁,数据库系统应去掉隔离级别的概念,只实现“100%的数据正确性并最大化并发性能”。而基于全部数据异常,才有望做到最大化并发性能。
六 PostgreSQL与MySQL的对比
在主要的关键之处,对PostgreSQL和MySQL的数据正确性能力做一个简洁对比,如表6所示。
表6 PostgreSQL与MySQL的数据正确性能力对比(绿色背景表示具有优势)
PostgreSQL |
对比项 |
MySQL |
100% |
一致性保证能力(可串行化) |
100% |
强 |
一致性保证能力(弱隔离级别) |
弱 |
2PL+MVCC+SSI+死锁检测 |
一致性保证算法 |
SS2PL+MVCC+死锁检测 |
好(规则多) |
一致性算法的性能优劣(理论) |
差(死锁检测多) |
实时调用(性能差) |
死锁检测 |
非实时调用(性能较好) |
层进式 |
隔离级别实现 |
混乱非层进式 |
强(DDL可被包含在事务块内,事务回滚彻底) |
事务模型 |
弱(DDL不能处于事务块内,事务回滚不彻底) |
多数数据异常用规则消除 |
最大优点 |
锁粒度小(元组级) |
SSI算法较为鸡肋 |
最大缺点 |
死锁检测太多性能低 |

在信创的大潮中,尤其是金融证券保险电信等行业,对于一致性要求特别高的场景,尤其看重数据的一致性能力。这些单位迫切需要了解:
1. 某个候选数据库的一致性能力是什么样子的?
2. 如何评测其数据的一致性能力?(渴望系统化的原理做指导)
而一致性八仙图和其背后的理论能正面回答这两个问题。这是因为:
一致性八仙图及其理论能分析、展现一切提供事务处理能力的数据库系统的数据正确性实现能力。
因此,有网友提出:
现在虽然有了一致性八仙图,也有了《一无是处MySQL》、《比较拉胯的PostgreSQL》等文对MySQL和PostgreSQL的数据正确性能力做了全面分析,是否能够能够再进一步,分析一下其他的数据库系统,尤其是国产的数据库系统如TDSQL、TiDB、OceanBase(网友问:有没有最拉胯的国产?)。如果能有深入的分析,将能帮助大家心里有谱,在信创产业中更好地做数据库选型。
其实,从一致性八仙图中,大家可以观察到各个数据库的实际的结果,心里已经有自己的结论。如一个朋友的评述为:
《比较拉胯的PostgreSQL》, 《更加拉垮的Oracle和SQL Server》,《一无是处的MySQL》,…。这么看来还是PG最好。
如果照这样的方式写下去,可能会出现《最拉胯的TXXX》等。
但是,我们还是需要正向地看,技术是帮助我们不断进步的。
从一致性八仙图看,已经能得出正确的结论。从数据内核实现层面的技术角度看,在并发算法的实现上看,PostgreSQL和CockroachDB确实是值得学习的。
一致性八仙图所反映出的一些问题,有的已经提交给一些厂商作以资改进。相信通过一致性八仙图,各个数据库将能有所改进。
如果有深入分析任何数据库数据一致性能力的需要,我们可一起交流沟通。 本文禁止用于“任何软件程序”。

4. 第三代分布式数据库(4)——为什么要100%保证数据的正确性?(下)
5. 第三代分布式数据库(4)——为什么要100%保证数据的正确性?(下)

更多事务处理和分布式相关知识,可参考如下2本图书。


扫码加关注,知数行云,专业有深度
原文作者:那海蓝蓝知数行云;如有侵权,请联系管理员删除。