事务和锁
# 概念
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。以BEGIN TRANSACTION开始,以ROLLBACK/COMMIT结束
# 事务四个特性(ACID)
- 原子性(Atomicity):逻辑上是不可分割的操作单元,事务的所有操作要么全部提交成功,要么全部失败回滚(用回滚日志实现,反向执行日志中的操作);
- 一致性(Consistency):事务的执行必须使数据库保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的;
- 隔离性(Isolation):一个事务所做的修改在最终提交以前,对其它事务是不可见的(并发执行的事务之间不能相互影响);
- 持久性(Durability):一旦事务提交成功,对数据的修改是永久性的
# 四个特性分别是通过什么实现的
原子性
MySQL数据库事务的原子性是通过undo log
实现的。
事务的所有修改操作(增、删、改)的相反操作都会写入undo log
,比如事务执行了一条insert语句,那么undo log
就会记录一条相应的delete语句。所以undo log
是一个逻辑文件,记录的是相应的SQL语句一旦由于故障,导致事务无法成功提交,系统则会执行undo log
中相应的撤销操作,达到事务回滚的目的。
持久性
MySQl数据库事务的持久性是通过redo log
实现的。
事务的所有修改操作(增、删、改),数据库都会生成一条redo
日志记录到redo log
.区别于undo log
记录SQL语句、redo log
记录的是事务对数据库的哪个数据页做了什么修改,属于物理日志。
redo日志应用场景:数据库系统直接崩溃,需要进行恢复,一般数据库都会使用按时间点备份的策略,首先将数据库恢复到最近备份的时间点状态,之后读取该时间点之后的redo log
记录,重新执行相应记录,达到最终恢复的目的。
隔离性的实现
已提交读(允许可重复读) 实现策略:数据的读取不加锁,数据的写入、修改、删除需要加行锁,可以克服脏读,但无法避免不可重复读
可重复读(允许幻读)
实现策略: MVCC(多个版本行控制)策略
一致性实现
一致性是通过上面三种来综合实现的
参考:
MySQL事务四大特性实现:面试官的终究拷问_zycxnanwang的博客-CSDN博客 (opens new window)
# 并发一致的问题
- 丢失修改:一个事务对数据进行了修改,在事务提交之前,另一个事务对同一个数据进行了修改,覆盖了之前的修改;
- 脏读(Dirty Read):一个事务读取了被另一个事务修改、但未提交(进行了回滚)的数据,造成两个事务得到的数据不一致;
- 不可重复读(Nonrepeatable Read):在同一个事务中,某查询操作在一个时间读取某一行数据和之后一个时间读取该行数据,发现数据已经发生修改(可能被更新或删除了);
- 幻读(Phantom Read):当同一查询多次执行时,由于其它事务在这个数据范围内执行了插入操作,会导致每次返回不同的结果集(和不可重复读的区别:针对的是一个数据整体/范围;并且需要是插入操作)
# 数据库的四种隔离级别
- 未提交读(Read Uncommited):在一个事务提交之前,它的执行结果对其它事务也是可见的。会导致脏读、不可重复读、幻读;
- 提交读(Read Commited):一个事务只能看见已经提交的事务所作的改变。可避免脏读问题;
- 可重复读(Repeatable Read):可以确保同一个事务在多次读取同样的数据时得到相同的结果。(MySQL的默认隔离级别)。可避免不可重复读,但是存在幻读现象;
- 如何实现?是通过MVCC的ReadView来实现,事务启动的时候不会拍快照,第一次读取时会拍快照(每个事务都有一个版本号,版本号是递增的),然后后面事务会读取版本号<=当前版本号的数据
- 参考:(MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制 - 知乎 (zhihu.com) (opens new window))
- 可串行化(Serializable):强制事务串行执行,使之不可能相互冲突,从而解决幻读问题。可能导致大量的超时现象和锁竞争,实际很少使用。
- 如何实现? 读的时候加上共享锁,写的时候加上排他锁就可以实现了
深入参考:Innodb中的事务隔离级别和锁的关系 - 美团技术团队 (meituan.com) (opens new window)
# 事务的几种类型
# 扁平事务(Flat Transactions)
是事务类型中最简单的一种,但在实际生产环境中,这可能是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMITWORK或ROLLBACK WORK结束,其间的操作是原子的,要么都执行,要么都回滚。
# 带有保存点的扁平事务(Flat Transactions with Savepoints)
除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点(Savepoint)用来通知系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。
保存点用SAVE WORK函数来建立,通知系统记录当前的处理状态。 当出现问题时,保存点能用作内部的重启动点,根据应用逻辑,决定是回到最近一个保存点还是其他更早的保存点。
# 链事务(Chained Transactions)
可视为保存点模式的一种变种。带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,因为其保存点是易失的(volatile),而非持久的(persistent)。
链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的一样。
链事务与带有保存点的扁平事务不同的是,带有保存点的扁平事务能回滚到任意正确的保存点。而链事务中的回滚仅限于当前事务,即只能恢复到最近一个的保存点。对于锁的处理,两者也不相同。链事务在执行COMMIT后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。
# 嵌套事务(Nested Transactions)
是一个层次结构框架。由一个顶层事务(top-level transaction)控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务(subtransaction),其控制每一个局部的变换。
# 分布式事务(Distributed Transactions)
通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
对于InnoDB存储引擎来说,其支持扁平事务、带有保存点的事务、链事务、分布式事务。对于嵌套事务,其并不原生支持。
# 脏读、幻度、不可重复读
# 脏读(读取未提交数据)
A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。
# 不可重复读(前后多次读取,数据内容不一致)
事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。
# 幻读(前后多次读取,数据总量不一致)
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。
# 不可重复读和幻读的区别?
(1) 不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2) 幻读是读取了其他事务新增的数据,针对insert和delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
这时候再理解事务隔离级别就简单多了呢。
# 如何避免脏读和幻读
使用好事务的四种隔离级别
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
# MVCC 多版本并发控制
全称Multi-Version Concurrency Control,即多版本并发控制
,主要是为了提高数据库的并发性能
。以下文章都是围绕InnoDB引擎来讲,因为myIsam不支持事务。
# 当前读,快照度
当前读 它读取的数据库记录,都是当前最新
的版本
,会对当前读取的数据进行加锁
,防止其他事务修改数据。是悲观锁
的一种操作。
快照读 快照读的实现是基于多版本
并发控制,即MVCC,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本
的数据。
# 实现原理
主要是版本链
,undo日志
,Read View
来实现的
版本链 数据库里面会有三个隐藏字段,分别是db_trx_id
(事务id)、db_roll_pointer
(回滚指针,也叫undo log索引)、db_row_id
(自增id)
undo日志 主要用于记录
数据被修改之前
的日志,在表信息修改之前先会把数据拷贝到undo log
里。当事务
进行回滚时
可以通过undo log 里的日志进行数据还原
。
Read View 读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照
。Read View主要是用来做可见性
判断的, 即当我们某个事务
执行快照读
的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务
能够看到哪个版本
的数据,既可能是当前最新
的数据,也有可能是该行记录的undo log里面的某个版本
的数据。(可重复读就是通过这个来实现的,这个主要是一个规则,会按照一定的规则来对里面的数据进行读取)
# 两段锁协议和乐观并发控制
参考:全网最全一篇数据库MVCC详解,不全你打我 (juejin.cn) (opens new window)
# MySQL的几种锁
按范围来说主要包括 表锁、行锁、页锁
细分的话有下面这几种锁
# 行锁
这个就是可以对某一行进行上锁,这个是通过索引来实现的。
# 间隙锁
间隙锁是针对某个区间开的锁。间隙锁一定是开区间,比如(3,5)
锁定一个范围,但不包括记录本身(它锁住了某个范围内的多个行,包括根本不存在的数据)。
间隙锁什么时候触发
间隙锁的触发条件必然是命中索引的,当我们查询数据用范围查询而不是相等条件查询时,查询条件命中索引,并且没有查询到符合条件的记录,此时就会将查询条件中的范围数据进行锁定(即使是范围库中不存在的数据也会被锁定)
深入理解数据库行锁与表锁 - 知乎 (zhihu.com) (opens new window)
# 邻键锁
临键锁是行锁+间隙锁,即临键锁是是一个左开右闭的区间,比如(3,5]
# 共享锁和排他锁
共享锁其实就是读锁,排他锁时候写锁
共享锁/排他锁都只是行锁,与间隙锁无关,这一点很重要,后面还会强调这一点。其中共享锁是一个事务并发读取某一行记录所需要持有的锁,比如select ... in share mode;排他锁是一个事务并发更新或删除某一行记录所需要持有的锁,比如select ... for update。
# 意向共享锁/意向排他锁
共享锁/意向排他锁属于表锁,且取得意向共享锁/意向排他锁是取得共享锁/排他锁的前置条件。
# 插入意向锁(IIX)
插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作。如果说间隙锁锁住的是一个区间,那么插入意向锁锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。与间隙锁的另一个非常重要的差别是:尽管插入意向锁也属于间隙锁,但两个事务却不能在同一时间内一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。这里我们再回顾一下共享锁和排他锁:共享锁用于读取操作,而排他锁是用于更新或删除操作。也就是说插入意向锁、共享锁和排他锁涵盖了常用的增删改查四个动作。
# 自增锁
自增锁是一种特殊的表级锁,主要用于事务中插入自增字段,也就是我们最常用的自增主键id。
MySQL常见的七种锁详细介绍_Saintyyu的博客-CSDN博客_mysql的锁 (opens new window)
# MySQL事务两段式提交
其实所谓的两阶段就是把一个事物分成两个阶段来提交。就像下图这样
两阶段提交的第一阶段 (prepare阶段):写rodo-log 并将其标记为prepare状态。
紧接着写binlog
两阶段提交的第二阶段(commit阶段):写bin-log 并将其标记为commit状态。
什么需要两段式提交呢?
因为MySQL有两个日志文件,我们需要两个日志都写入,我们需要保证两个日志的一致性。那么如果不使用两阶段提交的方式,直接写入redo log然后写入binlog有什么问题呢?
假设,写完redo log,系统挂了。那么重启后innoDB引擎会根据redo log日志来恢复数据库。这时候数据库里面的数据是正确的。但是binlog丢失了啊。如果你有从库,那么从库的数据就错误了。因为从库的数据是通过binlog同步的。
如果把这两个步骤反过来呢,先写入binlog 再写入redo log呢?那么就会redo log丢失,数据库实际上没有更新。但是从库通过binlog更新了。还是数据不一致。
所以需要 两阶段提交
来保证数据一致性。如果这时候写完redo log后挂掉了,因为redo log和binlog都没有数据,所以会回滚事务。
如果binlog和redo log都写入了,但是没有提交,那么重启后会提交事务。这样binlog和数据库就都有数据了。
参考: