浅谈innodb的锁与事物

序言

    本篇博客,想简单地阐述对innodb的锁与事物的理解,不喜勿喷。曾经向某位同学解释,所有数据库

的通用原理时,他问我:“这他妈和file system”有何区别?下面列了一些,但不局限于此。

图1 innodb vs file system

    简单地说,仅仅依赖file system无法保证并发写入 & 系统宕机数据的完整性 & 正确性。为了更好的阐

述清事物问题,我想从事物本身遇到 & 解决的问题出发,然后在下面锁的章节来阐述innodb又是分别如何解决的。

1. innodb事物原理

    事务可以简单的理解成:由一系列SQL组成的逻辑处理单元,要么全’部’执行,要么全’不’执行。通常来说,事物具备4个属性(ACID):

    Atomic(原子性): 这是事物最基础也是最重要的特性。由一系列SQL组成的逻辑处理单元,要么全’部’执行,要么全’不’执行。最简单的例子,举个最简单的例子:去银行转账,分为读取账户余额,扣除自己余额,给对方账号余额加钱。如果满足不了原子性,可能会造成,你自己扣了钱,
同时,对方一分没有收到。

    Consistency(一致性): 一致性指:无论如何操作,数据库的状态要是满足约束的。简单地说,你的某一列是唯一列(不允许重名),一定不会因为频繁的更新、删除、加入,导致该列出现两个重复的值。

    Isolation(隔离性):数据库会提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。之所以有这个概念也是为了保证并发操作下,数据库数据的准确性。

    Durable(持久性):持久性指,一旦事物完成,其保存的数据即是持久的即使数据库出现故障,宕机,数据也是可以恢复的。但是这里需要强调一点,持久性并不意味着高可用,如果存储数据的磁盘坏掉了,存储引擎也无能为力。

1. 1 概述

    持久性主要由redo log(重做日志)保证。原子性、一致性由undo log保证。
    而隔离性主要通过锁机制以及mvcc(多版本并发控制)来保证。

1.2 原子性

    原子性,要求一个事物的所有sql操作,要么全部成功,要么全部失败,不会存在中间态。为了实现这样的效果最简单的实现方式:一定有一个地方存储了整个sql操作集的逆向操作记录。在事物失败时,可以通过这些记录,让数据库回到事物开始时的状态。

    Innodb引入的概念是undo log,用于帮助事物失败时的回滚,保证事物的原子性。不像下述提到redo log, undo log存放在数据库内部一个特殊的段中(undo segment)。undo用于将数据库逻辑的回到之前的状态,拿最简单的购物举例,当前有存活500件,你买一件失败了。由于并发事物存在,假设100个在购买,每个人购买一个都成功了。这个时候回滚之后库存是401, 而非499。而至于undo log具体实现细节,就像redo log一样,需要拉出单独的章节阐述。

1.3 一致性

1.4 隔离性

    事物的隔离性,主要约束并发事物情况下,数据更新的情况。分为四个级别:

(1) Read Commited(脏读):这一等级的隔离性为0,并发事物A、B相互间可以感知到对方尚未提交的数据。这一级别下,会存在脏读的问题。为了方便理解,画了一幅图。
在这里插入图片描述

图2 脏读示例

(2) Read Uncommited (不可重复读):这一等级的隔离性可以避免上述提到的脏读。即可以保证并发事物A、B之间的隔离性。但是依旧会存在一个问题,不可重复读。简单地说,你可以使用同一个sql在一个事物内,读取到两个不同的值。
在这里插入图片描述

图3 不可重复读

(3) Read Repeatable(幻读):在此隔离级别下,可以避免不可重复读。而会存在另一个问题,幻读。幻读乍一看有点像不可重复读,但是他们还是有区别。造成不可重复读的原因是,一个事物对某一列的修改与提交,被另一个事物感知到了,就会造成,我们两次读取,读到不同的值。而幻读指的非update,而是insert操作。事物a,在进行范围查询时,会因为事物B的insert的操作而在多次读取中读取到不同的值。
在这里插入图片描述

图4 幻读

(4) Serialable(顺序读):此隔离级别下,所有并发的事物将串行化,无论多复杂的操作都不会存在数据的问题,但是性能会大打折扣,因为很容易理解 & 不常用不过多介绍。

