msyql事务ACID特性及其实现原理

前序知识

重做日志(redo log)

重做的含义就是根据redo log重新做一遍数据更新。

当对数据库在内存种执行修改操作成功后,并不是直接将内存种的数据页刷到磁盘上,而是将修改信息(哪个数据页哪里发生了修改)写入redo buffer,redo buffer再根据三种策略中的一种顺序写入磁盘中的redo log中,随后由mysql服务选择合适时机根据redo log记录,将新数据刷入磁盘中。

在关系数据库中,这种先预写日志后面再将数据刷盘的机制,称为WAL(Write-ahead logging),翻译成中文就是预写式日志。

redo log保证了

  • 更新不丢失
    由于redo log位于磁盘中,因此保证了数据库宕机重启后,仍旧可以根据redo log将数据写入磁盘中。
  • 高性能
    由于顺序读写速度远高于随机读写速度,而对redo log的写操作采用顺序写的方式,因此,即使redo log位于磁盘中,其写的速度也非常快,进而保证了数据库更新操作的高性能。

回滚日志(undo log )

undo的中文意思是 不做了,其含义就是已经执行过的操作不做了,回滚到做之前的状态。

InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志。当事务错误或者rollback,mysql会利用undo log中备份的数据恢复到事务开始之前的状态。

同 redo log类似,添加undo log日志记录并不是直接写入undo log日志,而是先写入 undo buffer中,然后再写入undo log中。

mysql锁机制

事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

Mysql的锁机制主要解决写数据的问题,在一定程度上保证多个事务之间写操作的隔离。

MySQL中的锁主要有

  • 按照功能分:读锁和写锁;
  • 按照作用范围分:表级锁和行级锁;
  • 读锁:又称“共享锁”,是指多个事务可以共享一把锁,都只能访问数据,并不能修改。
  • 写锁:又称“排他锁”,是不能和其他事务共享数据的,如果一个事务获取到了一个数据的排他锁,那么其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。
  • 表级锁:是指会将整个表进行锁定,性能较差,不同存储引擎支持的锁的粒度不同,MyISAM引擎支持表级锁,InnoDB引擎支持表级锁也支持行级锁。
  • 行级锁:会将需要操作的相应行进行锁定,性能好。
  • 意向锁:意向锁是表级锁,如果在一个事务已经对一个表中的某个数据加上了排他锁或共享锁,那么就可以加上意向锁,这样当下一个事务来进行锁表的时候发现已经存在意向锁了,就会先被阻塞,如果不加意向锁的话,第二个事务来锁表的时候需要一行一行的遍历查看是否有数据已经被锁住了。
  • 间隙锁:间隙锁是为了防止产生幻读而加的锁,加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间(但是并不包含当前记录)。这样就保证了在间隙锁执行的时候,新增的数据会阻塞,保证了一个事务中的两次查询获得的记录数都是一致的。
  • Next-Key Lock:Next-Key Lock是行级锁和间隙锁的结合产生的锁,因为间隙锁是不会锁住当前记录的而Next-Key Lock是会将当前记录也锁住的。

mvcc

MVCC全称是【Multi-Version ConCurrency Control】即多版本控制协议。MVCC的主要是靠在每行记录上增加隐藏列和使用undo log来实现的,隐藏列主要包括,改行数据创建的版本号(递增的),删除时间,指向undo log的指针等。
在同一个时刻,不同的事物读取到的数据可能是不同的(多版本)。对于MVCC来说最大的优点就是读不加锁,因此读写不冲突,并发性能好。
MVCC与undo log配合,允许不同数据在同一时刻操作(读或写)同一个数据,在一定程度上保证多个事务之间读、写的隔离性。

MVCC主要是通过快照读和当前读两个操作。

  • 快照读(先读版本号-比较版本号-读数据)
    MVCC为了保证并发的效率,在进行读取数据的时候是不加锁的,在执行select的时候(不带锁的普通select),会先读取当前数据的版本号,如果在select还没返回结果时,有事务将此行数据进行了修改,那么版本号就会比执行select的时候的大,所以为了保证select读取数据的一致性,就只会读取小于或等于当前版本的数据,这个历史版本的数据就是从undo log中获取到的。
  • 当前读(读最新版本-加锁-修改数据)
    当执行insert、update、delete的时候,是读取的当前最新的版本数据,并且会给当前记录加上锁,用来保证在操作的时候不会被别的事务将版本号进行修改。

事务定义

事务是逻辑上的一组不可分割的数据库操作序列,要么都执行,要么都不执行。事务也是数据库并发控制的基本单位。事务的定义同时也是事务的存在理由,即将一系列的数据操作绑定成一个整体统一管理。其典型应用场景是银行不同账户之间的转账。

张三账户现有1000元,李四账户现有500元,此时是一种状态A。

某天,张三账户向李四账户转账300元

  • 若成功则张三账户减少300元,余额为700元;李四账户增加300元,余额为800元。此时是一种状态B。

  • 若失败,则回滚操作,使张三账户仍旧是1000元,李四账户仍旧是500元。此时是一种状态A。

