Mysql 学习笔记:InnoDB 事务和 ACID 模型

用户头像
马迪奥
关注
发布于: 2020 年 09 月 13 日
Mysql学习笔记:InnoDB事务和ACID模型

InnoDB架构图镇楼



1. ACID模型



事务是一种操作数据的方式,一个事务可以是一条SQL语句,一组SQL语句或整个程序,满足以下特征:



  • Atomic(原子性):事务中包含的操作被看做一个逻辑单元,要么都成功,要么都失败

  • Consistency(一致性):一致性指事务将数据库从一致状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。

  • Isolation(隔离性):隔离不同事务,避免互相干扰,保障所见即所得

  • Durability(持久性):事务一旦提交就是永久性的。发生宕机等故障,数据库也能恢复



2. InnoDB的实现

InnoDB和ACID模型:https://dev.mysql.com/doc/refman/5.6/en/mysql-acid.html

2.1 redo & undo



redo log称为重做日志,是物理日志,记录的是磁盘页的修改操作。redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。undo log是逻辑日志,记录的是数据行记录。undo log也包含两部分:undo log buffer、undo log。

2.2 持久性



事务一旦提交操作成功,该事务所做的更改就不会受到电源故障、系统崩溃等问题影响。持久性通常涉及到对磁盘存储的写入,并具有一定数量的冗余,以防止写入操作期间出现电源故障或软件崩溃。



InnoDB通过Force Log at Commit机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit操作完成才算完成。在InnoDB存储引擎中,由两部分组 成,即redo log和undo log。redo log用来保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能。redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。



刷数到磁盘是在commit时发生的,有3中不同到策略,通过innodb_flush_log_at_trx_commit参数控制:



image.png



master thread会每秒把redo log buffer和undo log buffer刷新到磁盘中,即使没有commit,这就是为什么即使是长事务,commit操作也很快的原因。所以设置为0时,实际是没有额外操作。2比1要安全,因为1是把数据写入用户空间,mysql服务挂了数据就丢了,2会把数据写入系统空间,只会在服务器宕机是发生数据丢失。



虽然用户可以通过设置参数innodb_flush_log_at_trx_commit为0或2来提 高事务提交的性能,但是需要牢记的是,这种设置方法丧失了事务的ACID特性

2.3 隔离性



隔离性要解决的几个问题:



1.脏读

事务A对缓冲池中的数据做了修改并且还没有被提交(commit),这时被另外一个事务B读取到了数据,因为查询是优先走缓存的。



2.不可重复读



事务A中对同一行数据多次读取,如果在这期间事务B对数据进行了修改,那么事务A会读取到提交过的数据,造成了不一致。



3.幻读



可重复读要求对相同数据多次查询结果要一致,显然幻读并不属于不可重复读,对幻读的解决是在serializable级别中,但是InnoDB在RR级别就解决了幻读问题,希望不要把这两个概念搞混。

  • 事务A查询orderid=1 and status=1的记录,发现记录不存在操作insert

  • 事务B插入orderid=1 and status=1的记录,commit

  • 事务A commit

  • 数据重复插入



4.丢失更新



丢失更新体现逻辑上,事务A的更新操作会被事务B覆盖,如:

  1. 事务A把状态改为2,未commit

  2. 事务B把状态改为3,commit

  3. 事务A commit,应用程序继续执行后续操作



这时就发生了逻辑错误,即:当前状态没有改为2,不符合逻辑预期。



ANSI/ISO SQL标准定义了4中事务隔离级别



下面解读一下RC和RR级别

2.4.1 RC 解决脏读



RC级别主要为了解决脏读,即:不能读取到未提交的数据。通过MVCC来实现,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。



脏读的原因是事务提交之前对数据的变更会更新缓冲池中的data page,select会直接查询index page和data page,所以能查询到。如何隔离呢?1.锁定读,在修改时进行读取会被阻塞 2.非锁定读。为了提升并发性能在RC和RR级别中使用的是非锁定读,通过读取undo log多个版本的快照数据实现隔离。在RC中总是读取被锁定行的最新一份快照数据(快照读),因为总能读取到最新数据所以不可重复读。



2.4.2 RR 解决不可重复读和幻读



RR级别主要解决2个问题:1.不可重复读 2.幻读



不可重复读



可重复读核心要实现的是在当前事务执行过程中对相同数据的多次查询结果要一致,其它事务可继续修改数据。通过读取事务开始时的行数据版本就能实现了(当前读),上面提到过版本是通过undo log来实现的。



