今天, 笔者简单谈谈自己对 ACID 特性实现原理的理解. 本文主要探讨 MySQL InnoDB 引擎下的 ACID 实现原理, 对什么是事务, 隔离级别简单回顾一下.
作为二本上岸大厂的后端应届生, 深知没人带一路摸索的艰辛, 想把自己的心路历程与经验心得收获分享给大家. 后期大厂面试系列持续更新中.....
1 前文
之前有同学在面阿里二面被问到: MySQL 是如何实现 ACID 的? 其实, 如果叫简单介绍什么是 ACID, 大家肯定都能回答, 但是, 想要答好底层如何实现 ACID 特性的, 还得考考功力啦!
今天, 笔者简单谈谈自己对 ACID 特性实现原理的理解. 本文主要探讨 MySQL InnoDB 引擎下的 ACID 实现原理, 对什么是事务, 隔离级别简单回顾一下.
2 事务与 ACID
何为事务呢? 书上给予的概念多而难于理解, 笔者对事务的理解: 一系列操作组成, 要么全部成功, 要么全部失败. 它具备 ACID 四大特性, 在并发下, 可能存在脏读, 幻读, 不可重复读的并发问题, 于是又引出了四大隔离级别.
01 事务 ACID 特性
MySQL 作为一个关系型数据库, 以最常见的 InnoDB 引擎来说, 是如何保证 ACID 的.
(Atomicity)原子性: 一些列操作要么全部成功, 要么全部失败
(Isolation)隔离性: 事务的结果只有提交了其他事务才可见
(Consistency)一致性: 数据库总时从一个一致状态变到另一个一致状态(事务修改前后的数据总体保证一致 转账)
(Durability)持久性: 事务提交后, 对数据修改永久的
02 原子性
在聊原子性之前, 我得先给大家普及一个东西 --undo log, 这是啥玩意儿呢? 如果想要详细了解或则想知道它具体内部咋实现的可以仔细去看书, 这里我就简单分享我的理解, 知道这些, 面试基本够用啦.
undo log, 它是一种回滚日志, 既可以用来实现隔离性 MVCC, 也可以保证原子性. MVCC 待会谈论. 实现原子性的关键, 是事务回滚时能够撤销所有已经成功执行的 sql 语句.
当事务对数据库进行修改时, InnoDB 会生成对应的 undo log,undo log 会保存事务开始前老版本的数据, 当事务发生异常, 便会 rollback 回滚到老版本状态. 当发生回滚时, InnoDB 会根据 undo log 的内容做相反逻辑操作.
insert 语句, 回滚时会执行 delete;
delete 语句, 回滚时会执行 insert;
update 语句, 回滚时便执行相反的 update, 把数据改回来.
总之, MySQL 的原子性便是由 undo log 来保证, undo log 的作用我做了一下归纳总结:
作用: undolog 记录事务开始前老版本数据, 用于实现回滚, 保证原子性, 实现 MVCC, 会将数据修改前的旧版本保存在 undolog, 然后行记录有个隐藏字段回滚指针指向老版本.
03 持久性
在聊持久性之前, 我们得先知道 redo log. 老规矩, 想深入学习理解看书噢, 这里只做笔者面试回答分享.
我们以一个生活小案例来理解一下下:
redo log, 是一种物理日志. 它类似于一个卸货的小推车, 我们卸货若是每下一件物品就拿着去入库, 那岂不是特浪费时间(效率低, 还要找到合适存库位置). 此时, 若有一个小推车, 我们将货物首先存放在小推车, 当推车满了再往库里存, 岂不大大增加了效率.
MySQL 中也用了类似思想, 我们再更新数据库时, 先将更新操作记录在 redo log 日志, 等 redo log 满了或则 MySQL 空闲了再刷盘.
其实就是 MySQL 里经常说到的 WAL 技术, WAL 的全称是 Write-Ahead Logging, 它的关键点就是先写日志, 再写磁盘, 也就是先装小推车, 等不忙的时候再装库.
总之, MySQL 的持久性便是由 redo log 来保证, redo log 的作用我做了一下归纳总结:
redo log
物理日志
作用: 会记录事务开启后对数据做的修改, crash-safe
特性: 空间一定, 写完后会循环写, 有两个指针 write pos 指向当前记录位置, checkpoint 指向将擦除的位置, redolog 相当于是个取货小车, 货物太多时来不及一件一件入库太慢了这样, 就先将货物放入小车, 等到货物不多或则小车满了或则店里空闲时再将小车货物送到库房. 用于 crash-safe, 数据库异常断电等情况可用 redo log 恢复.
以下只作了解:
写入流程: 先写 redo log buffer, 然后 wite 到文件系统的 page cache, 此时并没有持久化, 然后 fsync 持久化到磁盘
写入策略: 根据 innodb_flush_log_at_trx_commit 参数控制(我的记忆: innodb 以事务的什么提交方式刷新日志)
0-->事务提交时只把 redo log 留在 redo log buffer
1-->将 redo log 直接持久化到磁盘(所以有个双 "1" 配置, 后面会讲)
2-->只是把 redo log 写到 page cache
04 隔离性
说到隔离性, 我们都知道 MySQL 有四种隔离级别, 用来解决存在的并发问题. 脏读, 幻读, 不可重复读.
那么不同隔离级别, 隔离性是怎样实现的呢? 具体实现原理是怎样的呢? 接下来我们就谈谈, 看不懂没关系, 老规矩, 结尾会进行总结滴!
一句话: 锁 + MVCC.
锁
1, 表锁
lock table table_name read/write
myisam 执行 select 自动加读锁, 执行 update/delete/insert 自动加写锁
表加了读锁, 不会阻塞其他线程的读操作, 阻塞写操作
表加了写锁, 读写操作都阻塞
2, 行锁
锁的类型
间隙锁 - gap lock: 锁定区间范围, 防止幻读, 左开右开, 只在可重复读隔离级别下生效 -|- 为了阻止多个事务将记录插入到同一范围内, 而这会导致幻读问题的产生
记录锁 - record Lock: 锁定行记录, 索的索引, 索引失效, 为表锁
临键锁 - next-key Lock:record lock+gap lock 左开右闭(解决幻读)
锁的模式
select .... for update
持有写锁, 别的不可加读锁, 也不可加写锁
select .... lock in share mode
持有读锁, 别的可以再加读锁, 不可加写锁
共享锁 - 读锁 - S 锁
排他锁 - 写锁 - X 锁
意向锁: 读意向锁 + 写意向锁
自增锁
需要的时候加上, 并不是马上释放, 等事务提交才释放, 两阶段锁协议
3, 全局锁 -- 全库逻辑备份
4, 死锁
两个或多个事务在同一资源上相互占用, 并请求加锁时, 造成相互等待, 无限阻塞
innodb 回滚拥有最少排他行级锁的事务
设置锁等待超时时间
乐观锁与悲观锁
悲观锁用数据库自带锁机制 -- 写多
乐观锁用 version 版本机制或 CAS 算法 -- 读多写少, 很少发生冲突情况
MVCC
是什么: 多版本并发控制.
原理提炼总结: 使用版本链 + Read View
详解:
版本链: 同一行数据可能有多个版本
innodb 数据表每行数据记录会有几个隐藏字段, row_id, 事务 ID, 回滚指针.
1,Innodb 采用主键索引(聚簇索引), 会利用主键维护索引, 若表没有主键, 就用第一个非空唯一索引, 若没有唯一索引, 则用 row_id 这个隐藏字段作为主键索引.
2, 事务开启会向系统申请一个事务 ID, 严格递增, 会向行记录插入最近操作它的那个事务的 ID
3,undolog 会记录事务前老版本数据, 然后行记录中回滚指针会指向老版本位置, 如此形成一条版本链. 因此可以利用 undo log 实现回滚, 保证原子性, 同时用于实现 MVCC 版本链.
图 3 版本链形成
Read View 读已提交隔离级别下, 会在每次查询都生成一个 Read View, 可重读读只在事务开始时生成一个 Read View, 以后每次查询都用这个 Read View, 以此实现不同隔离界别.
Read View 里面包含些什么?(一致性视图)
一个数组 + up_limit_id(低水位)+low_limit_id(高水位)(这里的 up,low 没写错, 就是这么定义的)
1, 数组里包含事务启动时当前活跃事务 ID(未提交事务), 低水位就是活跃事务最小 ID, 高水位就是下一次将分配的事务 ID, 也就是目前最大事务 ID+1.
数据可见性规则是怎样实现的?
数据版本的可见性规则, 就是基于数据的 row trx_id 和这个一致性视图 (Read View) 的对比结果得到的.
视图数组把所有的 trx_id 分成了几种不同的情况
图 4 数据版本可见性规则
读取原理:
某事务 T 要访问数据 A, 先获取该数据 A 中的事务 id(获取最近操作它的事务的事务 ID), 对比该事务 T 启动时刻生成的 readview:
1, 如果在 readview 的左边(比 readview 都小), 表示这个事务可以访问这数据(在左边意味着该事务已经提交)
2, 如果在 readview 的右边(比 readview 都大), 表示这个版本是由将来启动的事务生成的, 是肯定不可见的;
3, 如果当前事务在未提交事务集合中:
a, 若 row trx_id 在数组中, 表示这个版本是由还没提交的事务生成的, 不可见;
b. 若 row trx_id 不在数组中, 表示这个版本是已经提交了的事务生成的, 可见.
不可以访问, 获取 roll_pointer, 通过版本链取上一版本.
根据数据历史版本事务 ID 再重新与视图数组对比.
这样执行下来, 虽然期间这一行数据被修改过, 但是事务 A 不论在什么时候查询, 看到这行数据的结果都是一致的, 所以我们称之为一致性读.
总之, MySQL 的隔离性便是由 MVCC + 锁来保证, 各个隔离级别实现原理我做了一下归纳总结:
隔离级别原理及解决问题分析:
读未提交: 原理: 直接读取数据, 不能解决任何并发问题
读已提交: 读操作不加锁, 写操作加排他锁, 解决了脏读. 原理: 利用 MVCC 实现, 每一句语句执行前都会生成 Read View(一致性视图)
可重复读: MVCC 实现, 只有事务开始时会创建 Read View, 之后事务里的其他查询都用这个 Read View. 解决了脏读, 不可重复读, 快照读 (普通查询, 读取历史数据) 使用 MVCC 解决了幻读, 当前读 (读取最新提交数据) 通过间隙锁解决幻读(lock in share mode,for update,update,detete,insert), 间隙锁在可重复读下才生效.(默认隔离级别)
可串行化: 原理: 使用锁, 读加共享锁, 写加排他锁, 串行执行
总结: 读已提交和可重复读实现原理就是 MVCC Read View 不同的生成时机. 可重复读只在事务开始时生成一个 Read View, 之后都用的这个; 读已提交每次执行前都会生成 Read View
05 一致性
一致性是事务追求的最终目标, 前问所诉的原子性, 持久性和隔离性, 其实都是为了保证数据库状态的一致性.
当然, 上文都是数据库层面的保障, 一致性的实现也需要应用层面进行保障. 也就是你的业务, 比如购买操作只扣除用户的余额, 不减库存, 肯定无法保证状态的一致.
你把周围的人看作魔鬼, 你就生活在地狱; 你把周围的人看作天使, 你就生活在天堂.
本文转载自微信公众号「小龙 coding」, 可以通过以下二维码关注. 转载本文请联系小龙 coding 公众号.
来源: http://database.51cto.com/art/202109/683670.htm