1.5 持久性

    通常一个事物最常规的写法如下,开启一个事物,组合几条sql, 关闭一个事物。为了保证持久性,就
要求在commit之后,数据不会丢,即使数据库宕机。
    我们最直观的想法是,一定有一个地方记录了我们sql1, sql2, sql3的执行动作,来保证即使数据库宕机了,我们依旧可以根据记录的操作来进行数据恢复。而胜任这一工作的就是redo log。 Innodb如何解决持久性问题? 在commit时,会将该事物内所有的操作,写入到redo log之中,才算本次操作完成。而当DB宕机时,就可以根据redo log来进
行数据修复。

图2 Redo Log

在这里插入图片描述
图3 Redo Log格式

    需要知晓的是,redo log存储的是物理日志,记录的是对物理页物理修改的操作,并且满足幂等性
(自行百度)。举个例子,update a where a.id = 1; 你可以理解成,在redo log中记录了,update操作对数据库的影响,比如
影响了索引页、数据页等,。最简单的,你可以理解成 redo log body中存的就是这条sql语句(作用是这样,当然实际比这个
复杂的多)。而在数据库崩溃之后,在恢复时,就可以通过这部分记录来还原出数据库本来的样子。至于更细节的部分,我
们单独拎出章节阐述。

Innodb锁原理

    按照数据存储的角度来看,mysql的锁可以分为表级锁、页级锁、行级锁。我们可以从开销与并发度来看待这几种锁的区别
表级锁:开销小,加锁快,锁粒度大,发生冲突概率最大, 因而并发度也最低。
行级锁:开销大,加锁慢,锁粒度小,发生冲突概率最低,因而并发度也最小。而页锁则介于两者之间。

     InnoDB无需过多的介绍页级锁 & 表锁,为了理解InnoDB的引擎的特性,需要把目光更多的聚焦于行级锁。InnoDB提供了两种标准的行级锁:X锁(互斥锁)、S锁(共享锁),并且需要记住的是,Innodb的行锁锁住的是索引,而非记录本身,这是非常关键的一点,所以在设计时,要特别注意索引与业务场景的结合。还记得前一章,我们所介绍到InnoDB事物级别吧,就需要与锁结合起来看,需要对行锁有更深入的认识。由此引入,行级锁的算法介绍:InnoDB存储引擎针对行级锁,有三种对应的算法:Record Lock;Gap Lock: Next Key Lock

     Record Lock : 单个行记录上上锁(锁住索引本身);
     Gap Lock ; 锁住的是一个范围; (解决幻读)
     Nexk Key Lock(Record Lock + Gap Lock)

     create table room(
          id,--主键
          room_name -- 房间名称 
     )
     ## 举个例子
     select  * from room where id = 5; (sql 1)
     select * from room where id > 5; (sql 2)

针对sql 1: 如果此时针对主键索引列id = 5, 没有加上X锁,我们会因为读对齐加上行级锁。
针对sql 2: 则是会对 (5,某个索引值)加上S锁,针对这个范围内的所有更新会被失效掉。

有了以上的认知,我们在把目光回归到事物的隔离级别上:

Read Commited: 是如何解决脏读的? 我们知道之前的脏读的根本原因:在并发事物中一个事物读取到另一个事物未提交的数据。因而解决的方法是,保证事物整体在全部逻辑执行完毕后,并且提交成功后,才将全部对数据的影响映射到innodb的数据存储上。

Read Repeatable:是如何解决不可重复读的?同时innodb又是如何进而在该级别下解决幻读的?刚才,我们解决了脏读的问题,那么不可重复读呢?不可重复读:我们在一个事物内,针对同一个sql,查到了两份不同的数据。其实解决不可重复读也很简单,加锁 & 只需要保证在每一个sql引发的锁,都在事物结束之后释放掉即可。还是拿并发的事物A、B举例,事物A正试图修改列1,而事物B因为业务属性在一个事物内会读多次列1。无论是A先执行,还是B先执行,只要保证锁在事物结束掉释放,都可以保证可重复读取。
幻读呢? 幻读:在一个事物内,针对同一个范围查询的sql,查到了两份不同数据。很简单,Next Lock Key正好锁住记录本身 & 锁住对应范围。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页