幻读

在RR中通过加锁来解决幻读问题:Next-Key Lock,包含Gap Lock + Record Lock。如果索引中含有唯一索引,则降级为Record Lock,这样只会锁定索引本身不会出现范围锁。



image.png



如果插入order_id=2的也会被锁定,原因是因为我在创建表时没有对order_id加索引,看下加了之后的效果:alter table `record` add index order_id_status (`order_id`,`status`) ;



image.png



如果不创建组合索引会分别对order_id和status做范围锁,这样基本和锁表没什么区别了,使用起来挺危险的。所以如果要用锁一定要确认锁的范围,最好使用主键或组合索引来缩小锁定范围。

2.4 原子性



事务的原子性保障一组操作要么成功要么失败,通过redo log和undo log实现。为什么要这么做的?因为数据库事务的原子性比操作系统的原子性情况要复杂,存在失败的可能,比如插入重复的主键就会报错,事务不能继续执行,需要回滚保证原子性。



以转账为例:A转账100元给B,需要保证A和B的账户余额一起完成减少和增加。

2.5 一致性



提到一致性估计立马想起来的就是分布式系统中不同数据副本之间的数据一致性问题了,而ACID中的一致性是指必须使数据库从一个一致性状态变到另一个一致性状态,如何理解?

  • 满足约束:类型一致、not null、唯一值

  • 运算结果一致:a = 100;begin ... set a = a-100; ... set a = a-100; commit; a应该等于-100。



在Mysql中除了上述数据完整性约束和运算一致性之外,还存在数据一致性问题,前面不是刚提到ACID一致性不是指数据一致性吗?这是因为在Mysql中存在多个数据副本,如:Mysql InnoDB DML操作会先写入buffer,这样数据就存在缓存和磁盘两个地方,除了定时把缓存数据写入磁盘,还使用doublewrite buffer和崩溃恢复机制减少数据丢失导致的数据不一致问题。



image.png



如果开启了bin log还使用了内部XA事务解决bin log和redo log之间的数据一致性,这些都是Mysql中的一致性相关问题。同时还提供了锁机制解决在并发场景下的丢失更新导致数据和预期不一致问题。

3.锁

单靠事务并不能解决对共享资源的并发操作带来的互相干扰,这需要通过锁来解决。InnoDB实现了两种标准的行级锁:

  • 共享锁(S Lock),允许事务读一行数据。

  • 排他锁(X Lock),允许事务删除或更新一行数据。



3.1 锁的类型

3.1.1 一致性非锁定读

由于S锁和X锁是不兼容的,如果读取的行当前加了X锁,那么读取不会等待锁释放,会去读取行快照。在InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据的操作叫做“非锁定读”。非锁定读通过undo log来实现,若读取的记录被其它事务占用,当前事务可以通过undo读取之前的行版本信息(需要进行还原得到之前的数据,和rollback一样)。

3.1.2 一致性锁定读

在RR隔离级别下select操作使用非锁定读,但是某些情况下用户需要显示的对读取加锁来保证数据的一致性,如:并发场景下,请求1把订单从2改为3,请求2把订单状态从2改为4,这样就会出现状态被修改了2次会造成bug。这时就需要对读取进行锁定,锁定读是对select语句加锁:

  • select for update 对行加X锁

  • select lock in share mode 对行加S锁



符合我们预期的应该只有for update

3.2 锁的算法



  • Record Lock:单个行记录上的锁。通过锁主键索引实现行锁。

  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。通过锁非聚集索引实现范围锁。

  • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。

4. 内部XA事务





1、2完成但3没完成,就会导致主从不一致。InnoDB通过先做PREPARE操作,接着再进行bin log和redo log的写入,如果宕机了,等恢复后检查uxid是否已经提交,没提交就重新提交,相当于做了最终一致。





5. 常见问题



Q1:耗时操作为什么不能在事务中进行?(事务长时间不提交会有什么问题?)

A1:事务使用锁来保证并发写操作之间的互斥,耗时操作相当于长事务,会把mysql线程阻塞,最终不可用




参考:



发布于: 2020 年 09 月 13 日 阅读数: 49
用户头像

马迪奥

关注

学如逆水行舟,但求得悟真理 2018.11.29 加入

坐标饿了么(花名:悟真),目前处于求知和探索的状态。

评论

发布
暂无评论
Mysql学习笔记:InnoDB事务和ACID模型