数据库的事务与锁

学习数据库的事务与锁之前需要先了解下什么是并发:

  • 多个用户对同一数据进行交互叫做并发。如果不加以控制,并发可能引起很多问题。数据库提供了可以合理解决并发问题的方案。

一、事务

  • 事务的概念:类比于银行转账,A转给B100元,那么A的账户减少500,B的账户增加500元,这个两个操作必须全部执行,要么就全部不执行。数据库的的事务总是将指定的一条语句或多条语句看成一个全部执行或者全部不执行的最小组合全部组合。
  • 事务类型:如SELECT INSERT UPDATE DELETE

1- 显示事务处理模式

通过START TRANSACTION 标记事务起始点,如果开启事务后执行的语句是按照期望正确执行的则以COMMIT TRANSACTION 来结束事务;如果开始事务后执行的语句结果不是所期望的,并希望取消刚才的操作,则以ROLLBACK TRANSATION 来结束事务,(相当于撤销),该动作成为回滚。前滚指的是再次执行一次事务中的操作。

2- 自动提交事务模式

默认情况下,MySQL是自动提交的。在该模式下,每条语句都被认为是一个事务。当每个SQL语句执行完成后,不是被提交就是被回滚,如果执行成功则提交,执行失败则回滚。
注意:在该模式下,执行的语句如果出现编译错误(例如关键字错误)而非运行错误该批语句都不会执行,而不是执行后再回滚;如果是运行错误不会导致该批语句都不执行,而是错误语句会回滚,之前语句正常执行。

3-隐式事务处理模式

可以通过 SET IMPLICIT_TRANSACTIONS ON 和 SET IMPLICIT_TRANSACTIONS OFF 来启动和关闭隐式模式。与显示事务处理模式相比,它省略了事务起始点,也就是START TRANSATION。

PS:事务也是可以嵌套的,这里不进行详细说明。

二、并发访问引起的问题

并发:两个用户或者两个以上的用户在同一时间与同一对象进行交互。例如春节抢票,抢的人越多,并发数就越高,对系统性能要求就越高。

1- 丢失更新

当两个或多个事务对同一数据最初的值进行更新时,由于每个事务都不知道其他事务的存在,最后提交的事务中的更新操作就会覆盖掉其他事务所做的更新,这就导致其他事务的更新操作丢失。

2- 脏读

事务完成数据更新后,这时其他事务去查询该行数据的时候读取的数据是临时的,如果最后更新的事务被回滚,这个临时数据对于查询的事务来说就是“脏数据”。

3- 不可重复读

在一个事务两次的查询之间,同一数据被其他事务更新,导致同一事务中的两次查询结果不同,这样的现象叫做不可重复读。确定的某条记录。

4- 幻影读

一个事务中指定范围的两次查询结果因为其他事务更新了符合范围的数据,导致两次结果查询结果不同,这样的现象叫做幻影读。幻影读不仅仅只适用于符合条件的范围内的记录有多少,还适用于涉及到范围概念的情况。与范围数据有关。

三、锁

锁,即锁定,在数据库的概念为:在哪些数据或者对象上获取了锁就对对应的数据或者对象进行了锁定,其他事务就无法获取和现有锁相冲突的锁。锁是事务用来保护与自己交互的数据或者对象不受其他事务干扰的机制,实现了事务与事务之间的隔离。正是因为锁的存在,才可以根据业务需求合理地解决并发访问带来的问题。

1- 锁的粒度与锁升级

数据库可以在某一行获取锁,也可以对某一张表获取锁,也可以对整个数据库获取锁。这种多层次的锁结构成为锁的粒度。锁的粒度越粗,并发度越低,系统性能越高。
下面可以申请锁的粒度类型:

  • 行或行标识符(RID):属于行级锁(InnoDB),用于锁定堆中某个行的行标识符。
  • 键(Key):属于行级锁,在索引的键上存放锁,用户保护事务中的键的范围。
  • 页(Page):锁定该页中的所有数据或键。
  • 区(Excent):锁定整个区段,包括里面的页以及页中的数据行和键。
  • 表(Table):锁定整个表以及与表关联的所有对象,如表中的数据行、索引键。
  • 数据库(Database):锁定整个数据库。

由低层次的锁升级到高层次的锁成为锁升级。

2- 锁的类型

