数据库基础


乐观锁和悲观锁

  • 悲观锁:认为数据随时会被修改,因此每次读取数据之前都会上锁,防止其它事务读取或修改数据;应用于数据更新比较频繁的场景;
  • 乐观锁:操作数据时不会上锁,但是更新时会判断在此期间有没有别的事务更新这个数据,若被更新过,则失败重试;适用于读多写少的场景。乐观锁的实现方式有:
    • 加一个版本号或者时间戳字段,每次数据更新时同时更新这个字段;
    • 先读取想要更新的字段或者所有字段,更新的时候比较一下,只有字段没有变化才进行更新

事务的概念和特性

概念:事务(Transaction)是一个操作序列,不可分割的工作单位,以BEGIN TRANSACTION开始,以ROLLBACK/COMMIT结束

特性(ACID):

  • 原子性(Atomicity):逻辑上是不可分割的操作单元,事务的所有操作要么全部提交成功,要么全部失败回滚(用回滚日志实现,反向执行日志中的操作);
  • 一致性(Consistency):事务的执行必须使数据库保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的;
  • 隔离性(Isolation):一个事务所做的修改在最终提交以前,对其它事务是不可见的(并发执行的事务之间不能相互影响);
  • 持久性(Durability):一旦事务提交成功,对数据的修改是永久性的

会出现哪些并发一致性问题?

  • 丢失修改:一个事务对数据进行了修改,在事务提交之前,另一个事务对同一个数据进行了修改,覆盖了之前的修改;
  • 脏读(Dirty Read):一个事务读取了被另一个事务修改、但未提交(进行了回滚)的数据,造成两个事务得到的数据不一致;
  • 不可重复读(Nonrepeatable Read):在同一个事务中,某查询操作在一个时间读取某一行数据和之后一个时间读取该行数据,发现数据已经发生修改(可能被更新或删除了);
  • 幻读(Phantom Read):当同一查询多次执行时,由于其它事务在这个数据范围内执行了插入操作,会导致每次返回不同的结果集(和不可重复读的区别:针对的是一个数据整体/范围;并且需要是插入操作)

数据库的四种隔离级别

  • 未提交读(Read Uncommited):在一个事务提交之前,它的执行结果对其它事务也是可见的。会导致脏读、不可重复读、幻读;
  • 提交读(Read Commited):一个事务只能看见已经提交的事务所作的改变。可避免脏读问题;
  • 可重复读(Repeatable Read):可以确保同一个事务在多次读取同样的数据时得到相同的结果。(MySQL的默认隔离级别)。可避免不可重复读;
  • 可串行化(Serializable):强制事务串行执行,使之不可能相互冲突,从而解决幻读问题。可能导致大量的超时现象和锁竞争,实际很少使用。

常见的封锁类型?

意向锁,排他锁,共享锁

意向锁是 InnoDB 自动加的, 不需用户干预。 对于 UPDATE、 DELETE 和 INSERT 语句, InnoDB 会自动给涉及数据集加排他锁(X); 对于普通 SELECT 语句,InnoDB 不会加任何锁; 事务可以通过以下语句显式给记录集加共享锁或排他锁: 共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE。 其他 session 仍然可以查询记录,并也可以对该记录加 share mode 的共享锁。但是如果当前事务需要对该记录进行更新操作,则很有可能造成死锁。 排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE。其他 session 可以查询该记录,但是不能对该记录加共享锁或排他锁,而是等待获得锁

  • 排它锁(Exclusive Lock)/ X锁:事务对数据加上X锁时,只允许此事务读取和修改此数据,并且其它事务不能对该数据加任何锁;
  • 共享锁(Shared Lock)/ S锁:加了S锁后,该事务只能对数据进行读取而不能修改,并且其它事务只能加S锁,不能加X锁
  • 意向锁(Intention Locks):
    • 一个事务在获得某个数据行对象的 S 锁之前,必须先获得整个表的 IS 锁或更强的锁;
    • 一个事务在获得某个数据行对象的 X 锁之前,必须先获得整个表的 IX 锁;
    • IS/IX 锁之间都是兼容的;
    • 好处:如果一个事务想要对整个表加X锁,就需要先检测是否有其它事务对该表或者该表中的某一行加了锁,这种检测非常耗时。有了意向锁之后,只需要检测整个表是否存在IX/IS/X/S锁就行了

