数据库并发控制
数据库并发控制是数据库管理系统(DBMS)中的一种重要策略,用于确保在多个用户或事务同时访问数据库时,数据的一致性和隔离性得到维护。
主要目的
数据库并发控制是指管理多个事务同时访问同一数据库时的数据一致性和完整性,以防止数据冲突和不一致。其主要目的包括:
- 避免更新丢失:确保当多个事务同时更新同一条记录时,所有的更新都能得到保存,而不是被其他事务的操作覆盖。
- 避免脏读:防止一个事务读取了另一个事务未提交的更新,这可能导致读取到错误的数据。
- 避免不可重复读:确保在同一事务中,多次读取同一数据的结果是一致的,防止在两次读取之间有其他事务对数据进行更新。
- 避免幻读:确保在一个事务执行过程中,同一查询在事务开始和结束时看到的行数是一致的,防止在事务执行过程中有其他事务插入或删除记录。
解决方案
数据库并发控制主要采用以下几种:
- 锁机制:
- 共享锁(S锁):允许多个事务同时读取同一数据,但不允许修改。
- 排他锁(X锁):禁止其他事务读取或写入锁定的数据,确保写操作的独占性。
- 锁的粒度:包括行级锁、表级锁和页级锁,不同的锁粒度对系统性能和数据一致性有不同的影响。
- 两阶段锁协议(2PL):事务分为加锁阶段和解锁阶段,加锁阶段可以不断申请锁但不能释放锁,解锁阶段可以释放锁但不能再申请锁。
- 时间戳技术:
- 为每个事务分配一个唯一的时间戳,根据时间戳的顺序来控制事务的并发执行,避免事务之间的冲突。
- 时间戳排序方法包括基本时间戳排序和多版本时间戳排序。
- 多版本并发控制(MVCC):
- 通过为每个数据对象维护多个版本,允许事务在不互相阻塞的情况下并发执行。
- 包括快照隔离和可序列化隔离,快照隔离在事务开始时创建数据的快照,事务在执行期间读取快照数据;可序列化隔离则通过维护数据对象的多个版本,实现事务的序列化执行。
- 乐观并发控制(OCC):
- 假设数据冲突很少发生,事务在执行期间不进行锁定,提交时再检查冲突。
- 如果发现冲突,则回滚事务并重新执行。
隔离级别
隔离级别(Isolation Level)是指多个事务并发执行时,为保证数据的一致性和正确性所采用的控制方案。不同的隔离级别决定了数据库系统如何以及在多大程度上保证事务之间的隔离和并发执行。
隔离级别决定了事务在并发环境中访问和修改数据时的可见性和一致性。数据库的隔离级别主要用来处理并发控制问题,避免脏读、不可重复读、幻读等现象。
读未提交
读未提交(Read Uncommitted)
是数据库事务隔离级别中的最低级别。在读未提交的隔离级别下,一个事务可以读取另一个事务尚未提交的修改。
在读未提交级别下,事务可以读取到其他事务所做的临时修改,即使这些修改最终可能会被回滚或不提交。
读未提交的隔离级别虽然可以提供最高的并发性和读取性能,但同时也带来了一些严重的问题,包括:
脏读
(Dirty Read)不可重复读
(Non-Repeatable Read)幻读
(Phantom Read)
因为读未提交级别的问题比较严重且不符合数据的一致性要求,因此在实际应用中较少使用。
读已提交
读已提交
(Read Committed):在读已提交的隔离级别下,一个事务只能读取到已经提交的其他事务所做的修改,一个事务所做的修改在提交之前对其它事务是不可见的。
读已提交级别解决了读未提交级别的脏读问题,确保了事务之间的数据隔离性和一致性。在这个级别下,每次读取都只能读取到已提交的数据,从而避免了读取到不一致或无效数据的情况。
但是,读已提交级别仍然可能导致不可重复读(Non-Repeatable Read)和幻读(Phantom Read)的问题
读已提交级别下的并发读取仍然存在竞争条件和问题。多个事务可以同时读取同一数据,并且其他事务可以在读取操作之间修改该数据。因此,读已提交级别可能导致一些不一致的结果,但可以避免脏读。
如果应用程序需要更高的数据一致性,可以考虑使用更严格的隔离级别,如可重复读或可串行化。但请注意,更严格的隔离级别可能会带来更多的锁争用和性能开销。需要根据具体应用场景和需求进行权衡和选择。
可重复读
可重复读
(Repeatable Read):在可重复读的隔离级别下,事务在执行过程中看到的数据集是一致的,即便其他事务对数据进行了修改,事务也不会读取到这些修改。
在可重复读级别下,一个事务在开始时会创建一个一致性视图,该视图包含事务开始时数据库中的所有数据。事务读取的数据都是基于该一致性视图,不受其他并发事务对数据的修改影响。即使其他事务对数据进行了插入、更新或删除操作,事务读取的数据仍然保持一致。
可重复读级别解决了读已提交级别的不可重复读问题,确保了在同一个事务中多次读取同一条记录时,得到的结果是一致的。但是,可重复读级别仍然可能导致幻读(Phantom Read)的问题
可串行化
可串行性是并发事务正确调度的准则,即一个给定的并发调度,当且仅当它是可串行化的,才认为是正确调度
串行化调度
可串行化调度
(Serializable Schedule),多个事务并发执行的情况下,系统能够保证各个事务的执行结果和以某种顺序串行执行的结果一致的调度方式。
简而言之,就是系统对并发执行的多个事务进行调度时,当且仅当其结果与按某一次序串行地执行这些事务时的结果相同。在可串行化调度下,多个事务虽然并发执行,但是它们的操作顺序看上去像是在一个串行的时间轴上运行。
为了实现可串行化调度,数据库系统应该满足 ACID
(原子性
、一致性
、隔离性
、持久性
)的特性,并通过各种 锁机制
、MVCC
(多版本并发控制)等技术来解决并发操作的问题,确保每个事务操作执行的正确顺序。具体来说,可串行化调度需要满足以下两个条件:
-
等价原则:系统执行的结果和所有可能的串行执行结果相等。
-
视图可重构性:系统的执行结果不受事务提交的顺序影响。
可串行化 Serializable
可串行化(Serializable)是数据库事务隔离级别中的最高级别,也是最严格的隔离级别。在可串行化的隔离级别下,事务串行执行,保证了最高级别的数据隔离性和一致性。
在可串行化级别下,一个事务在执行期间会对所涉及的数据进行锁定,防止其他事务对这些数据进行并发的读取、写入、插入或删除操作。这意味着其他事务无法与正在执行的事务并发地操作相同的数据。
可串行化级别解决了读已提交和可重复读级别中的脏读、不可重复读和幻读问题。由于事务串行执行,一个事务只能看到其他事务已提交的数据,不会读取到其他事务未提交的数据。同时,其他事务也无法修改已被锁定的数据,从而保证了数据的一致性。
尽管可串行化级别提供了最高的隔离性,但也可能带来较高的性能开销。因为并发执行被限制了,事务需要等待其他事务释放锁才能继续执行,可能导致性能下降。
通常情况下,只有在特殊的需求下才会选择可串行化级别,例如对数据一致性要求非常高、并发冲突风险较大的场景。
大多数应用场景下,Repeatable Read
或 Read Commited
级别已经能够满足数据隔离和一致性的需求。在选择隔离级别时,需要仔细评估应用的需求、并发访问模式和性能要求。
冲突可串行化
冲突操作:是指不同事务对同一个数据的读写操作和写写操作。除此之外,其他操作均为不冲突操作
-
事务 T i T_i Ti 读数据 X X X ,事务 T j T_j Tj 写数据 X X X
-
事务 T i T_i Ti 写数据 X X X ,事务 T j T_j Tj 写数据 X X X
-
不同事务的冲突操作不可交换
-
同一事务内部的两个操作不可交换
-
不同事务,同一数据的读读操作可以交换
-
不同事务,不同数据,无论读写均可交换
可串行化调度的充分条件是冲突可串行化
冲突可串行化:一个调度 S C SC SC 在保证冲突操作的次序不变的情况下,通过交换两个事务不冲突操作的次序得到另一个调度 S C ‘ SC^` SC‘ 。如果 S C ‘ SC^` SC‘ 是串行的,则称调度 S C SC SC 为冲突可串行化的调度。若一个调度是冲突可串行化的,那么它一定是可串行化的调度
多版本并发控制
多版本并发控制(Multi-Version Concurrency Control,MVCC)
是一种用于实现数据库并发控制的技术。它通过在数据库中维护多个数据版本来达到事务的隔离,并允许事务并发执行而不会相互影响。
MVCC
也是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现 Read Commited
和 Repeatable Read
这两种隔离级别。而 Read Uncommited
总是读取最新的数据行,无需使用 MVCC。Serializable
隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
MVCC
的基本思想是,在每次数据修改操作(如插入、更新、删除)时,将新的数据版本存储在数据库中,而不是立即覆盖原有的数据版本。每个事务在开始时创建一个时间戳(如事务开始的时间),该时间戳被用于标识该事务所能看到的数据版本。
当一个事务读取数据时,它只能看到那些在其开始时间之前已经提交的数据版本。这意味着每个事务都能看到一个一致性的数据视图,即使其他事务正在并发地修改或删除数据。事务读取的数据版本不受其他事务未提交修改的影响,从而解决了 可重复读
级别下的“不可重复读”和“幻读”的问题。
MVCC
通过在每个数据版本上维护一些元信息(如版本号、创建时间戳、销毁时间戳等)来实现事务隔离。
数据库系统根据事务的时间戳和数据版本的元信息来决定事务能否读取或修改数据,并且在并发操作中进行冲突检测和解决。
MVCC
的好处是能够提供较高的并发性能,因为读操作不会互斥地锁定数据,多个事务可以并发地读取相同的数据。然而,它也需要额外的存储空间来保存多个数据版本,并在查询时进行版本的选择和判断。
MVCC 是许多现代数据库系统的核心技术,如 PostgreSQL
、MySQL 的 InnoDB
引擎等,它们使用 MVCC 来实现不同的隔离级别,并提供高效的并发控制和数据一致性。
版本号
版本号是一种元数据,用于标识数据的不同版本。
在 MVCC 中,每个数据版本都会分配一个唯一的版本号,事务读取数据时使用该版本号来决定读取哪个版本的数据。版本号通常作为数据的隐藏列存储在数据库中,对应于每个数据行都会有一个版本号。
系统版本号
在数据库中,系统版本号是指标识一个数据库管理系统发布的版本的编号或标识符。
通常情况下,数据库系统版本号由数字、字母等组成,遵循一定的命名规则或约定。不同的数据库管理系统各自具有不同的版本号格式和命名规则。每开始一个新的事务,系统版本号就会自动递增。
事务版本号
事务版本号是指在事务处理过程中,为了实现并发控制和隔离性,为每个事务标识的唯一编号或标识符,为事务开始时的系统版本号。
在并发执行的事务中,事务版本号用于确保事务之间的隔离和一致性,避免读取到其他事务未提交的数据或读取到已被其他事务修改的数据。
事务版本号可以通过数据库管理系统或事务管理机制来生成和管理,以确保事务的序列化顺序和隔离性。
快照
快照是一个时间点的数据库图像(副本),用于事务执行过程中获取一致性的数据视图。
在 MVCC 中,每个事务都会建立一个快照,该快照会记录事务开始时间点的数据库状态(所有表的内容和状态),包括当前未提交的事务产生的数据版本。在事务执行过程中,事务只能看到快照中记录的最新数据版本。
快照通常用于数据备份、故障恢复、数据历史查询和一致性读取等目的。通过创建数据库的快照,可以将数据库还原到某个特定时间点的状态,或者用于生成数据报表等操作。
Undo 日志
在数据库中,Undo日志(Undo Log)
是一种记录事务操作的数据结构。它记录了在事务操作中所做的修改的逆操作,在回滚、并发控制以及恢复系统中起到重要作用。
当一个事务修改了数据库中的数据时,Undo日志 会记录这个修改前的数据,即旧值。如果事务需要回滚,或者系统需要将数据回滚到某个特定的时间点,就可以使用这些旧值将数据回滚到原始状态。另外,Undo日志也可以用于并发控制,防止事务之间的修改相互干扰。
在数据库中,Undo日志 通常是通过两种方式实现:
-
在更新数据前将旧数据写入 Undo日志,然后将新值写入磁盘,以确保数据更新的原子性。如果事务需要回滚,就可以使用 Undo日志 中的旧值来还原数据。
-
通过 MVCC 技术,在每个事务完成之后,将其修改的数据和旧值保存在Undo日志中,以便其他事务可以读取其中的旧值。该日志通过回滚指针把一个数据行(Record)的所有快照连接起来
数据隐藏列
数据隐藏列是指在数据库表中定义的列,这些列不会直接对外暴露,普通用户无法直接访问或操作它们。隐藏列通常用于存储一些辅助信息或元数据,这些信息对于数据库系统内部的操作和控制非常重要,但对于普通用户来说是不可见的。
隐藏列在多种数据管理场景中都有广泛的应用:
-
版本号列:在实现多版本并发控制(MVCC)的数据库中,每个数据版本会关联一个版本号,这个版本号通常作为隐藏列存储在表中。它用于标识数据的不同版本,以实现事务隔离和读取一致性。
-
创建时间戳和更新时间戳列:这些隐藏列用于记录数据的创建时间和上次更新时间。它们可以用于数据审计、版本追踪和性能优化等目的。
-
删除标志列:在逻辑删除的场景中,可以添加一个隐藏的逻辑删除标志列来表示数据是否被删除。这样可以避免物理删除,而是通过更新隐藏列的值来实现逻辑删除的功能。
如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
-
数据行版本列:在某些情况下,为了实现数据的同步和冲突检测,可以添加一个隐藏的数据行版本列。
指示创建一个数据行的快照时的系统版本号,该列存储一个递增的整数值,每当数据行发生变化时,版本号就会增加,从而确保其他操作不会同时修改相同的数据行。
隐藏列可以通过数据库管理工具或查询语言来访问和操作,但普通用户在进行数据操作时通常无需关注或直接使用它们,因为它们主要是为了提供一些内部的管理和控制功能。隐藏列的存在提供了一种有效的方式来扩展数据库模式,加强数据的管理和控制能力。
在 Repeatable Read 级别下实现 MVCC
当开始新一个事务时,该事务的版本号一定大于当前所有数据行快照的创建版本号
-
SELECT
:多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
把没有对一个数据行做修改的事务称为
T
,T
所要读取的数据行快照的创建版本号必须小于T
的版本号,因为如果大于或者等于T
的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,
T
所要读取的数据行快照的删除版本号必须大于T
的版本号,因为如果小于等于T
的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 -
INSERT
:将当前系统版本号作为数据行快照的创建版本号 -
DELETE
:将当前系统版本号作为数据行快照的删除版本号 -
UPDATE
:将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。
快照读和当前读
在数据库系统中,快照读和当前读是读取数据的两种方式。
经常使用的快照读取包括数据库中的多版本并发控制(MVCC)和快照隔离级别(Snapshot Isolation Level)等。而当前读取则通常应用于不需要特殊隔离级别的事务中。
快照读
快照读(Snapshot Read)是指在一个事务中按照一个固定时间点的视角来读取数据,也就是读取的是该事务开始之前的数据快照,减少加锁带来的开销。
select * from table ...;
在进行快照读取时,不会读取其他正在进行修改的事务的数据。这种方式的读取不会阻塞其他事务,因此可以保证高并发性和数据的一致性。但是,由于读取的是先前的数据快照,所以可能读取到已经更新或删除的数据。
当前读
当前读(Current Read)是指在一个事务中按照当前时间点的视角来读取数据,也就是读取的是最新的数据,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update;
delete;
在进行当前读取时,如果发现另一个事务正在修改已被读取的数据,则会被阻塞,直到该事务完成或回滚。这种方式的读取确保了读取数据的准确性,但可能会对并发性能产生影响。
总结
数据库并发控制是确保数据一致性和隔离性的关键技术。通过合理的封锁策略、并发调度协议以及适当的隔离级别,可以有效地解决并发带来的问题。在实际应用中,应根据具体需求选择合适的并发控制策略,以平衡并发度和数据一致性。