本文主要探讨MySQL的ACID是如何实现的,对于什么是事务,隔离个别以及锁相关的只是不做过多的
讨论。
MySQL作为当下最受欢迎的关系型数据库,可应用于JAVA,Python/ target=_blank class=infotextkey>Python,C++等诸多平台,了解其底层实现,对于每一个软件开发者是必不可少的过程。接下来就以Innodb为例,介绍一下MySQL的ACID在底层是如何实现的。
先简单说说隔离性中的四种隔离级别:Read UnCommited<Read Commited<Repetable Read<Serialiable(可见性,描述的是不同事务之间能否获取其他事务对数据库数据所做的修改)
隔离级别 |
说明 |
Read UnCommited |
读未提交,一个事务的修改,不管是否已将提交,对于其他事务都是可见的 |
Read Commited |
读已提交,一个事务的修改,在提交之前,对于其他事务都是不可见的 |
Repetable Read |
可重复读,一个事务中对相同查询条件数据的多次读取,结果都是一样的 |
Serializable |
串行化,同步执行,效率太低,意义不大,一般不推荐使用 |
接下来了解一下,不同隔离级别产生的不同问题:脏读,不可重复读,幻读
隔离级别 |
脏读 |
不可重复读 |
幻读 |
读未提交 |
√ |
√ |
√ |
读已提交 |
× |
√ |
√ |
可重复读 |
× |
× |
√ |
串行化 |
× |
× |
× |
那么多种隔离级别,隔离性是如何实现的,为什么不同事务之间能够做到互不干扰?接下来就来了解一下隔离性的实现方案:锁与MVCC
先来说说锁的种类,看看MySQL中有哪些锁
粒度
从粒度来区分锁有表锁,行锁,页锁。表锁又分为:共享锁,排他锁,自增锁等。行锁是在引擎层由各个引擎自己实现的,但是并不是所有的存储引擎都包含行锁,MyISAM就没有行锁。
行锁的种类
在Innodb中,行锁是通过给索引中的索引项加锁来实现的,也就是只有通过使用索引来检索数据才会使用到行锁,否则使用的还是表锁。行锁同样分为两种:共享锁和排他锁,以及获取锁之前必须获取当前表的意向共享锁和意向排他锁(先获取表的意向共享锁和意向排他锁或者更强的锁,然后才能给表中的每行记录添加共享锁和排他锁)。
1、共享锁:读锁,允许事务添加S锁,不允许事务添加X锁,即允许事务读数据,不允许事务写数据。
加锁方式:select locak in share mode
2、排他锁:写锁,不允许其他事务添加S锁或者X锁。加锁方式:insert,delete,update,select for update
行锁是在需要的时候才加锁的,但并不是在不需要了就立刻释放,而是要等到事务结束时才释放。这个就是锁的两阶段提交。
锁之隔离性
大致介绍了一下锁,我们可以知道,有了锁,当事务正在写数据时,其事务获取不到写锁就无法写数据,一定程度上保证了事务的隔离性。但是在数据库的使用中,我们发现在写数据的同时,我们还能读数据,这就说明了MySQL中并不是单单依靠锁来实现事务的隔离性。接下来我们就俩了解一种实现隔离性的一种比较折中的方式-----MVCC
前面说到,有了锁,事务就不能写数据,但是还可读数据,而且当其他事务已经对当前行进行了修改并提交,还是可以读到重复数据。这就是多版本的并发控制,MVCC(Multi-Version Cocurrenty Controller)
版本连
innodb中的行记录存储格式中还有一些额外的字段:RowId,Data_trx_id和Data_roll_ptr
Data_trx_id:最后一次修改该行记录的事务的事务ID
Data_roll_ptr:回滚指针。指向该行记录的前一个版本。在undo log中以链表的形式组织
行记录的信息图
ReadView(读视图)
在每次查询数据之前都会创建一个ReadView,具有以下一个属性:
trx_ids:当前系统中活跃的事务ID的集合
low_trx_id:最晚创建的事务,当前事务链中最大的事务编号ID,也就是最近创建的,除本身之外最大的事务编号ID
up_trx_id:最先开始的事务,当前事务连中最小的事务编号ID,也就是当前系统中创建最早但还未提交的事务
creator_trx_ic:当前创建ReadView的事务ID编号
up_trx_id <= low_trx_id <= creator_trx_id
ReadView数据模型
开始查询
一条SELECT语句过来,找到一行数据:
/*注意:最后修改包括:update | delete | insert*/
1、Data_tx_id < up_trx_id:说明最后修改该行记录的事务之前就存在,并已提交,对其他事务可见的
2、Data_trx_id > low_trx_id : 说明最后修改改行记录的事务还没开始,对于其他事务不可见
up_trx_id < Data_trx_id < low_trx_id:查看trx_ids是否存在Data_trx_id,如果存在,说明最后修改改行记录的
3、事务还没提交,其他事务不可见,如果不存在,说明该条事务已经提交,其他事务可见
版本链示意图
RR级别的幻读
RR级别如何在RC级别上解决幻读的:
事务A |
事务B |
|
事务A |
事务B |
开始事务 |
开始事务 |
|
开始事务 |
开始事务 |
快照读查询金额为500 |
快照读查询金额为500 |
|
快照读查询金额为500 |
|
更新金额为400 |
|
|
更新金额为400 |
|
提交事务 |
|
|
提交事务 |
|
|
select快照读金额为500 |
|
|
select快照读当前金额为400 |
|
select lock in share mode当前读金额为400 |
|
|
select lock in share mode当前读金额为400 |
1、左表与右标的唯一区别在于左表中事务B在事务A提交前做了一次快照读,而右在事务A修改金额提交事务之前没有进行一次快照读。所以我们知道事务快照读的结果非常依赖第一次事务快照读的结果,既事务首次出现快照读的结果非常关键,它决定该事务后续快照读的结果
2、当一个事务中同时出现快照读和当前读时才会出现幻读
3、MVCC并没有彻底解决幻读
在隔离性中MVCC依靠undo log中的版本链查找旧版本数据。在原子性的实现中同样依赖于undo log。当事务对数据进行修改时innodb生成相应的undo log;如果事务执行失败或者显示调用rollback,便可以利用undo log将数据恢复到旧版本时的数据。undo log属于逻辑日志,它记录了SQL执行相关的信息。
以update为例:当执行update时,undo log中会包含被修改行的主键(以便知道修改了哪些行),哪些列,以及相关的值,回滚时便可根据这些信息修改成update之前的状态。
MySQL中有很多的日志文件,持久性依赖于redo log。
一条SQL更新语句怎么运行
持久性跟写有关,MySQL中提供了WAL即使。WAL技术全称Write-ahead logging,他的关键点在刷新磁盘之前,必须要先写redo log.
Redo log
更新一行记录的时候,Innodb会先将这条记录写到redo log(并更新内存),这个时候更新就算完成了。在适当的时候将这个操作记录更新到磁盘中,这个往往是在比较空闲的时候进行,redo log有两个特点:
1、大小固定,循环写
2、crash-safe
Redo log有两个状态,称为两阶段提交,如果不适用两阶段提交,会导致在数据库状态和用他的日志恢复处理的状态不一致。
Buffer Pool
Innodb中还提供额缓存,Buffer pool保存了磁盘中部分数据页的映射,作为访问数据库的缓冲。
读取数据时,会向读取buffer pool中的数据,如果没有才会读取磁盘中的数据
向数据库写入数据时,会先将数据写入buffer pool,然后定期将buffer pool中写入的书记刷新到磁盘
Buffer pool大大提高了数据可的读写效率,但也带来了问题,当Buffer pool中的数据还没有刷新到磁盘时,数据库宕机,那么Buffer pool中的数据就丢失了,事务的持久性无法保证。所以还需借助redo log的辅助,更新数据时除了将数据更新到Buffer pool中,还会将此次更新操作计入到redo log当中。当提交事务时,会调用fsync将redo log刷新到磁盘中。
如果MySQL宕机时,重启服务器时会读取redo log中的数据,恢复数据库中的数据。redo log采用WAL技术,所有的修改都会先写入redo log中,再更新到buffer pool中,保证MySQL不会因为宕机导致MySQL丢失了持久性。而且这样做有两个优点:
1、刷脏也时随机IO,redo log是顺序IO
2、刷脏页页页为单位,一条数据的修改整页都要修改,redo log只包含真正需要修改的数据,减少了无效IO
bin log与redo log的区别
1、层次:redo log是Innodb存储引擎特有的,而binlog是server中的,叫做归档日志文件
2、内容:redo log是物理文件,记录某个数据页上做了什么修改;而bin log是物理文件,
是语句的原始逻辑,如:给ID=2的这行记录的c字段加2
3、写入:redo log是循环写,写入世纪比较多,bin log是追加,再事务提交的时候写入
一致性是事务追求的最终目标,有前面的原子性,持久性,隔离性保证。当然一致性除了数据库的保障还要有业务层的保障,比如购买商品,除了扣除用户余额之外,还要减少库存,这样才能保证数据一致性。
MySQL 都很熟, ACID 也知道是个啥,但 MySQL 的 ACID 怎么实现的?
有时候,就像你知道了有 undo log、redo log 但可能并不太清楚为什么有,当知道了设计的目的,了解起来就会更加清晰了。