锁的作用:用于管理对共享资源的并发访问,保证数据库的完整性和一致性

什么是封锁粒度

MySQL 中提供了两种封锁粒度:行级锁以及表级锁

封锁粒度小:

  • 好处:锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高;
  • 坏处:系统开销大(加锁、释放锁、检查锁的状态都需要消耗资源)

如何给MySQL加锁

SELECT ... LOCK In SHARE MODE;
SELECT ... FOR UPDATE;

三级封锁协议

  • 一级封锁协议:事务在修改数据之前必须先对其加X锁,直到事务结束才释放。可以解决丢失修改问题(两个事务不能同时对一个数据加X锁,避免了修改被覆盖);
  • 二级封锁协议:在一级的基础上,事务在读取数据之前必须先加S锁,读完后释放。可以解决脏读问题(如果已经有事务在修改数据,就意味着已经加了X锁,此时想要读取数据的事务并不能加S锁,也就无法进行读取,避免了读取脏数据);
  • 三级封锁协议:在二级的基础上,事务在读取数据之前必须先加S锁,直到事务结束才能释放。可以解决不可重复读问题(避免了在事务结束前其它事务对数据加X锁进行修改,保证了事务期间数据不会被其它事务更新)

什么是两段锁协议?

事务必须严格分为两个阶段对数据进行加锁和解锁的操作,第一阶段加锁,第二阶段解锁。也就是说一个事务中一旦释放了锁,就不能再申请新锁了。

可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。事务遵循两段锁协议是保证可串行化调度的充分条件。

什么是 MVCC?

多版本并发控制(Multi-Version Concurrency Control, MVCC),MVCC在每行记录后面都保存有两个隐藏的列,用来存储创建版本号删除版本号

  • 创建版本号:创建一个数据行时的事务版本号(事务版本号:事务开始时的系统版本号;系统版本号:每开始一个新的事务,系统版本号就会自动递增);
  • 删除版本号:删除操作时的事务版本号;
  • 各种操作:
    • 插入操作时,记录创建版本号;
    • 删除操作时,记录删除版本号;
    • 更新操作时,先记录删除版本号,再新增一行记录创建版本号;
    • 查询操作时,要符合以下条件才能被查询出来:删除版本号未定义或大于当前事务版本号(删除操作是在当前事务启动之后做的);创建版本号小于或等于当前事务版本号(创建操作是事务完成或者在事务启动之前完成)

通过版本号减少了锁的争用,提高了系统性能;可以实现提交读可重复读两种隔离级别,未提交读无需使用MVCC

快照读与当前读

使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销:

select * from table ...;

当前读读取的是最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁:

select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update;
delete;

数据库的范式?

  • 第一范式(1NF,Normal Form):属性不应该是可分的。举例:如果将“电话”作为一个属性(一列),是不符合1NF的,因为电话这个属性可以分解为家庭电话和移动电话…如果将“移动电话”作为一个属性,就符合1NF;
  • 第二范式 2NF:每个非主属性完全依赖于主属性集(候选键集);
    • B完全依赖于A,就是说A中的所有属性唯一决定B,属性少了就不能唯一决定,属性多了则有冗余(叫依赖不叫完全依赖)。举例:(学号,课程名)这个主属性集可以唯一决定成绩,但是对于学生姓名这个属性,(学号,课程名)这个属性集就是冗余的,所以学生姓名不完全依赖于(学号,课程名)这一属性集;
    • 主属性集/候选码集:某一组属性能够唯一确定其它的属性(主键就是从候选键集中选的一个键),而其子集不能,这样的属性组中的属性就是主属性;不在候选码集中的属性成为非主属性;
    • 可以通过分解来满足 2NF:将(学号,课程名,成绩)做成一张表;(学号,学生姓名)做成另一张表,避免大量的数据冗余; 满足1NF后,要求表中的所有列,都必须依赖于主键,而不能有任何一列与主键没有关系,也就是说一个表只描述一件事情;
  • 第三范式 3NF:在 2NF 的基础上,非主属性不传递依赖于主属性
    • 传递依赖:如果C依赖于B,B依赖于A,那么C传递依赖于A;
    • 3NF在2NF的基础上,消除了非主属性之间的依赖;比如一个表中,主属性有(学号),非主属性有(姓名,院系,院长名),可以看到院长名这个非主属性依赖于院系,传递依赖于学号。消除的办法是分解。 必须先满足第二范式(2NF),要求:表中的每一列只与主键直接相关而不是间接相关,(表中的每一列只能依赖于主键);

