YashanDB 并发控制和一致性
本文内容来自 YashanDB 官网,原文内容请见 https://doc.yashandb.com/yashandb/23.3/zh/%E6%A6%82%E5%BF%B5%E6%89%8B%E5%86%8C/%E4%BA%8B%E5%8A%A1%E6%9C%BA%E5%88%B6/%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6%E5%92%8C%E4%B8%80%E8%87%B4%E6%80%A7.html
为了充分利用系统资源(内存、CPU、网络等),YashanDB 允许多个会话并行访问、修改数据库内容,如果对并发操作没有加以控制,就会破坏数据库的完整性和一致性。
YashanDB 通过多版本并发控制、事务隔离级别以及锁来维护数据库的一致性:
多版本并发控制:主要处理读写之间的并发。
事务隔离级别:控制多个事务之间的并发,并发事务在不同的隔离级别下只能访问对应可见版本的数据。
锁机制:主要处理写写之间的并发,通过锁机制控制不同事务对同一数据的并发修改。
多版本并发控制
# 读一致性
YashanDB 通过数据多版本实现读一致性,在修改数据时,会在 UNDO 表空间中保留数据的历史版本,使读写互不阻塞,并发事务可以访问一致版本,其特点如下:
查询一致性:用户执行 SQL 语句查询到的都是已经提交的、可见的、一致的数据版本。
读写不阻塞:用户执行 SQL 语句修改数据时,不阻塞并发事务查询正在修改的数据。
YashanDB 以 SCN(System change Number)系统变更版本号作为事务可见性判断依据,SCN 是一个时间相关的数值,事务提交时会推进系统 SCN。查询 SQL 语句以特定的 SCN 为视角,判断已提交事务对当前查询的可见性,从而获取到一致性的结果。
查询语句访问数据是以 Block(数据块/页)为单位,通过判断 Block 上 Xslot(事务槽位)对应事务的事务可见性:
对于可见的事务,生成一个对查询可见的一致性读 Block,又称 CR(Consistent Read) Block。
对于不可见的事务,通过 Xslot 指向的回滚段中的历史记录,还原到可见的版本。

以 HEAP block 为例,当前 Block 上存在 4 个 row,row2 和 row4 对应的事务对当前查询 SCN 不可见,通过 Xslot 上指向的 undo Row,找到对应的可见版本。
row2:需要应用一次历史版本得到可见的版本。
row4:其可见的历史版本不存在(insert 的 undo 意味着行是新插入的,对当前查询不可见)。
将 undo 记录应用于 HEAP block 上,生成一个对查询可见的 CR block,从而满足查询的一致性。此时并发事务仍然可以对页面上的记录进行访问和修改,并不影响当前语句对 CR block 的访问。
YashanDB 所有部署形态都满足读一致性,共享集群中一个 block 可以被多个实例同时访问、修改,可能产生多个实例的事务参与同一个 HEAP block 的 CR block 生成,整个过程在全局缓存中完成。
语句级一致性读
用户执行 SQL 查询语句时获取基于某一时间点的 SCN,并在查询过程中使用此 SCN 进行一致性读。
YashanDB 默认的多版本读一致性是语句级的。
事务级一致性读
事务级一致性读在满足语句级一致性读原则的基础上,每条查询语句获取的查询 SCN 采用当前事务开始时的快照,即同一个事务内所有语句获取的是同一个版本的数据。
# 写一致性
写一致性定义两条(或多条)并发执行的语句需要以近似串行化的方式执行,其本质是当并发执行的修改语句产生互相影响时,后发生的一方会触发语句重启。在一些需要修改保持一致性的场景下,YashanDB 会自动以写一致性的方式执行。
以一个实际用户场景为例,在没有写一致性的情况下,下面并发语句会存在漏更新问题:

事务隔离级别
数据库事务的并发可能会对事务之间的读写产生一定影响:
脏读:一个事务读取了另外一个尚未提交事务修改的数据。
不可重复读:同一个事务内,多条语句重复读取同一行数据,读取到的数据发生变化。
幻读:同一事务内,多条语句重复读取同一条件的数据,读取到的结果集数量发生变化。
事务隔离级别能确保多个事务并发执行时的行为,影响数据的一致性和并发性能。在 ANSI 标准中定义了四种事务隔离级别:
读未提交(Read Uncommitted)
最低级别的隔离级别,性能较好,但会破坏数据的一致性。
在此级别下,允许脏读,即一个事务可能会看到其他并发执行事务未提交的修改。
读已提交(Read Committed)
此隔离级别保证事务访问其他事务修改数据时,只能读取已提交的数据版本。避免出现脏读,但存在不可重复读现象。
此类级别还包含读当前提交(Current Committed),只能读取已提交的数据版本,不存在脏读和幻读,但无法保证语句内的读一致性,且可能存在不可重复读场景。
可重复读(Repeatable Read)
在读已提交的基础上,同一个事务内所有语句看到的数据版本都是一致的,避免了脏读和不可重复读,但仍然存在幻读。
可串行化(Serializable)
最严格的隔离级别,事务之间完全隔离,保证了并发事务之间不会产生冲突,避免了脏读、不可重复读和幻读。
不同的隔离级别会导致并发数据访问时可能会出现以下问题:

