软考架构——数据库的控制功能
数据库的控制功能主要指对数据库中数据的管理和维护,确保数据的安全性、完整性和一致性。数据库控制功能主要包括:并发控制、性能优化、完整性约束、以及备份与恢复。
一、并发控制
(一)事务
DBMS 运行的基本工作单位是事务,事务是用户定义的一个数据库读写操作序列,这些操作序列要么全做,要么全不做,是一个不可分割的工作单位。
事务具有以下四个基本特性,通常被称为ACID 特性:
- 原子性(Atomicity):事务中的所有操作是一个不可分割的整体,要么全部完成,要么全部不完成,不能部分地完成。一旦某一步执行失败,就会全部回滚到初始状态。
- 一致性(Consistency):事务必须使数据库从一个一致性状态转换到另一个一致性状态。这意味着事务执行过程中和执行结束后,数据库中的数据必须满足所有预定义的规则和约束。
- 隔离性(Isolation):一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的事务之间不会相互影响。多个事务并发运行,但看上去像串行调度执行一样。
- 持久性(Durability):一旦事务被提交,它对数据库的修改就是永久性的,即使系统发生故障也不会丢失。
一致性是事务 ACID 四大特性中最重要的属性,而原子性、隔离性和持久性,都是作为保障一致性的手段。
(二)数据不一致
如果数据不一致,那么在并发“读 - 写”或并发“写 - 写”的场景,容易出现以下问题:
1. 修改丢失
T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。
T1 | T2 |
---|---|
读 A=10 | |
读 A=10 | |
A=A-5,写回 | |
A=A-8,写回 |
T1 对 A 的修改被 T2 覆盖,T1 的修改不起作用。
2. 脏读
T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
T1 | T2 |
---|---|
读 A=20 | |
A=A+50,写回 | |
读 A=70 | |
rollback | |
A=20,写回 |
T1 对 A 进行修改,但之后回滚撤销,T2 读取的数据与数据库中不一致,是错误的数据(脏数据)。
3. 不可重复读
T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
T1 | T2 |
---|---|
读 A=20 | |
读 B=30 | |
计算 A+B=50 | |
读 A=20 | |
A=A+50,写回 | |
读 A=70 | |
读 B=30 | |
计算 A+B=100 |
T1 读取数据并进行计算,T2 更新了数据,T1 再次读取数据,然而计算结果不一致。
4. 幻读
T1 读取某个范围的数据,T2 在这个范围内插入或删除数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
(三) 隔离级别
为了解决数据不一致的问题,数据库中的事务隔离分为如下四种:
- 读未提交:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读。
- 读已提交:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库默认的隔离级别。
- 可重复读:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以防止脏读和不可重复读,但是幻读仍有可能发生。这是 MySQL 数据库默认的隔离级别。
- 串行化:最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行。
隔离级别 | 导致脏读 | 导致不可重复读 | 导致幻读 | 实现机制 |
---|---|---|---|---|
读未提交 | Y | Y | Y | 无 |
读已提交 | N | Y | Y | 读锁 |
可重复读 | N | N | Y | 读锁 +MVVC |
串行化 | N | N | N | 对所有数据行加锁 |
隔离级别从上到下越来越严格,并发造成的数据不一致可能性越小,但需要付出的性能代价越大。
(四) 锁
数据库的隔离级别是通过锁机制来实现的。从不同的角度对锁进行分类:
- 根据 性能
- 乐观锁:通过版本对比来实现并发控制,假设事务之间不会发生冲突,直到提交操作时才会检查是否有冲突。
- 适用于并发冲突较少的场景。
- 悲观锁:假设事务之间会发生冲突,因此在访问数据之前就会加锁,保证同一时间只有一个事务能够访问数据。
- 适用于并发冲突较多的场景。
- 乐观锁:通过版本对比来实现并发控制,假设事务之间不会发生冲突,直到提交操作时才会检查是否有冲突。
- 根据 对数据库的操作粒度
- 表锁:锁定整个表,在事务操作时会锁定整张表,影响表中所有数据。
- 每次操作锁住一张表。加锁的开销小,加锁快,不会出现死锁;锁定粒度大,容易发生锁冲突,并发度最低。适用于整张表数据迁移的场景。
- 行锁:只锁定某行数据,使其他事务无法修改该行数据,但不影响表中其他数据的访问。
- 每次操作只锁住表中一行数据。加锁的开销大,加锁慢,可能会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
- 表锁:锁定整个表,在事务操作时会锁定整张表,影响表中所有数据。
- 根据 对数据库的操作类型
- 读锁 ( 共享锁,S 锁 Shared):在事务读取数据时加锁,其他事务可以读取同一数据,但不能进行写操作。
- 事务 T1 对数据库添加 S 锁,事务 T1 可以进行读写,事务 T2 只能读不能写。
- 写锁 ( 排它锁,X 锁 eXclusive):在事务对数据进行写入或修改时加锁,阻止其他事务对该数据的读写操作。
- 事务 T1 对数据库添加 X 锁,事务 T1 可以进行读写,事务 T2 无法读写。
- 读锁 ( 共享锁,S 锁 Shared):在事务读取数据时加锁,其他事务可以读取同一数据,但不能进行写操作。