#并发控制和一致性

为了充分利用系统资源(内存、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会自动以写一致性的方式执行。

以一个实际用户场景为例,在没有写一致性的情况下,下面并发语句会存在漏更新问题:

会话1 会话2 会话3 解读
create table employee (id int, title int, age int)
partition by range (title) (
partition p1 values less than (5),
partition p2 values less than (10),
partition p3 values less than (15)
) enable row movement;

insert into employee values (1, 4, 24);
insert into employee values (2, 8, 28);
insert into employee values (3, 12, 30);
commit;
会话1创建职工测试表并插入三条记录。
select * from employee where id = 2 for update; 通过for update语句锁定分区2内的数据。
update employee set age = age + 1; 对所有员工的年龄增加1,此时会话3会被会话2阻塞,需等待会话2事务结束。
update employee set title = 6 where id = 3;
commit;
更新id为3的职工title由12更新为6并提交,此时产生数据的跨分区变更,职工3的信息从分区3搬迁到分区2中。
rollback; 更新成功 会话2解除锁定,会话3更新成功。
select * from employee; 在没有写一致性的保护下,职工3的年龄会出现漏更新的情况。

写一致性定义的是并发执行的语句间的关系,而并发事务间的关系请查阅事务隔离级别

# 事务隔离级别

数据库事务的并发可能会对事务之间的读写产生一定影响:

  • 脏读:一个事务读取了另外一个尚未提交事务修改的数据。

  • 不可重复读:同一个事务内,多条语句重复读取同一行数据,读取到的数据发生变化。

  • 幻读:同一事务内,多条语句重复读取同一条件的数据,读取到的结果集数量发生变化。

事务隔离级别能确保多个事务并发执行时的行为,影响数据的一致性和并发性能。在ANSI标准中定义了四种事务隔离级别:

  • 读未提交(Read Uncommitted)

    最低级别的隔离级别,性能较好,但会破坏数据的一致性。

    在此级别下,允许脏读,即一个事务可能会看到其他并发执行事务未提交的修改。

  • 读已提交(Read Committed)

    此隔离级别保证事务访问其他事务修改数据时,只能读取已提交的数据版本。避免出现脏读,但存在不可重复读现象。

    此类级别还包含读当前提交(Current Committed),只能读取已提交的数据版本,不存在脏读和幻读,但无法保证语句内的读一致性,且可能存在不可重复读场景。

  • 可重复读(Repeatable Read)

    在读已提交的基础上,同一个事务内所有语句看到的数据版本都是一致的,避免了脏读和不可重复读,但仍然存在幻读。

  • 可串行化(Serializable)

    最严格的隔离级别,事务之间完全隔离,保证了并发事务之间不会产生冲突,避免了脏读、不可重复读和幻读。

不同的隔离级别会导致并发数据访问时可能会出现以下问题:

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可串行化 不可能 不可能 不可能

YashanDB支持的事务隔离级别为读已提交和可串行化。

# 读已提交

YashanDB默认采用读已提交隔离级别,同样可以通过SQL语句设置隔离级别为读已提交。

SET TRANSACTION isolation LEVEL read committed;

读一致性

事务内每条语句严格按照语句级一致性读执行,语句开始执行时获取系统最新SCN作为查询SCN,并且在整个语句执行过程中采用同一SCN进行查询,生成一致性的结果集。

写冲突

写冲突场景下,一个事务会尝试修改另外一个未提交事务修改的行记录,此时会触发行锁等待,直到对方事务结束:

  • 如果等待的事务回滚,此时当前事务会继续锁定当前行并进行修改。

  • 如果等待的事务提交,此时当前事务会读取最新版本并进行条件检查,如果符合条件会继续锁定当前行并进行修改。

下面以一个实际示例说明读已提交隔离级别:

会话1 会话2 解读
create table t1 (id int);
insert into t1 values (1);
insert into t1 values (2);
commit;
会话1创建测试表并插入两条记录。
set transaction isolation level read committed;
select * from t1;
会话2设置当前事务隔离级别为读已提交。并查询表数据。
update t1 set id = -1 where id = 1;
commit;
会话1更新第一条记录的id为-1并提交。
select * from t1; 会话2再次查询表数据,读取到会话1事务更新后的值。
update t1 set id = -2 where id = 2; 会话1更新第二条记录的id为-2。
update t1 set id = id * 10 where id = 2; 会话2尝试更新相同记录,此时产生事务等待。
commit; 会话1提交后,会话2更新成功。
select * from t1; 会话2查询结果为-1和-20。

# 可串行化

YashanDB支持的串行化属于快照级串行化,提供了事务级一致性读能力,并提供写写串行化冲突检测机制。可以通过下面SQL语句设置隔离级别为可串行化:

SET TRANSACTION isolation LEVEL serializable;

读一致性

事务内的每条语句严格按照事务级一致性读进行,事务启动时会获取当前系统的SCN作为当前事务查询的SCN。整个可串行化事务运行过程中采用同一个SCN进行查询,生成一致性的结果集。

写冲突

可串行化的写冲突检测机制与读已提交的写冲突处理不同:

  • 如果等待的事务回滚,此时当前事务会继续锁定当前行并进行修改。

  • 如果等待的事务提交,此时会触发串行化写冲突,会串行化冲突错误。

下面以一个实际示例说明可串行化隔离级别:

会话1 会话2 解读
create table t1 (id int);
insert into t1 values (1);
insert into t1 values (2);
commit;
会话1创建测试表并插入两条记录。
set transaction isolation level serializable;
select * from t1;
会话2设置当前事务隔离级别为可串行化。并查询表数据。
update t1 set id = -1 where id = 1;
commit;
会话1更新第一条记录的id为-1并提交。
select * from t1; 会话2再次查询表数据,读取到会话1更新前的值。
update t1 set id = id * 10 where id = -1; 会话2更新报错,检测到串行化冲突错误。

# 锁机制

锁是数据库内控制并发事务对数据的修改的一种机制,而数据库内数据由元数据、用户数据共同组成。基于数据类别的并发有以下几种:

  • 并发事务对元数据的冲突修改,即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(事务槽位)登记锁信息。

行锁只有排他锁一种类型,不支持行级共享锁。

可以通过如下语句显式锁定要访问的行。

SELECT * FROM employee FOR UPDATE;

# 死锁与检测

当多个事务获取并修改同一数据库资源时,会产生资源等待(例如等待表锁释放、等待行锁释放等)。当多个事务互相等待彼此释放资源时会产生死锁现象。此时单靠并发事务自身无法识别并解除死锁,YashanDB支持对产生死锁的事务进行检测并处理。

表锁死锁

以显式加表锁为例,构造表锁死锁场景:

会话1 会话2 解读
create table t1 (id int);
create table t2 (id int);
创建测试表t1、t2。
lock table t1 in exclusive mode; 会话1对t1表加排他锁。
lock table t2 in exclusive mode; 会话2对t2表加排他锁。
lock table t2 in exclusive mode; 会话1对t2表尝试加排他锁,此时会等待会话2事务。
lock table t1 in exclusive mode; 会话2对t1表尝试加排他锁,此时会等待会话1事务。
此时数据库检测会话1、会话2互相等待,报死锁错误并解除死锁。

行锁死锁

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

会话1 会话2 解读
create table t (id int);
insert into t values (1);
insert into t values (2);
commit;
创建测试表,并插入两行记录。
update t set id = -id where id = 1; 会话1更新row1。
update t set id = id * 10 where id = 2; 会话2更新row2。
update t set id = -id where id = 2; 会话1更新row2,此时尝试对row2加锁,等待会话2事务。
update t set id = id * 10 where id = 1; 会话2更新row1,此时尝试对row1加锁,等待会话1事务。
此时数据库检测会话1、会话2互相等待,报死锁错误并解除死锁。