数据库的控制功能
数据库的控制功能主要指对数据库中数据的管理和维护,确保数据的安全性、完整性和一致性。数据库控制功能主要包括:并发控制、性能优化、完整性约束、以及备份与恢复。
一、并发控制
(一) 事务
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):在事务读取数据时加锁,其他事务可以读取同一数据,但不能进行写操作。
二、性能优化
(一) 设计优化
数据库范式层次越高,数据库中的冗余信息越少。但规范化使得关系模式不断被拆解,在查询时,需要频繁执行数据表的连接操作。而连接操作比较耗时,导致查询效率低下。
通过反规范化设计,在设计数据库时,合理增加冗余属性,减少连接操作,可以提升数据库性能。常用的反规范化设计如下:
- 增加冗余列:在多个表中具有相同的列,减少查询时的连接操作。
- 增加派生列:在表中增加通过属性计算生成的列,减少查询时的计算量,从而加快查询速度。
- 重新组表:合并经常需要连接的表,减少连接操作。
- 分割表:将关系进行水平或垂直分割,提升并行访问度。
(二) 索引优化
索引是提高数据库查询速度的利器,创建索引时应遵循以下原则:
- 建立索引时,应选用经常进行查询、分组、过滤操作,而不常更新的属性。
- 索引的数目不宜太多,过多的索引会导致 insert、update、delete 语句的执行效率降低。
- 选择性低的属性(即具有较少不同值的字段)不适合建立索引。
- 对于数据量较小的表不必建立索引。
(三) 读写分离
读写分离是将数据库的读写操作分散到不同的数据库节点上,实现小幅提升写性能,大幅提升读性能的效果。
通常情况下,读写分离采用 一主多从 的结构,一个主数据库负责写,多个从数据库负责读。主数据库和从数据库之间会进行数据同步,以保证从数据库中的数据准确性。
以 MySQL 为例,主从复制 的过程如下:
- 主库将数据库中数据的变化写入到主库的 bin log。
- 从库连接主库,从库创建一个 io 线程,请求主库复制 bin log。
- 主库创建一个 dump 线程,向从库发送 bin log。
- 从库的 io 线程接受 bin log,并写入到从库的 relay log 中。
- 从库的 sql 线程读取 relay log,重放事务操作,将主库操作应用到从库上。
主从复制的优势在于:
- 性能提升:通过将读请求分散到多个从数据库,可以减轻主数据库的压力,提升整体系统性能。
- 负载均衡:读请求可以被均匀分配到多个从数据库,避免单点过载。
- 高可用性:如果主数据库出现故障,可以快速切换到从数据库,增强系统的可用性;当主数据库上数据丢失时,可以用从数据库进行恢复。
- 可扩展性: 可以通过增加更多的从数据库实例来应对不断增长的读请求。
(四) 分区分片
数据库分区和分片都是为了提高数据库的性能、可扩展性和管理效率,但它们的实现方式有所不同。
- 分区:将一个大表分成多个小的、逻辑上独立的部分,这些部分被称为分区。分区通常在同一个数据库实例内进行,分区的数据结构和表的结构保持一致。
- 分片:将数据库拆分成多个独立的数据库实例,每个实例称为一个分片。每个分片可以在不同的服务器上运行,因此可以跨物理机器分布数据。
分区和分片的区别如下:
特征 | 分区 | 分片 |
---|---|---|
划分维度 | 逻辑上的划分 | 物理上的划分 |
存储范围 | 通常在单个数据库实例内 | 可以跨多个数据库实例或服务器 |
使用目的 | 优化数据库查询性能 | 提高数据库可扩展性和并发性 |
实现方式 | 使用 PARTITION BY 子句 | 使用多个物理表 |
管理难度 | 相对简单 | 相对复杂 |
(五) 冷热分离
冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据:
- 冷数据 是指不经常访问,对当前项目价值较低,但需要长期保存的数据。一般存储在存储在低成本低性能的介质中(如 HDD)。
- 热数据 是指经常被访问和修改,且对访问速度要求较高的数据。一般存储在高性能存储介质中(如 SSD)。
三、完整性约束
数据库的完整性约束是确保数据库中数据准确性和一致性的规则和限制,有助于维护数据的可靠性,防止无效或不一致的数据被插入到数据库中。以下是一些常见的数据库完整性约束:
- 实体完整性(主键约束):主键唯一,且不能为 NULL。
- 参照完整性(外键约束):外键的取值必须在关联表的主键中存在,或者为 NULL。
- 触发器:在数据被插入、更新或删除之前或之后执行 SQL 代码,以确保数据的完整性。
四、备份与恢复
(一) 备份
数据库备份方式有多种:
- 按备份实现方式分类
- 物理备份:在操作系统层面上对数据库的物理文件(数据文件、日志文件等)进行备份,适合大型数据库系统。
- 冷备份:在数据库系统完全停止运行的情况下进行备份。
- 热备份:利用备份软件,在数据库系统运行的情况下进行备份。
- 逻辑备份:指利用 DBMS 自带的工具软件备份数据库的内容(表结构,表数据等),适合数据库容量不大的情况。
- 物理备份:在操作系统层面上对数据库的物理文件(数据文件、日志文件等)进行备份,适合大型数据库系统。
- 按备份数据量分类
- 完全备份:备份数据库中的所有数据。
- 增量备份:备份自上次备份(无论是全备份还是增量备份)以来发生变化的数据。
- 差异备份:备份自上次全备份以来发生变化的数据。
(二) 恢复
数据库日志在数据库恢复过程中发挥了重要作用。以 MySQL 数据库为例,MySQL 有三种日志:
Undo Log(回滚日志)
作用: Innodb 存储引擎层生成的日志,记录事务未完成时的数据状态,允许在事务回滚时撤销对数据库的修改,保证了事务的 原子性 。
使用场景: 在事务发生错误或被用户回滚时,通过 逆向扫描 undo log,撤销已执行的操作,确保数据恢复到修改之前的状态。Redo Log(重做日志)
作用: Innodb 存储引擎层生成的日志,当事务提交后,数据会被写入到 redo log 中,以便在系统崩溃后恢复未完成的事务,保证了事务的 持久性 。
使用场景: 在数据库崩溃或意外关闭后,通过 正向扫描 redo log,重新执行已提交的事务,确保所有已修改的数据不会丢失。Bin log(二进制日志)
作用: Sever 层生成的日志,记录所有对数据库的更改操作,包括 DDL(数据定义语言)和 DML(数据操作语言),主要用于数据同步和恢复。
使用场景: 在主从复制中,bin log 用作数据传输的依据;在备份恢复时,可以根据 bin log 重放操作,以恢复数据到特定时间点。