数据库设计的范式
什么是范式
范式——数据表设计的基本原则、规则
范式的分类
范式分为6大类。数据库的范式设计越高阶,冗余度就越低,同时高阶的范式一定符合低阶范式的要求。平常的数据库设计中,最多到BCNF,普遍是3NF。
第一范式(1NF)
强调属性的原子性,即属性不可再分。比如一张表中,“用户信息”这个字段就可以拆分成电话、住址等更小颗粒度的字段,所以这张表不符合数据库设计对第一范式的要求。如果像这样,存在不完全依赖,可以拆分成多个表,主体和这些表形成一对多的关系。
第二范式(2NF)
在满足第一范式的基础上,还要满足数据表里的每一条数据记录,都是可唯一标识的。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。也就是说,其他数据项和主键之间是完全依赖的关系。
第三范式(3NF)
非主键和其他非主键直接没有间接或直接依赖关系。也就是,不能出现A->B->C这样的关系。
BCNF(巴斯范式)
主要是在3NF上做了一些改进,降低了数据库冗余度。
第四范式(4NF)
删除了同一张表中的多对多关系
第五范式(5NF、完美范式)
第五范式是在第四范式的基础上做的进一步规范化。第四范式处理的是相互独立的多值情况,而第五范式则处理相互依赖的多值情况。
优缺点
优点:数据的标准化有助于消除数据库中的数据冗余。
缺点:降低了查询效率,因为范式等级越高,设计出来的表就越多,进行数据查询的时候就可能需要关联多张表,不仅代价昂贵,而且可能会使得一些索引失效。
第三范式通常被认为在性能,扩展性和数据完整性方面达到了最好的平衡。
反范式化
在满足业务需求的前提下,提高查询效率,用空间换实际。只有当冗余数据能大幅度提升查询效率的时候,才会反范式化。并且,这个冗余满足:1.不需要经常修改、2.查询的时候不可或缺。
事物
什么是事物
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。例如:在关系数据库中,一个事务可以是一条SQL语句,一组SQL语句或整个程序。事务是恢复和并发控制的基本单位。
事物的特性
原子性
一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
一致性
事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
隔离性
一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性
也称永久性。一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
***事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于高“可用性”的范畴。因此,事务只能保证数据库的高“可靠性”,而高“可用性”需要整个系统共同配合实现。
理解事物
例如,去银行取钱这个事物中,包含着几个步骤,像交钱给工作人员,填单子,签字,确认并存入等等。但是,如果在某个步骤中出现了bug,为了保证交出去的钱还能拿回来,就有了数据库中事物的这个术语,也就是要么成功,要么失败,并恢复原状。
常用事物的分类
扁平事物
是实际生产环境中最常用、最简单的事务类型。事务从BEGIN WORK开始,从COMMIT WORK或ROLLBACK WORK结束。发生错误时回滚到事务的起始位置,无法回滚部分操作。
带有保存点的扁平事物
因为普通的扁平事物回滚所有的操作开销太大,这种事务能设置多个保存点,当发生错误时可以回滚到事务中指定的保存点,而不需要将整个事务回滚。
高并发中可能出现的问题
脏读
指一个事务在处理过程中,读到了另一个未提交的事务中的数据。例如,A向B转账100元这个事务需要两条SQL语句,首先给B的账户添加100元,A的账户减少100元,假如执行完第一条SQL语句后发生了阻塞,此时B对其账户进行查询操作,发现其账户已经多出了100元,但是此时事务阻塞结束,执行第二条语句时,发生了问题,这时数据库就需要回滚数据,返回事务执行之前的状态,那么B的账户会再减少100元,那么之前B查询到的多出100元的结果就是脏读。
不可重复读
指在一个事务中读取了两次同一个数据,但是结果不一致,这是因为这个事务两次查询操作之间的时候被另外一个事务修改了数据。举个栗子,事务A进行了两次查询账户的操作,事务B对账户进行了修改操作,改为减少100元,那么在A事务第一次查询账户后如果发生阻塞,B事务此时开启并执行,那么再执行A事务的第二次操作时,就会发现账户少了100元,这就是发生了不可重复读。
脏读是A事务读取了B事务未提交的数据,不可重复读是读取了B已提交的数据。
幻读
幻读是指在一个事务的操作后发现了未被操作的数据。比如A事务想把所有人的账户数据全部改为100元,但此时事务B对该表进行了一个插入操作,增加了一个人Q和账户200元,也就是表增加了一行,那么A事务执行完毕后,会发现有一个人的账户未被修改过,这就是发生了幻读。
幻读和不可重复读都是读取了另一个已经提交的事务,不可重复读的重点在于update和delete,而幻读的重点是insert。
如何避免
隔离级别有4个,由低到高依次为Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读 这几类问题。
Read uncommitted 读未提交
在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。
因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。
Read committed 读提交
在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。
Repeatable read 重复读
在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。
Serializable 序列化
该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。
乐观锁&悲观锁
乐观锁
用于低冲突环境,假设事物之间不怎么发生冲突。在乐观锁中,数据读取和写入之间不会锁定资源,但会在数据提交时检查是否有冲突。
例子
假设有一个在线图书商店,记录每本书的库存数量。使用乐观锁来管理库存更新的过程如下:
读取数据:用户A查看某本书的库存量为10本。
业务处理:用户A决定购买这本书,系统计划将库存减少1。
在提交前检查冲突:在用户A提交订单之前,系统检查自用户A读取数据后库存是否发生变化。如果库存仍然是10本,说明没有冲突,系统将库存更新为9本并完成订单。如果库存已经不是10本(比如另一个用户B已经购买了一本),系统会重新读取当前库存并要求用户A重新确认订单。
悲观锁
悲观锁适用于高冲突环境,它假设冲突很可能发生,并在数据处理过程中锁定资源以防止其他事务的干扰。
例子
还是那个在线图书商店。
锁定数据:用户A想要购买一本书。系统在读取库存信息的同时,对这条记录加锁,防止其他用户对这个库存记录进行修改。
业务处理:系统读取库存数量(比如10本),用户A决定购买这本书,系统计划将库存减少1。
提交并解锁:系统更新库存为9本,并完成用户A的订单。随后,释放对这条记录的锁定,使得其他用户可以访问和修改这个库存记录。