YashanDB 支持的事务隔离级别为读已提交和可串行化。
# 读已提交
YashanDB 默认采用读已提交隔离级别,同样可以通过 SQL 语句设置隔离级别为读已提交。
Copied!
读一致性
事务内每条语句严格按照语句级一致性读执行,语句开始执行时获取系统最新 SCN 作为查询 SCN,并且在整个语句执行过程中采用同一 SCN 进行查询,生成一致性的结果集。
写冲突
写冲突场景下,一个事务会尝试修改另外一个未提交事务修改的行记录,此时会触发行锁等待,直到对方事务结束:
如果等待的事务回滚,此时当前事务会继续锁定当前行并进行修改。
如果等待的事务提交,此时当前事务会读取最新版本并进行条件检查,如果符合条件会继续锁定当前行并进行修改。
下面以一个实际示例说明读已提交隔离级别:

# 可串行化
YashanDB 支持的串行化属于快照级串行化,提供了事务级一致性读能力,并提供写写串行化冲突检测机制。可以通过下面 SQL 语句设置隔离级别为可串行化:
Copied!
读一致性
事务内的每条语句严格按照事务级一致性读进行,事务启动时会获取当前系统的 SCN 作为当前事务查询的 SCN。整个可串行化事务运行过程中采用同一个 SCN 进行查询,生成一致性的结果集。
写冲突
可串行化的写冲突检测机制与读已提交的写冲突处理不同:
如果等待的事务回滚,此时当前事务会继续锁定当前行并进行修改。
如果等待的事务提交,此时会触发串行化写冲突,会串行化冲突错误。
下面以一个实际示例说明可串行化隔离级别:

锁机制
锁是数据库内控制并发事务对数据的修改的一种机制,而数据库内数据由元数据、用户数据共同组成。基于数据类别的并发有以下几种:
并发事务对元数据的冲突修改,即 DDL 间并发。
并发事务对用户数据的冲突修改,即 DML 间并发。
并发事务对用户数据和元数据之间的冲突修改,即 DDL 与 DML 间并发。
通过不同粒度的锁进行上述场景的并发控制,在 YashanDB 中面向用户的锁主要有表锁和行锁。
# 表锁管理
表锁主要发生 DDL 语句或修改数据的 DML 语句,在语句执行时自动加锁,直至事务结束时自动释放。表锁模有两种模式:
Share Lock(表级共享锁,S):最低级的表锁,允许 DML 并发执行,DML 修改数据时会加表级共享锁来阻塞并发 DDL 的执行。
Exclusive Lock(表级排他锁,X):最高级别的表锁,DDL 操作时会加表级排他锁,阻塞其他并发的 DDL 和 DML 执行。
可以通过 lock table employee in exclusive mode 语句对目标表显式加排他锁。
# 行锁管理
行锁主要发生在 DML 语句修改数据时,事务修改数据时会锁定要修改的行记。在 YashanDB 中行锁是一种物理锁,通过 Block 上的 Xslot(事务槽位)登记锁信息。
行锁只有排他锁一种类型,不支持行级共享锁。
可以通过如下语句显式锁定要访问的行。
Copied!
# 死锁与检测
当多个事务获取并修改同一数据库资源时,会产生资源等待(例如等待表锁释放、等待行锁释放等)。当多个事务互相等待彼此释放资源时会产生死锁现象。此时单靠并发事务自身无法识别并解除死锁,YashanDB 支持对产生死锁的事务进行检测并处理。
表锁死锁
以显式加表锁为例,构造表锁死锁场景:

行锁死锁
以更新事务为例,构造行锁死锁场景:

版权声明: 本文为 InfoQ 作者【YashanDB】的原创文章。
原文链接:【http://xie.infoq.cn/article/0250559ae638ebf8322becb6a】。文章转载请联系作者。
评论