不符合范式会出现什么异常?

  • 冗余数据:某些同样的数据多次出现(如学生姓名);
  • 修改异常:修改了一个记录中的信息,另一个记录中相同的信息却没有修改;
  • 删除异常:删除一个信息,那么也会丢失其它信息(删除一个课程,丢失了一个学生的信息);
  • 插入异常:无法插入(插入一个还没有课程信息的学生)

存储过程

存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合。想要实现相应的功能时,只需要调用这个存储过程就行了(类似于函数,输入具有输出参数)。

优点:

  • 预先编译,而不需要每次运行时编译,提高了数据库执行效率
  • 封装了一系列操作,对于一些数据交互比较多的操作,相比于单独执行SQL语句,可以减少网络通信量
  • 具有可复用性,减少了数据库开发的工作量;
  • 安全性高,可以让没有权限的用户通过存储过程间接操作数据库;
  • 易于维护

缺点:

  • 可移植性差,存储过程将应用程序绑定到了数据库上;
  • 开发调试复杂:没有好的IDE;
  • 修改复杂,需要重新编译,有时还需要更新程序中的代码以更新调用

Drop/Delete/Truncate的区别?

  • Delete用来删除表的全部或者部分数据,执行delete之后,用户需要提交之后才会执行,会触发表上的DELETE触发器(包含一个OLD的虚拟表,可以只读访问被删除的数据),DELETE之后表结构还在,删除很慢,一行一行地删,因为会记录日志,可以利用日志还原数据;
  • Truncate删除表中的所有数据,这个操作不能回滚,也不会触发这个表上的触发器。操作比DELETE快很多(直接把表drop掉,再创建一个新表,删除的数据不能找回)。如果表中有自增(AUTO_INCREMENT)列,则重置为1;
  • Drop命令从数据库中删除表,所有的数据行,索引和约束都会被删除;不能回滚,不会触发触发器;

什么是触发器

触发器(TRIGGER)是由事件(比如INSERT/UPDATE/DELETE)来触发运行的操作(不能被直接调用,不能接收参数)。在数据库里以独立的对象存储,用于保证数据完整性(比如可以检验或转换数据)。

有哪些约束类型?

约束(Constraint)类型:主键(Primary Key)约束,唯一约束(Unique),检查约束,非空约束,外键(Foreign Key)约束。

什么是视图?什么是游标?

  • 视图:从数据库的基本表中通过查询选取出来的数据组成的虚拟表(数据库中存放视图的定义)。可以对其进行增/删/改/查等操作。视图是对若干张基本表的引用,一张虚表,查询语句执行的结果,不存储具体的数据(基本表数据发生了改变,视图也会跟着改变);可以跟基本表一样,进行增删改查操作(ps:增删改操作有条件限制);如连表查询产生的视图无法进行,对视图的增删改会影响原表的数据。好处:
    • 通过只给用户访问视图的权限,保证数据的安全性
    • 简化复杂的SQL操作,隐藏数据的复杂性(比如复杂的连接);
  • 游标(Cursor):用于定位在查询返回的结果集的特定行,以对特定行进行操作。使用游标可以方便地对结果集进行移动遍历,根据需要滚动或对浏览/修改任意行中的数据。主要用于交互式应用。

文章作者: 小游
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小游 !
  目录