以上转账过程可以看到,张三和李四的状态要么是A要么是B,不可能存在其他过渡状态,这就是事务所发挥的作用,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态

特别注意的是,MySQL的 InnoDB 引擎提供事务和行锁,下文讨论的事务内容基于InnoDB引擎。

事务具备以下四种特性

原子性(Atomicity)

操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。

实现原理

  • redo log保证正常执行的原子性(指令全部得到执行)
    mysql规定在执行这些需要保证原子性的操作时必须以组的形式来记录的redo日志,在进行系统崩溃重启恢复时,针对某个组中的redo日志,要么把全部的日志都恢复掉,要么一条也不恢复。
  • undo log保证异常执行的原子性(回滚到执行前的状态)
    一个数据修改操作(增删改)对应一个undo log日志,一个事务包含多个数据修改操作。当事务遇到异常或rollback时,从undo日志中找到该事务对应的记录,会根据undo log回滚到事务执行前的状态即可。

隔离性(Isolation)

当对数据库进行并发访问时,会产生两类五种并发问题。数据库引擎为了解决这些问题,保证在并发操作过程中数据一致性,就引入了隔离性的概念,即隔离性的本质作用是控制并发访问数据库。其思想同java的并发编程框架类似。

其定义为数据库允许多个并发事务同时对相同数据进行的读写的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致

两类五种事务并发问题

当事务并发操作时,若不采取任何措施,可能出现以下两类五种问题

两类指

  • 写-写

    一个事务中的写操作对另一个事务中的写操作的影响,包括1)第一类丢失更新 2)第二类丢失更新

  • 写-读
    一个事务中的写操作对另一个事务中的读操作的影响,包括1)脏读 2)不可重复读 3)幻读

tips:写操作和回滚操作会造成数据变化,读不会造成数据变化。

事务并发的问题是在于各个事务只关注自己的操作链,没有关注在自己事务的操作期间,其他事务对数据的修改操作,造成自己的数据已经无效。

第一类丢失更新(回滚出错,B写后提交-A读后回滚-数据库状态出错)

其实例入图,

  • 初始状态下,张三的余额为1000元
  • S1阶段: 开启A事务和B事务。此处B事务的开始时间没有要求,只要其结束时间位于A事务提交之前即可。
  • S2阶段:A事务和B事务都查询到张三的余额为1000元。
  • S4阶段:B事务提交修改操作。对于B事务来说,此时张三的最新余额应该是1100元。
  • S10阶段:A事务异常触发回滚操作。对于A事务来说,它认为张三的余额应该恢复到事务执行前,即1000元。

在这里插入图片描述

站在上帝视角可以发现,A事务的回滚操作覆盖了B事务的更新操作,使B事务更新丢失。

第二类丢失更新(两次提交出错,A写后提交-B写后提交-数据库状态出错)

同第一类丢失更新类似,只是事务的回滚变成了修改后提交。

  • B事务在S4阶段完成事务修改提交,B事务认为此时张三的余额为1100元
  • A事务在S9-S10阶段完成数据修改提交,A事务认为此时张三的余额为800元。
    在这里插入图片描述

A事务的修改提交操作覆盖了B事务的修改提交,导致B事务的更新丢失。

脏读(A写-B读-A回滚-B数据出错)

脏读是指事务读取到其他事务未提交的数据。

未提交数据可认为是临时数据,临时数据有可能是最终数据(提交),也有可能不是最终数据(回滚)。当临时数据转化为最终数据时,没有问题。当临时数据不等于最终数据时,导致出现脏读问题。

脏读过程如图所示,

  • S2阶段:B事务更新id=1的记录,将name设置为李四,但并未提交;
  • S3阶段:A事务在读取id=1的记录,获得name值为李四;
  • S4阶段:
    A事务提交事务,使用 name = 李四执行后续操作;
    B事务发生异常,执行回滚操作,即将id=1的记录的name值重新设置为张三;

在S4阶段,由于B事务的回滚,导致A事务读取的name值是脏数据,即所谓脏读。

在这里插入图片描述

不可重复读(A读-B写-A再读出错)

不可重复读是指在同一次事务中前后查询不一致的问题。

不可重复读出错情况如下图

  • S2阶段: A事务读取id=1的记录,其name=张三;
  • S3阶段:B事务更新id=1的记录,设置其name为李四;
  • S4阶段:
    A事执行其他操作;
    B事务提交;
  • S5阶段:A事务再次读取id=1的记录,其name=李四,与S2阶段的查询结果不同,即所谓的不可重复读。

在以上过程中,A事务并未修改id=1的操作,但前后两次读取该记录的值不一致,即所谓的不可重复读。

在这里插入图片描述

幻读(A读-B写-A再读出错)

事务 A 根据条件查询得到了 N 条数据,但此时事务 B 删除或者增加了 M 条符合事务 A 查询条件的数据,且B事务已提交完成,真实的数据集已经发生了变化。但事务 A 再次进行查询的时候,却查询不出来这种变化,产生了幻读现象。

