大师,我想悟透MySQL数据库的事务!

前言:本篇文章基于MySQL的InnoDB引擎。

什么是事务?

事务是一个逻辑概念,在这个逻辑概念中,由多个不同的动作组成,在执行多个动作时,人为的把这多个动作当做一个业务逻辑来处理,对于一个业务逻辑来说,结果只有成功或失败,当多个动作都成功时,这个业务逻辑才会被认为是成功,而事务的目的就是要保证这些动作要么都成功,要么都失败。

四个特性ACID

ACID特性:指的是原子性(atomicity),一致性(consistency),隔离性(isolation),持久性(durability)。

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态应满足完整性约束。
  • 隔离性(Isolation):事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
  • 持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。

如何保证原子性

MySQL事务的原子性是通过undo log来保证的。

那什么是undo log?

在数据进行修改(即DML和DDL操作)的时候,会记录undo log,即和binlog相反的日志记录,存储的主要是逻辑日志。

  • 举个例子,当新增一条数据时,会在binlog中记录一条insert日志,同时会在undo log记录的一条与binlog相反的delete日志。
    同样,在update一条记录时,binlog会记录update日志,undo log会记录一条与binlog相反的update记录。
undo log有什么作用?

undo log 主要用来做回滚和多版本并发控制(MVCC)。

  • 回滚:当执行 rollback 时,就可以从undo log中的读取到相应的内容并进行回滚。
  • 版本控制:用到行版本控制的时候,也是通过undo log来实现,当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
引出结论,如何保证原子性?
  • 当事务错误时,即事务中的一部分操作已经成功,但另一部分操作失败,这时会执行回滚,使用undo log产生的相反的操作,这样就能将已经执行成功的操作撤销,从而达到回滚的目的。因为支持回滚操作,所以我们就能保证“要么全部被执行,要么都不执行”。

  • 当事务成功时,当事务提交的时候,innodb不会立即删除undo log,因为后续还可能会用到undo log,如隔离级别为repeatable read时,事务读取的都是开启事务时的最新提交行版本,只要该事务不结束,该行版本就不能删除,从而保证其他事务发生异常时能正常回滚。

如何保证一致性

一致性这个概念比较抽象,我先来说下我对一致性的理解:一致性分为主观一致性和客观一致性,同时还需要满足约束条件。

什么是客观一致性?

客观一致性的意思就是程序在满足约束条件的情况下,通过AID(原子性、持久性和隔离性)来保证数据在事务执行前后的一致性。

  • 举个例子:
AB转账,AB钱的金额被约束不能小于0
假设转账之前,A500元,B500元,AB的总和是1000元
那么AB转账500之后,A0元,B1000元,AB的钱加起来还是1000

这个就是客观上的事务的一致性,即程序写的逻辑是正确的,从认知上来看也是正常的,同时满足约束条件,事务会执行成功,事务执行前A和B的总和是1000元,事务执行后A和B的总和还是1000元,所以该事务满足一致性。

  • 再举个例子:
AB转账,AB钱的金额被约束不能小于0
假设转账之前,A500元,B500元,AB的总和是1000A要向B转账1000元,如果转账成功后A会有-500元,B1500元,AB的钱加起来还是1000元
虽然AB的钱总和还是1000元,但是不满足约束,此时会回滚

该场景中,事务执行前A和B的总和是1000元,事务执行后A和B的总和还是1000元,虽然满足客观上的一致性,但是不满足约束条件,就把数据还原到了事务执行前的状态来保证一致性,所以也可以说约束条件为事务提供了一致性的保证。

  • 再举个例子:
我去银行存钱,账户存之前是0元
我要存1000元,账户存完是1000

从数据上看,账户金额是不一致的,从客观的角度来看,虽然你手里没有了1000元,但是你账户上也多了1000元,这是的的确确存在的,所以同样满足一致性的。

什么是主观一致性?

开发者故意写出违反约束的代码,导致事务能正常执行完成,从数据的角度来看,执行前后的状态是一致的,但是从认知的角度来看确实不一致的。
举个例子:

AB转账,AB钱的金额没有约束限制
假设转账之前,A500元,B500元,AB的总和是1000A要向B转账1000元,转账后A-500元,B1500元,AB的钱加起来还是1000

事务执行前A和B的总和是1000元,事务执行后A和B的总和还是1000元,从数据上看虽然是一致的,但是从主观认知上看,A账户变成负值了,这明显不合法,所以不满足主观上的一致性;所以开发者要满足一致性,需主观去控制正确的代码逻辑。

引出结论,如何保证一致性?

事实上,原子性、持久性和隔离性都是为了保证一致性,并且需要主观去控制正确的约束,来保证主观和客观上都一致。

如何保证隔离性

MySQL给出了四种隔离级别来保证隔离性。

四种隔离级别:Serializable、repeatable read、read committed、Read uncommitted
Serializable(串行化):事务之间以一种串行的方式执行,可避免脏读、不可重复读、幻读(虚读)情况的发生,安全性最高,效率也最低。
  • 脏读:指在一个事务内读取到了另外一个事务未提交的更新数据
  • 不可重复读:指在一个事务内读取到了另一个事务已提交的更新数据
  • 幻读:指在一个事务内读取到了另一个事务的已提交的新增数据
