05 数据库管理与维护
事务
基本概念
- 事务是由用户定义的一系列数据操作语句构成,这些操作语句要么全部执行要么全部不执行,是数据库运行的最小的、不可分割的工作单位。
- 所有对数据库的操作都是以事务为一个整体单位来执行或撤销,同时事务也是保证数据一致性的基本手段。
- 无论什么情况下,DBMS 都应该保证事务能正确、完整的执行。在关系数据库中,一个事务可以是一条 SQL 语句、一组 SQL 语句或整个程序。
事务的特性
事务由有限的数据库操作序列组成,但不是任意的操作序列能成为事务,它必须同时满足一下四个特性(ACID):
- 原子性 Atomicity:是指一个事务对于数据的所有操作都是不可分割的整体,这些操作要么全部执行,要么全部不执行。原子性是事务概念本质的体现和基本要求
- 一致性 Conistency:是指事物执行完成之后,数据库中的内容必须全部更新,确保事物执行后使数据库从一个一致性状态变成另一个一致性状态,此时数据库中的数据具备正确性和完整性。当数据库只包含事务成功提交的结果,则说明数据库处于一致性状态;如果数据库系统运行过程中发生了故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的更新操作有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说不一致的状态,为了保证一致性,系统会对事务中对数据库的所有已完成的操作全部撤销,回滚到事务开始时的一致性状态。
- 隔离性 Isolation:隔离性也成为独立性,表明一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持续性 Durability:持续性也称独立性,表明一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰
事务是并发控制的基本单位,保证事务的 ACID 特性是事务处理的重要任务。事务 ACID 特性可能遭到破坏的因素一般有两种:
- 多个事务并行运行时,不同事务的操作交叉执行。此时 DBMS 必须保证多个事务的交叉运行不影响这些事务的原子性
- 事务在运行过程中被强制停止。此时 DBMS 必须保证被强行停止的事务对数据库和其他事务没有影响
并发控制
例:学生去校园超市买洗衣粉,学生 a 买了 3 袋,学生 b 买了 4 袋;学生 a 去了收银台 A 结账,学生 b 去了收银台 B。假设现在超市的洗衣粉库存是 20 袋,收银台 A 和收银台 B 同时查看库存,为 20 袋,然后执行出库,收银台 A 修改库存为 20-3=17 袋并保存,收银台 B 修改库存为 20-4=16 并保存。此时会发生什么?
- 收银台 A 的库存修改被收银台 B 的修改所覆盖
- 或者收银台 B 的库存被收银台 A 的修改所覆盖
这种情况称为数据不一致,是由并发执行引起的。
并发操作导致的问题
(1)丢失修改
- 丢失修改是指当两个或两个以上的事务在更新同一数据值时,会发生某些修改被覆盖(丢失)的问题
- 产生这种异常的主要原因是由于每个事务都不知道其他事务的存在,自己的更新操作可能重写其他事务所做的更新,这将导致数据丢失。
(2)读脏数据
- 读“脏”数据是指一个事务读取了另一个事务运行失败过程中的数据。也就是说,事务 T1 更新了某一数据,并将更新结果写入数据库,事务 2 读取了这一数据(T1 更新后的数据)。但由于某种原因 T1 撤销了更新操作,T1 修改过的数据又恢复为原值,此时 T2 读取的数值与数据库中实际数据值不一致。这种数据就是"脏"数据,脏,是指过时。
- 回到前面的例子。收银台 A、B 同时修改洗衣粉数量,收银台 A 修改洗衣粉数量为 17,并修改库存,此时收银台 B 开始结账,读取库存为 17。然后学生 a 不要洗衣粉了,收银台 A 执行事务回滚,将库存恢复为原值 20,而收银台 B 仍然在使用的是已经过时的 17,也就是“脏”数据。
(3)不可重复读
- 不可重复读是指事务 T1 读取数据后,事务 T2 对该数据进行读取并执行更新操作,修改了 T1 读取的数据,T1 操作完数据后,又重新读取这个数据,会发现读到的结果与前一次不一样。
- 回到前面的例子。收银台 A 在某一时刻读取的库存是 20 袋,过了一段时间,收银台 B 卖出 4 袋将数量更新为 16,此时收银台 A 再读取库存,读到的值不再是前面读到的 20,而是 16。
- 这种前后两次读取数据,得到不一致的值的情况,就是不可重复读。
(4)幽灵数据
- “幽灵”数据是不可重复读的一种特殊情况。它是指当事务 T1 按一定条件从数据库中读取某些数据后,事务 T2 在其中插入或删除数据,当 T1 再次按相同条件读取数据时,发现数据库中多出了一些数据或者之前的数据消失了,这些数据对于 T1 来说就是"“幽灵"数据。
并发控制的方法
原因:由于并发操作存在可能破坏 ACID 特性的问题,因此要控制并发,使其不能影响数据库的数据一致性
实现并发控制的主要方法是使用封锁机制,锁可以防止事务的并发问题,在多个事务并发执行时能够保证数据库的完整性和一致性。
封锁是指:一个事务 T 在对某个数据对象进行操作之前,先向系统发出请求,对其加锁。加锁后事务 T 对该数据对象有一定的控制权,在事务结束后释放锁加锁后事务 T 对该数据对象有一定的控制权,在事务结束后释放锁,而在事务 T 释放锁之前,其他事务不能更新此数据对象,以保证数据操作的正确性和一致性。封锁是一种并发控制基数,是一种用来调整对数据库中共享数据进行并行存取的技术。
前面超市的例子中,当收银台 A 要修改洗衣粉数量,在读取出数量前先封锁数量,再对数量进行读取和修改操作,这时收银台 B 就不能读取和修改,直到收银台 A 完成操作,将修改后的数量重新写回数据库,并释放对数量的封锁后,收银台 B 才可以读取和修改,这样就不会导致数据不一致的问题。
锁的类型
具体的控制由封锁的类型决定。基本的封锁类型有两种:排他锁(Exclusive Locks,简称 X 锁)和共享锁(Share Locks,简称 S 锁)
- 排他锁又称写锁,可以使事务对数据进行访问时,其他事务不能读取或更新锁定的数据。如果事务 T 对数据对象 R 加上 X 锁,则只允许事务 T 读取和更新 R,其他任何事务不能再对 R 加任何类型的锁,直到事务 T 释放 R 上的锁。这就保证了其他事务在 T 释放 R 上的锁之前不能再读取和更新 R。由此可见,X 锁采用的方法是禁止并发操作。
- 共享锁又称读锁,允许并发事务读取数据。如事务 T 对数据对象 R 加上 S 锁,则事务 T 能读取 R 但不能修改 R,其他任何事务只能再对 R 加 S 锁,而不能加×锁,直到事务 T 释放 R 上的 S 锁。这保证了其他事务可以读取 R,而不能在释放 R 上的 S 锁之前对 R 进行修改操作。(所有操作只能读,直到解开 S 锁)
排他锁与共享锁的控制兼容性如下:
T2\T1 | X | S | 无 |
---|---|---|---|
X | 否 | 否 | 是 |
S | 否 | 是 | 是 |
无 | 是 | 是 | 是 |
封锁技术
在使用排他锁和共享锁对数据对象进行加锁时,还需要约定一些规则:何时申请锁、持续时间、何时释放锁等。这些规则称为封锁协议。
对封锁方式规定不同的规则,就形成了不同级别的封锁协议,不同级别的协议能达到的数据一致性级别也不同。
三级封锁协议
一级封锁协议:
- 是指事务 T 在修改数对象之前必须先对其加 X 锁,直到事务结束(包括正常结束和非正常结束)时才释放。一级封锁协议可以防止更新问题的发生。
- 在一级封锁协议中,如果事务仅仅是读数据而不是更新数据,则不需要加锁。所以一级封锁协议不能保证可重复读和读脏数据。
二级封锁协议:
- 是指在一级封锁协议基础上,加上事务 T 在读取数据之前必须先对其加 S 锁,读取完后立即释放 S 锁。二级封锁协议可以防止数据丢失更新问题,还可以防止读脏数据。
- 在二级封锁协议中,由于事务 T 读完数据后立即释放了 S 锁,所以不能保证可重复读。
三级封锁协议:
- 是指在一级封锁协议基础上,加上事务 T 在读取数据之前必须先对其加 S 锁,读取完成后并不释放 S 锁,直到事务 T 结束才释放。三级封锁协议除了可以防止丢失更新和不读脏数据外,还可以防止不可重复读。
三个封锁协议均规定了对数对象的更新必须加 X 锁,而它们的主要区别在读取操作是否需要申请封锁,何时释放锁。
封锁协议 | X | S | 不丢失更新 | 不读脏数据 | 可重复读 |
---|---|---|---|---|---|
一级封锁协议 | 必须加锁,直到事务结束才释放 | 不加锁 | 是 | ||
二级封锁协议 | 必须加锁,直到事务结束才释放 | 加锁,读取完后立即释放锁 | 是 | 是 | |
三级封锁协议 | 必须加锁,直到事务结束才释放 | 必须加锁,直到事务结束才释放 | 是 | 是 | 是 |
封锁技术可以有效地解决并发操作的一致性问题,但也会带来活锁和死锁等新的问题
活锁
当两个或多个事务请求对同一数据进行封锁时,可能存在某个事务处于永远等待锁的情况,这种情况称为活锁。(可以简单理解为调度时该数据一直在被其他事务处理,而有个事务优先级可能最低即一直处理不到,也就是永远等待)
可以使用操作系统中的一些调度策略,比如最简单的方式就是采用先来先服务的策略。
死锁
在同时处于等待状态的两个或多个事务中,其中每一个事务又在等待其他事务释放封锁后才能继续执行,这样出现多个事务彼此相互等待的状态就称为死锁。
解决死锁的方法【略,可看操作系统篇】
并发的调度可串行性
- 数据库管理系统对并发事务中的操作调度是随机的,不同的调度会产生不同的结果
- 什么样的调度是正确的呢?显然串行调度是正确的。一般来讲,如果多个事务在某个调度下的执行结果与这些事务在某个串行调度下的执行结果相同,那么这个调度也是正确的。
- 虽然以不同顺序串行执行事务可能会产生不同的结果,但不会将数据库置于不一致的状态。
- 多个事务的并发执行什么情况下是正确的?当且仅当结果与按某一顺序串行化地执行这些事务时的结果相同。这种调度策略称为可串行化的调度。
- 可串行性时并发事务正确调度的准则。这个准则规定,一个给定的并发调度,当且仅当它可串行化时,才认为它是正确的调度。为保证并发操作的正确性,数据库管理系统的并发控制机制必须提供一定的手段来保证调度的可串行化。
例:假设有两个事务 T1 和 T2,分别包含下列操作:
- 事务 T1:读取 A,B;A=B-3;写回 A;
- 事务 T2:读取 A,B;B=A-3;写回 B;
假设 A、B 的初值均为 20,若按 T1,T2 的顺序执行后,其结果 A=17,B=14;若按 T2,T1 的顺序执行后,则结果 A=14,B=17。当并发调度时,如果执行的结果是这两者之一,则认为都是正确的并发调度策略。
- 第(1)种调度策略,是按照先 T1 后 T2 的顺序执行,执行结果是前面两种情况的第一种,因此是正确的调度。
- 第⑵种调度策略,是按照先 T2 后 T1 的顺序执行,执行结果是前面两种情况的第二种,因此是正确的调度。
- 第(⑶种调度策略,本质上和第一种调度策略一样,也是按照先 T1 后 T2 的顺序执行,执行结果是前面两种情况的第二种,因此是正确的调度。
- 第(4)种调度策略,从结果来看,A=17,B=17,与串行执行的结果不一致,因此是一种错误的调度。错误的原因在于 T1 读 B,T2 读 A,都是读完就释放了 S 锁,如果按照三级封锁协议的要求,就不会出现这种情况。
两段锁协议
为保证并发调度的正确性,数据库管理系统的并发控制机制必须提供一定的手段来保证调度的可串行化。
目前数据库管理系统普遍采用两段锁协议来实现并发调度的可串行化,从而保证调度的正确性。
两段锁协议是指所有的事务必须分为两个阶段对数据对象继续宁加锁和解锁。具体包括两个方面的内容:
- 在对任何数据进行读写操作之前,要先申请并获得对该数据的封锁;
- 在释放一个封锁之后,事务不再申请和获得对该数据的封锁;
两段锁中的两段就是把事务分为两个阶段:
- 第一阶段是申请封锁,在这个阶段,事务可以申请获得任何数据对象上的任何类型的锁,但是不允许释放任何锁;
- 第二阶段是释放封锁,在这个阶段,事务可以释放任何数据对象上的任何类型的锁,但不允许申请任何锁。
如果并发执行的所有事务都遵守两段锁协议,那么这些事务的任何并发调用都是可串行化的(遵守两段锁协议是可串行化调度的充分但不必要条件)
备份
第一种是完全备份。完全备份又称完全数据库备份,将备份整个数据库,不仅包括用户表、系统表、索引、视图、存储过程等所有数据库对象,还包括事务日志部分。完全备份操作简单,便于使用。通常情况对于规模较小的数据库而言,可以快速完成完全备份,但随着数据库规模不断增大,进行一次完全备份,需要花费更多的时间和空间。
第二种是差分备份。差分备份也称增量备份,与完全备份不同,它仅备份自上次完全备份以来数据改变部分的内容。差分备份相比完全备份而言备份速度更快,空间更节省。
如果数据库中的部分对象频繁更改,应使用差分备份,既可以频繁地备份修改,又不会产生完全备份的开销。
第三种是事务日志备份。数据库事务日志是单独的文件,它记录了数据库的改变。事务日志备份是对事务日志进行备份,备份时复制自上次备份以来对数据库所做的改变,仅需要很少的时间,因此建议频繁备份事务日志,从而减少丢失数据的可能性。
用户可以使用事务日志备份将数据库恢复到特定的时间点或恢复到故障点。
事务日志备份和差分备份有所不同。差分备份无法将数据库恢复到出现故障前某一个指定的时刻,它只能将数据库恢复到上一次差分备份结束的时刻。
第四种是文件或文件组备份。数据库由磁盘上的许多文件构成。如果数据库非常大,执行完全备份是不可行的,可以使用文件备份或文件组备份来备份数据库的一部分。