数据库引擎基于事务类型选择不同的锁,这些锁决定了并发事件访问资源的方式。此处只列出三种类型的锁。

(1)共享锁(S锁)
共享锁用于只需要读取不需要进行修改或更新数据的操作,如SELECT语句就是一种最基本常见的申请共享锁的语句。共享锁避免了不可重复读与幻影读问题。

(2)独占锁(X锁)
独占锁也成为排他锁。与其他所有的锁都冲突。当需要进行数据更改操作如INSERT、UPDATE、DELETE时,锁管理器就会分配X锁。一般情况下,数据修改时包含两个动作:读取需要的数据和修改数据,因此在数据修改时会申请共享锁和独占锁。在同一张表中修改数据,此时共享锁更应该成为更新锁,但是如果更新操作连接了其他表,那么其他表中就会存在共享锁,并在需要的数据上申请独占锁。

(3)更新锁(U锁)
更新锁和共享锁兼容,和独占锁冲突。更新锁和更新锁也冲突。修改数据时会先申请更新锁后申请独占锁, 更新锁是一种过渡锁。在进行数据搜索时持有了更新锁,由于更新锁和共享锁兼容,因此此时其他事务是允许读取数据的,当确定修改数据后,更新锁等待其他事务的共享锁释放后就会转换为独占锁(那么等待时间如何计算?),并将其他事务的相关资源的锁申请全部队列化堵在数据修改的进程外,直到独占锁释放,其他事务才能进行相关资源的申请。

  • 死锁:两个事务都在等待一个资源,但同时又相互阻止对方获取资源,这时就会发生死锁现象。例如,事务A和事务B都获取了某一行数据的共享锁(也就是可以查看该数据),当事务A想修改该数据时要将共享锁转化为独占锁,这就需要等待事务B释放共享锁,但是事务B也想修改数据,将共享锁转换为独占锁,它将等待事务A释放共享锁,这样两个事务之间形成了僵局。

更新锁和共享锁是兼容的,因此更新锁和共享锁可能在同一资源上相互共存,但是更新锁和更新锁是相互冲突的,所以只能有一个事务对数据有更新锁。在过度为独占锁前,只有更新锁的事务必须先等待其他事务释放所有的共享锁,这就避免了上述的死锁问题。

四、事务隔离级别

在数据库系统中可以通过设置事务隔离级别间接地控制锁,实现事务之间的隔离,从而解决并发问题。事务隔离级别是并发控制的整体解决方案,其实质是通过控制锁来控制事务之间如何进行隔离。

1- 提交读(READ COMMITTED)

查询申请的共享锁在语句执行完毕后就释放,不需要等待事务结束后释放;数据修改申请的独占锁一直持有,直到事务结束才释放。设置提交读,可以避免脏读问题,但是不能解决不可重复读和幻影读。

PS:设置事务隔离级别和查看。隔离级别的设置是对会话级别的,所以只对当前会话有效。

2- 可提交读(READ UNCOMMITTED)

未提交读是控制级别最低的级别,设置之后,该会话的所有读操作将不申请共享锁,因此读时将忽略所有的锁,但是更新时仍然会申请独占锁,这种情况下并发带来的问题都有可能发生。

3- 可重复读(REPEATABLE READ)

MySQL默认事务隔离级别,当设置为可重复读隔离级别时,除了独占锁会一直保持到事务结束,共享锁也一样到事务结束。可重复读隔离级别下,脏读、丢失的更新和不可重复读问题都能够避免,但是也因为共享锁一直持有,会导致其他事务不能对相关数据进行修改,降低了并发度和性能。可重复读隔离级别无法解决幻影读问题。

4- 串行化(SERIALIZABLE)

串行化隔离级别隔离层次最高,它能够避免丢失的更新、脏读、不可重复读和幻影读问题。设置为串行化隔离级别后,共享锁也将一直持有到事务结束。比可重复读更严格的是它的锁定是范围的,还包括潜在的数据修改。它保证了范围内两次查询结果不会出现增加记录或减少记录而出现幻象。

串行化隔离级别对锁控制的方式为:如果在查询指定条件的列上有索引,则在该列符合条件的范围记录上加上KEY粒度的锁,如果在查询条件的列上没有索引,则直接在表上加上共享锁。

五、隔离级别、锁和并发问题的关系