以新增数据为例(删除类似),幻读现象如下图所示。

  • S1阶段: A事务和B事务开启

  • S2阶段
    A事务查询stu表,查出一条数据;

    B事务执行插入一条数据;

  • S3阶段

    A事务执行其他操作;

    B事务提交,“李四”数据插入数据库中。

  • S4-S8阶段
    A事务执行其他操作;
    在A事务之外对数据库进行查询(同A事务在S2阶段的查询条件完全相同),可以查到两条数据;

  • S9阶段:A事务执行查询(同A事务在S2阶段的查询条件完全相同),只有一条数据,没有查id=2的数据;

在S9阶段出现幻读现象,即数据库已经添加了新数据,A事务中却查询不到新数据。对于事务A来说,它认为当前数据库的最大id为1,那么若在A事务中的S10阶段执行插入已一条记录,该记录的id=2。由于数据库id=2的记录真实存在,因此A事务必然无法成功完成插入操作。A事务就会感觉到幻觉,明明数据库中没有id=2的记录,但自己插入id=2的记录却不成功。

在这里插入图片描述

出现幻读的原因是数据库引擎为了保证在同一个事务中的可重复读(即mysql默认的隔离级别为REPEATABLE_READ (可重复读),该级别保证了在同一个事务中,同一个查询语句的查询结果一致。),在开启事务时刻,将该事务绑定数据库在此刻的状态,使该事务中的查询操作屏蔽了后续时刻数据库的变化。如上图中,A事务绑定了数据库在S1阶段的状态,虽然在S1阶段之后的S3阶段B事务的提交操作改变了数据库的状态,但A事务感知不到数据库的状态变化,进而造成了幻读。

​ 特别注意的是,不可重复读强调同一条记录的修改操作。在A事务中对同一条数据记录的修改操作,造成在B事务中该修改前后的相同查询结果不一致。幻读强调数据库中记录的增删操作,在A事务中对数据库记录进行增删操作,在B事务中该操作的后面无法感知到增删变化。

隔离级别

为了解决事务并发的问题,SQL定义组织提出了四种隔离级别。这四种隔离级别可以解决事务并发中的问题。

隔离级别脏读不可重复读幻读
未提交读(RU,read-uncommitted)存在存在存在
提交读(RC,read-committed)不存在存在存在
可重复读(RR,repeatable-read)不存在不存在存在
串行化(serializable)不存在不存在不存在

每一个数据库厂商都有自己实现这四种隔离级别的方式,mysql的innodb引擎使用锁机制和mvcc(参考资料1)实现了四种隔离级别。

持久性(Durability)

一个事务一旦提交,它对数据库中数据的改变是永久性的,不会因为宕机等故障导致数据丢失。

实现原理

为了提高数据库的读写效率,InnoDB使用Buffer Pool作为操作系统和磁盘之间的缓冲,BufferPool位于系统内存中,包含了磁盘中部分数据页的映射。当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。

由于BufferPool位于内存中,当Mysql宕机时,若BuferPool中的数据尚未写入磁盘,则导致数据丢失,无法保证事务的持久性,于是引入redo log解决该问题。

当事务提交时,mysql首先将操作记录在redo log中,然后再写入BufferPool。由于redo log位于磁盘中,不会因mysql宕机而消失,因此当mysql重启之后,可以从磁盘中的redo log读取记录,将数据写入磁盘,满足了持久性要求。

一致性(Consistency)

事务的执行使数据从一个状态转换为另一个状态,整个数据的完整性保持稳定。

实现原理

总的来说,一致性是目的,是一种结果。实现一致性需要在两方面做工作

  • 在数据库层面
    事务的AID特性保证了数据具备一致性。这句话的意思是只要实现数据库的AID特性,那么数据库中的数据一定是正确的。
    这个层面由数据库引擎保证,用户不用做任何操作。

  • 在应用层面
    基于数据库的AID特性,同时需要业务代码遵循正确的业务逻辑,实现业务数据的一致性。这就调调了业务代码逻辑必须正确。例如在张三给李四转账的例子中,代码将张三的钱减少了,但是代码没有给李四账户加钱,那肯定无法无法保证金额的一致性。

    这个层面需要用户在代码实现正确的一致性逻辑。

四大特性的关系

原子性,隔离性和持久性是数据库的属性,而一致性是应用程序的属性。只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的。

其他

解决并发问题的常用手段包括悲观锁和乐观锁。

悲观锁是一种解决并发问题的思想,它悲观的认为有其他的操作者,因此在自己开始阶段就锁定全部目标资源。在mysql中,悲观锁主要在select中使用(在其后加上 for update),其实现原理基于mysql的锁机制。悲观锁数

乐观锁也是一种解决并发问题的思想,它乐观的认为没有其他操作者,因此只在操作的最后阶段检查是否有其他操作者。在mysql中,乐观锁主要是通过添加版本字段+业务代码CAS控制实现对其的使用,乐观锁没有底层实现原理。

  • 并发不高 数据要求高,使用悲观锁方式
  • 并发不高使用乐观锁

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值