使用时间戳的并发控制
专栏内容:
- 手写数据库toadb
本专栏主要介绍如何从零开发,开发的步骤,以及开发过程中的涉及的原理,遇到的问题等,让大家能跟上并且可以一起开发,让每个需要的人成为参与者。
本专栏会定期更新,对应的代码也会定期更新,每个阶段的代码会打上tag,方便阶段学习。
开源贡献:
- toadb开源库
个人主页:我的主页
管理社区:开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.
文章目录
- 使用时间戳的并发控制
- 前言
- 概述
- 时间戳介绍
- 记录时间戳的方法
- 事务提交的记录
- 可以解决的问题
- 过晚的读
- 过晚的写
- 脏数据的问题
- mysql中的表现
- 基于时间戳调度的规则
- 调度器选择
- 读写请求的处理
- 多版本时间戳
- 时间戳与封锁
- 总结
- 结尾
前言
随着信息技术的飞速发展,数据已经渗透到各个领域,成为现代社会最重要的资产之一。在这个大数据时代,数据库理论在数据管理、存储和处理中发挥着至关重要的作用。然而,很多读者可能对数据库理论感到困惑,不知道如何选择合适的数据库,如何设计有效的数据库结构,以及如何处理和管理大量的数据。因此,本专栏旨在为读者提供一套全面、深入的数据库理论指南,帮助他们更好地理解和应用数据库技术。
数据库理论是研究如何有效地管理、存储和检索数据的学科。在现代信息化社会中,数据量呈指数级增长,如何高效地处理和管理这些数据成为一个重要的问题。同时,随着云计算、物联网、大数据等新兴技术的不断发展,数据库理论的重要性日益凸显。
因此,本专栏的分享希望可以提高大家对数据库理论的认识和理解,对于感兴趣的朋友带来帮助。
概述
在数据库中如何保证并发事务时,数据的一致性,也就是可串行化,会有采用调度器来进行协调各事务中动作的顺序,以衣是否可以执行等。调度器采用的模型主要有几种:
- 基于封锁的调度模型
- 基于时间戳的调度模型
- 基于有效性确认的调度模型
前几篇博文中分享了基于封锁的调度模型,本文主要介绍基于时间戳的调度模型,主要从时间戳的概念,可以保证的行为和存在的问题,调度规则,以及多版本的优化,与封锁模型的联合使用等方面进行介绍。
时间戳介绍
也就是记录上次读和写每个数据库元素的事务时间点,同时每个事务也有一个时间戳,记录它的开始时间点。
当有事务要请求该数据库元素时,比较这两个时间,根据事务的时间戳来调度,来确保串行调度。
记录时间戳的方法
-
理论上当多个事务开始的时间间隔大于时间最小计数时,使用时间来记录是可以达到目标的,但是往往时间的精度不足以记录多个同时开始的事务。
-
调度器维护一个计时器。每当一个事务开始时,计数器就加1,而新值成为该事务的时间戳。这种方法与时间无关,但是它们具有时间的特性,单调递增,不会重复,总是保证晚的事务比开始早的事务具有更高的时间戳;
事务提交的记录
当一个事务T读到另一事务U所写的数据,这一行为也是符合串行化规则,但是事务U最后中止了,并没有提交,这样事务T读到的是脏数据,这一问题肯定会导致数据库状态变得不一致,这是任何调度器都要防止的脏读。
除了两个事务和数据库元素上的时间戳外,还需要记录一个事务的提交状态位,当事务没有提交时,调度器也需要阻止其它事务的访问请求。
可以解决的问题
假如事务在开始的那一时刻就立即执行结束,那也就不会发生非可串行化的问题。往往事务中的各个动作都会持续一段时间,这就会过晚读和过晚写的问题发生,而当事务中止时,读取的此事务写的数据,就会发生脏读的情况。
过晚的读
- 问题描述
事务执行的时间轴是这样的
如图所示,事务T的读在事务U的写之后,而事务U的开始时间晚于事务T,这就导致事务T读到的数据不一致。
- 解决方法
当事务T的进行读请求时,发现当前数据元素上的时间戳晚于自己的事务开始时间戳时,事务T应该是需要中止,它什么都不能做了。
过晚的写
- 问题描述
事务执行的时间轴是这样的
如图所示,事务U开始时间晚于事务T,而事务U的读操作早于事务T,本应该事务U可以读到T写入的值,但是T的写入更晚。
- 解决方法
事务T因为时间戳晚于数据元素上的时间戳,也就是事务U访问的时间戳,应该中止事务T,让事务U可以读取正确的数据。
脏数据的问题
事务提交标志的设置,就是用来解决这个问题的,先来看两个问题。
-
问题一
|事务U | 事务T|
|:–|:–|
|begin; ||
|write(X) | |
|| begin;|
||read(X)|
|abort||
||commit;| -
问题二
|事务U | 事务T|
|:–|:–|
|begin; ||
|write(X) | |
|| begin;|
||write(X)|
||commit;|
|abort||
对于问题一,因为事务U在事务T之前启动,并写入X,所有事务T读取X是符合上面时间戳的规则,但是当事务U最终中止时,事务T读取的X就是脏数据,是数据库中本不存在的数据;
对于问题二,有趣的事情来了,此时事务T提交后,其实它是基于事务U的,比如X=1,事务U写入后X=2, 事务T写入后X=3,那么提交成功后X=3;而事务U回滚后,好像什么都不需要做,还是事务U回滚为X=1,事务T重新再做一遍呢?
- 解决方法
对于问题一的此类问题,请求读操作时,需要看当前数据元素是否已经提交,如果没有提交,需要中止当前请求,或推迟到该数据库元素提交之后再处理。
而对于问题二的此类问题,写操作请求时,也同样需要判断当前数据元素是否已经提交,如果没有提交,需要中止当前请求,或推迟到该数据库元素提交之后再处理。 当然,更晚的写也可以什么都不做,这被称为Thomas写法则,最后事务U中止后,它要回退它的写入和数据库元素上的时间戳,但是事务T的写入被跳过了,同时也提交完成了,此时想恢复事务T的操作已经不可能了。
mysql中的表现
mysql> show variables like 'transaction%';
+----------------------------------+-----------------+
| Variable_name | Value |
+----------------------------------+-----------------+
| transaction_alloc_block_size | 8192 |
| transaction_allow_batching | OFF |
| transaction_isolation | REPEATABLE-READ |
| transaction_prealloc_size | 4096 |
| transaction_read_only | OFF |
| transaction_write_set_extraction | XXHASH64 |
+----------------------------------+-----------------+
6 rows in set (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_concurrent;
+------+
| i |
+------+
| 5 |
+------+
1 row in set (0.00 sec)
-- 这此时另外启动一个事务,将i修改为6,并提交事务
mysql> select * from test_concurrent;
+------+
| i |
+------+
| 5 |
+------+
1 row in set (0.00 sec)
mysql> update test_concurrent set i = 3 where i = 5;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_concurrent;
+------+
| i |
+------+
| 6 |
+------+
1 row in set (0.00 sec)
可以看到mysql中,当前事务可以看到i=5,但确修改不成功,返回0 rows被updated,这就是一个很迷惑的现象。
基于时间戳调度的规则
经过上面问题的分析,现在我们概括基于时间戳调度的规则。
调度器选择
对于来自事务的读写操作请求,调度器有几种选择:
- 同意该请求
- 推迟请求
- 中止请求事务
读写请求的处理
调度器收到读写操作请求,
- 收到读操作请求时,检查当前数据库元素上次操作事务的提交状态,
- 如果已经提交,则再检查时间戳的先后顺序,如果请求事务的时间戳大于当前数据元素的时间戳,则可以同意请求,并将时间戳更新为当前事务;如果事务时间戳小于当前数据元素的时间戳,则需要中止;
- 如果尚未提交,则请求事务需要推迟;
- 当收到写操作请求时,先检查当前事务与数据库元素上的时间戳,
- 如果请求事务的时间戳大于当前数据元素的时间戳,再检查数据元素上次操作的事务是否提交,如果已经提交,则同意本次写请求;如果未提交,则需要推迟本次请求;
- 如果事务时间戳小于当前数据元素的时间戳,本次请求事务需要中止;
-
当收到事务提交请求时,更新数据元素上的提交状态;同时唤醒等待的事务请求;
-
当收到事务T中止请求时,那么回退事务T对应的所有操作数据;等待的事务需要重新发起读或写请求,因为需要检查事务T的写被中止后是否合法。
多版本时间戳
基于时间戳的并发控制调度器,如上面介绍的,会存在读写之间冲突,所以在这个基础上进行了一个重要的演进,就是同时保留数据库元素的多个带不同时间戳的版本,使得读写可以同时进行。
多版本时间戳的流程与上面流程类似:
- 当收到写操作请求时WT(X),如果请求被同意,那么X的一个新版本Xi被创建,它的时间戳为Ti(X);
- 此时收到一个读操作请求时RU(X)时,最新版本检查不通过时,查找时间戳小于事务U的版本X;也是就WT(X)执行前的版本,就是当前可读的版本,同意RU(X)在版本X上的读请求;
- 数据元素的时间戳与对应的版本有关;
- 当然再有事务的写请求来时,还是需要在最后的版本Xi上处理;
- 旧版本的清理,当X的某个版本上的时间戳小于任何当前活跃事务的时间戳时,就可以清理掉它了。
多版本时间戳的方式,解决了读写并发时的性能问题。
时间戳与封锁
在大多数只读事务或者并发读写同一元素的情况不频繁时,基于时间戳的调度比较有优势;
而当读写并发比较高,而且对同一数据库元素竞争较大时,封锁调度反而比较优,因为此种情况下基于时间戳的调度,需要进行频繁的回退操作。
在现代商用数据库中,会将事务分为只读事务和读写事务,在只读事务时,只使用时间戳的方式,而只读事务时采用两阶段锁的方式。
总结
基于时间戳的调度模型可以说是一种乐观的模型,它假设没有非可串行化行为发生,并且只有在违例发生时才会进行修正或者中止。与此相反,封锁的调度模型是假设非可串行化行为一定会发生,那么提前进行预防,并且推迟可能发生的事务,但不中止它们,它是一种悲观模型。
这两种模型,如果对于大量只读操作时,乐观型好于悲观型调度器。
结尾
非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!
作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。