Repeatable Read(可重复读):是MySQL默认的隔离级别,同一个事务中相同的查询会看到同样的数据行,可避免脏读、不可重复读情况的发生,安全性较高,效率较高。
  • 按理说,Repeatable Read隔离级别应该都会出现幻读,但是在快照读(snapshot read)场景下并不会出现幻读,这是因为MySQL在该隔离级别下用了 MVCC(多版本并发控制) 解决了该问题;
    当然,还是有幻读的情况存在的,在当前读(current read)场景下,涉及到数据的修改时有可能出现幻读,因为数据修改时,MySQL必须拿到最新的数据才能修改,不过当前读可以通过 Next-Key Lock 来解决的,该锁保证了在执行当前事务时,其他事务不能对当前事务作用的区间进行新增。
  • 快照读场景:
  1. 什么是快照读?
    快照读只是简单的select操作(不包括 select … for update,select … lock in share mode)
    在同一个事务中,事务开始时,第一条select查询会将结果集生成一个快照(snapshot),然后,还是在这个事务中,第二次查询会直接查询第一次查询时生成的快照,这个查询过程就叫做快照读,这样就避免了幻读问题。
  2. 快照读怎么实现的?
    快照读是由MVCC实现的,MVCC是多版本并发控制,快照就是其中的一个版本。
  3. MVCC怎么实现的?
    而MVCC是由undo log实现的,因为undo log存储着每次修改的数据,并且会记录B_ROLL_PTR(回滚指针,上一个版本的数据在undo log中的位置),同时会记录DB_TRX_ID(事务ID),所以可以通过DB_ROLL_PTR可以找到各个历史版本,并且由DB_TRX_ID决定使用哪个版本的快照。
  • 举个例子
时间事务A事务B
time1set autocommit=0set autocommit=0
time2start transactionstart transaction
time3select count(*) from tab
time4insert into tab(id,age) values(10,18)
time5commit
time6select count(*) from tab

实测结果:time3节点和time6节点查询出的结果是一样的,没有出现幻读。

  • 当前读场景:
  1. 什么是当前读?
    当前读的意思是该操作场景下,只能读当前的值。在事务中进行 insert/delete/update/select … for update/select … lock in share mode 操作时,这个场景就为当前读。

  2. 修改为何会产生幻读?
    在一个事务中,在进行修改操作时会触发行级锁,行锁只能锁住行,也就是只能锁住已存在的数据,但是新插入的这个记录是不存在的,所以无法锁住,如果这时另一个事务做了一个插入操作,而本事务用到了另一个事务插入的数据(修改操作会拿出最新的值进行操作),就会产生幻读。

  3. 当前读场景如何解决幻读?
    Innodb在Repeatable Read隔离级别下,通过next-key lock机制避免了幻读现象,next-key lock,实现相当于record lock(行级锁) + gap lock(间隙锁),该锁不仅会锁住记录本身,还会锁定一个范围,gap lock(间隙锁)是指对于键值在条件范围内但并不存在的记录加锁,这样其他事务就无法对间隙锁内的数据进行修改,从而解决幻读。
    间隙锁默认是关闭的,可通过innodb_locks_unsafe_for_binlog = 1是开启。

  • 举个会产生幻读的例子
时间事务A事务B
time1set autocommit=0set autocommit=0
time2start transactionstart transaction
time3select * from tab
time4insert into tab(id,age) values(11,18)
time5commit
time6update tab set age = 28 where id =11
time7select * from tab

实测结果:time3 查询时为普通查询,不会加间隙锁,time3节点和time7节点查询出的结果是不一样的,time7查出了id为11、age为28的数据,出现了幻读。

  • 举个不会产生幻读的例子
时间事务A事务B
time1set autocommit=0set autocommit=0
time2start transactionstart transaction
time3select * from tab where id > 10 lock in share mode
time4insert into tab(id,age) values(11,18) (此时会阻塞,等待A事务结束,或等待超时)
time5update tab set age = 28 where id =11
time6select * from tab where id > 10

实测结果:time3 查询时加了lock in share mode(也可以用for update),这时会对id>10的这一范围内存在的间隙加锁,所以time3节点和time6节点查询出的结果是一样的,没有出现幻读,而此时的事务B会被阻塞,等待A事务结束或超时

Read Commited(读已提交):一个事务可以读到另一个事务已经提交的数据,安全性较低,效率较高
Read Uncommited(读未提交):一个事务可以读到另一个事务未提交的数据,安全性低,效率高

如何保证持久性

持久性是由redo log保证的。

什么是redo log?

当数据库进行增删改时,会记录的是物理修改的内容(xxxx页修改了xxx)到redo log中。

redo log有什么作用?

redo log在事务开始的时候,就开始记录每次修改的信息,InnoDB引擎会先将记录写到redo log中,再写到内存中,最后再写到磁盘,这份redo log记载着这次在某个页上做了什么修改;

引出结论,那redo log如何保证持久性的?

MySQL引入了redo log,当数据库进行增删改时记录redo log日志,所以,当我们修改的时候,写完内存了,但数据还没真正写到磁盘,此时我们的数据库挂了,我们可以根据redo log来对数据进行恢复,将redo log加载到内存里边,那内存就能恢复到挂掉之前的数据了,这样就保证了持久性。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值