并发与事务控制
正如基础概念中所述,SmartEDB 通过运用事务块来管理数据库数据的多线程和多进程竞争,并且提供了三种不同的事务管理器,它们根据乐观或悲观策略来处理对数据的并发访问。
SmartEDB 事务管理器支持三种事务隔离级别,这可能会影响并发数据库访问的性能。而且,在某些情况下,事务优先级或调度策略可以进行调整,从而影响事务管理器对数据库事务的管理方式。
用于管理并发的 API 和结构取决于所使用的编程语言。请使用以下链接查看针对您的开发环境的详细说明和示例:
开发语言 | 说明 |
---|---|
C / C++ | C / C++语言中的并发和事务管理 |
Java | Java 语言中的并发和事务管理 |
C# | C# 语言中的并发和事务管理 |
关键目标
事务是数据库中的工作单元,即对数据执行的单个逻辑操作。SmartEDB 事务严格遵循 ACID 属性。ACID 模型是数据库理论中最古老且最重要的概念之一,它确立了数据库管理系统必须努力实现的四个关键目标:原子性、一致性、隔离性和持久性。任何未能满足这四个目标中的任何一个的数据库系统都不能被视为可靠的。
原子性
原子性确保数据库修改遵循“全有或全无”的规则。每个事务都被视为一个不可分割的整体。如果事务的一部分失败,整个事务将被取消。无论出现任何数据库管理系统、操作系统或硬件故障,保持事务的原子性都是至关重要的。
一致性
一致性保证只有符合规则的有效数据才会被写入数据库。如果某个事务违反了数据库的一致性规则,整个事务将被回滚,数据库将恢复到之前的一致状态。相反,如果事务成功执行,它会将数据库从一个一致的状态转换到另一个一致的状态。
隔离性
隔离性要求同时发生的多个事务不会相互干扰。例如,如果乔和玛丽在同一时间分别发起不同的事务,这两个事务应以隔离的方式运行,互不影响。这可以防止乔的事务读取玛丽事务中未提交的中间数据。ANSI/ISO 定义了四种可能的隔离级别,其中三种由 SmartEDB 支持:可串行化、可重复读和已提交读(未提交读不支持)。有关 SmartEDB 对这些隔离级别的实现,请参阅“事务隔离级别”部分。
持久性
持久性确保一旦事务提交,其结果就不会丢失。通过使用数据库备份和事务日志,即使在后续出现任何软件或硬件故障的情况下,也能恢复已提交的事务。
事务阻塞
为了强制执行 ACID 原则,SmartEDB 要求所有数据库访问均在事务的上下文中进行。事务的开始和提交方法定义了一个事务块,该块作为单个数据库操作应用。如果需要,可以调用事务回滚方法,以丢弃在事务开始之后实施的任何数据更新,而不将这些更改应用到数据库。
事务隔离级别
事务隔离定义了一个事务所做的更改在何时以及如何对其他并发事务可见。数据库管理系统通常提供多个事务隔离级别,这些级别决定了一个事务必须与由其他事务所做的数据修改隔离的程度。根据 ANSI/ISO SQL 标准,共有四个事务隔离级别:未提交读取、已提交读取、可重复读取和可串行化。SmartEDB 事务管理器支持其中的三个级别,具体如下:
已提交读取(Read Committed)
使用此级别时,事务始终读取已提交的数据。事务不会读取其他事务已更改但尚未提交的数据,但这不能保证在事务结束之前数据不会被其他事务更改。在使用 MVCC(但不是 MURSIW)的情况下,如果在事务期间重新读取某个值,该值可能会发生变化,因为另一个事务可能已对该对象进行了更改并提交。
可重复读取(Repeatable Read,快照)
这是 SmartEDB 中的默认隔离级别。当 MVCC 事务在可重复读级别运行时,它会在读取对象时创建一个对象的快照。重新读取该对象时,会重新读取快照,而不是数据库中已提交的对象(该对象可能已被更改),因此得名“可重复读”。对于 MURSIW,事务既为已提交读,也为可重复读,因为任何写事务都不能与其他事务并行执行。
可串行化(Serializable)
此级别对所有写入事务应用排他锁——同一时间没有其他写入事务可以运行。不过,借助 MVCC,“读取者”仍可与“写入者”以及彼此并行运行。请注意,MVCC 写入事务的这种“顺序”处理仅适用于具有 MCO_SERIALIZABLE 隔离级别的事务;具有其他隔离级别的事务可以同时处于活动状态。使用 MURSIW 时,写入事务始终是串行化的。
示例
为了更好地理解在多版本并发控制(MVCC)中选择隔离级别的重要性,考虑以下两个并发执行的事务 t1 和 t2 对数据集进行读取和更新的情况:
a=1 b=2
t1: e=b ; a=a+1; c=a+b
t2: f=a ; b=b+2; d=a+b
可序列化的案例#1
t1: e=b e=2
t1: a=a+1 a=1+1 a=2
t1: c=a+b c=2+2 c=4
t2: f=a f=2
t2: b=b+2 b=2+2 b=4
t2: d=a+b d=2+4 d=6
-------------------------
Result: a=2, b=4, c=4, d=6, e=2, f=2
可序列化的案例#2
t2: f=a f=1
t2: b=b+2 b=2+2 b=4
t2: d=a+b d=1+4 d=5
t1: e=b e=4
t1: a=a+1 a=1+1 a=2
t1: c=a+b c=2+4 c=6
-------------------------
Result: a=2, b=4, c=6, d=5, e=4, f=1
可重复读取
t1: read a a(t1)=1
t1: read b b(t1)=2
t2: read a a(t2)=1
t2: read b b(t2)=2
t1: e=b(t1) e=2
t2: f=a(t2) f=1
t2: b(t2)=b(t2)+2 b=2+2 b(t2)=4
t2: d=a(t2)+b(t2) d=1+4 d=5
t2: commit
t1: re-read a a(t1)=1
t1: re-read b b(t1)=2
t1: a(t1)=a(t1)+1 a=1+1 a(t1)=2
t1: c=a(1)+b(t1) c=2+2 c(t1)=4
t1: commit
-----------------------------
Result: a=2, b=4, c=4, d=5, e=2, f=1
读提交
t1: read a a(t1)=1
t1: read b b(t1)=2
t2: read a a(t2)=1
t2: read b b(t2)=2
t1: e=b(t1) e=2
t2: f=a(t2) f=1
t2: b(t2)=b(t2)+2 b=2+2 b(t2)=4
t2: d=a(t2)+b(t2) d=1+4 d=5
t2: commit
t1: re-read a a(t1)=1
t1: re-read b b(t1)=4
t1: a(t1)=a(t1)+1 a=1+1 a(t1)=2
t1: c=a(1)+b(t1) c=2+4 c(t1)=6
t1: commit
-----------------------------
Result: a=2, b=4, c=6, d=5, e=2, f=1
总之,这些案例各自截然不同的结果是:
Serializable #1: a=2, b=4, c=4, d=6, e=2, f=2
Serializable #2: a=2, b=4, c=6, d=5, e=4, f=1
Repeatable Read: a=2, b=4, c=4, d=5, e=2, f=1
Read Committed: a=2, b=4, c=6, d=5, e=2, f=1
事务优先级与调度
借助 MURSIW,SmartEDB 支持事务优先级;也就是说,可以在运行时为每个事务分配一个优先级值。在事务向运行时注册之时,事务调度器会检查优先级值,并相应地将事务在事务队列中向前或向后移动。使用 MURSIW 事务管理器时,应用程序通常作为前台事务执行,不过借助事务优先级机制,如有必要,应用程序也可以在运行时将事务的执行“提升”至高优先级。SmartEDB 的事务优先级定义如下:
常量 | 说明 |
---|---|
MCO_TRANS_IDLE | 最不重要的事务 |
MCO_TRANS_BACKGROUND | 比前台事务次要但比空闲事务重要 |
MCO_TRANS_FOREGROUND | 正常优先级事务 |
MCO_TRANS_HIGH | 比前台事务重要但比中断服务请求次要 |
MCO_TRANS_ISR | 非常重要的事务 |
MVCC 事务处理
在 MVCC 中,事务优先级具有略微不同的含义。优先级高于“正常”(MCO_TRANS_FOREGROUND)的事务会锁定整个索引,将对树和哈希结构的并行访问限制为只读访问。通过这样做,事务在游标移动期间避免锁定索引页,这在需要遍历大量对象时可以提高性能。当游标到达结果集末尾或调用游标关闭方法时,锁将被释放。将对树和哈希结构(索引)的并行访问限制为只读访问意味着,当所讨论的类没有 oid 或 autoid 时,如果并行事务更新同一类的对象,则在调用事务提交函数时会被阻塞,直到优先级较高的任务释放锁。对于具有 oid 或 autoid 的类,oid 或 autoid 索引会在调用类的 new 方法时立即更新,因此并行任务将在该函数上被阻塞,直到优先级较高的任务释放锁。需要权衡并行任务被阻塞的可能性与避免在每次游标移动(例如游标的 next 方法)期间设置和释放短期锁所带来的性能提升。
MURSIW 事务调度策略
MURSIW 事务管理器根据事务优先级将所有传入的事务放入队列。当新的传入事务试图访问数据库运行时,MURSIW 管理器会确保:
- 避免冲突:确保没有与当前活跃事务发生冲突。
- 优先级检查:确保队列中没有更高或同等优先级的事务在等待。
如果传入的事务是读写事务(MCO_READ_WRITE),数据库运行时不会检查更高优先级的事务。这是因为即使当时存在更高优先级的事务,也意味着这些事务正在等待其他事务完成,从而阻止传入事务获得对数据库的独占(“读写”)访问。一旦事务完成(提交或回滚),MURSIW 算法会从队列中选择下一个事务进行处理。
为了防止活跃的“只读”事务阻塞对更高优先级“读写”事务的访问,并确保先进先出的调度策略,MURSIW 事务管理器不允许优先级低于队列中读写事务优先级的传入只读事务立即被调度。这样可以确保高优先级的读写事务能够及时获得所需的资源。
应用程序可以通过以下标志来控制事务优先级的默认调度策略,这些标志决定了具有相同优先级的事务如何处理:
- MCO_SCHED_FIFO:默认的先进先出调度策略。
- MCO_SCHED_READER_FAVOR:优先处理只读事务,将其置于相同优先级的读写事务之前进入队列。
- MCO_SCHED_WRITER_FAVOR:优先处理读写事务。
常量 | 说明 |
---|---|
MCO_SCHED_FIFO | 默认的先进先出调度策略 |
MCO_SCHED_READER_FAVOR | 优先处理只读事务,将其置于相同优先级的读写事务之前进入队列 |
MCO_SCHED_WRITER_FAVOR | 优先处理读写事务 |