目录
多版本并发控制(MVCC——Multi-Version Concurrency Control)
事务
-
概念
事务指的是一组操作的集合,事务的产生,其实是为了当应用程序访问数据库的时候,简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题
当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么去处理它
-
特性
事务操作的正确性,通过以下几个特性来保障
特性 | 描述 |
原子性 | 事务不可再分,要么全提交,要么全回滚 |
隔离性 | 两个事务操作的结果在未提交前互相不可见 |
持久性 | 即使发生故障导致数据库崩溃,事务操作的结果应该依然存在 |
一致性 | 系统从一个正确的状态到另一个正确的状态,即正确的状态前后一致 |
-
举例
下面举个例子有助于理解:
在银行,A用户向B用户转账100元
首先,查询A用户余额是否够100元,如果够100元,A账户余额-100元,B账户余额+100元
原子性体现在,查询,A减少,B增加这三个步骤不能分开单独执行,数据库崩溃,则事务不会提交,通过这一组操作才能实现转账
隔离性体现在,转账过程中,有另外一个事务并行处理,两个事务互不干扰,在提交前双方的操作结果对对方都不可见
持久性体现在,如果转账结束,该事务提交完成,即使整个数据库系统崩溃,结果依然不会改变
一致性体现在,转账前后,A减少100元,B就应该增加100元,前后保持正确的一致状态
-
总结
可见,一个事务操作只有满足一致性,最终的结果才是正确的
在没有并发事务的情况下,事务串行执行,只要满足事务的原子性和持久性,一定满足一致性
在有并发事务的情况下,事务并行执行,除了原子性和持久性之外,还需要满足并发事务的隔离性,才能满足一致性
并发一致性问题
根据上述的情况,在事务并发的情况下,如果不满足隔离性,会产生并发一致性问题,我们对其进行分类
-
分类
问题 | 描述 | 状态 |
丢失修改 | 事务AB同一时刻修改数据,A先修改,B后修改,在A未提交前,B的修改将A的修改覆盖 | AB同时写,B覆盖A,A写入无效 |
脏读 | 事务A读取到了事务B修改未提交的数据,B将其撤销,A读到的结果与实际结果不符 | A读B写B撤销,A读的是脏数据 |
不可重复读 | 事务A多次读取数据,中间事务B、C、D进行写入后直接提交,A多次读结果不一致 | A多次读未提交,BCD写后直接提交 |
幻读 | 事务A在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的数据 | A对一个范围查询两次,第二次看到了第一次没有查询到的值 |
-
区分
要注意 脏读、不可重复读与幻读 的 区别
脏读是读到的数据与实际数据库中存储的数据不同,即读到的是未提交的数据,是错误的结果
不可重复读是多次读到的数据结果不同,但都是数据库中存储的真实值,都是已提交的数据,是正确结果
幻读是一种特殊的不可重复读,不可重复读强调的是修改,而幻读强调的是数据的增删
比如对同一行元素的查询,多次查询到的结果不同,是不可重复读的现象,强调数据被修改
对同一范围多个元素的查询,多次查询看到了不同(多或少)的结果数量,强调数据的增删
深入理解详见:
-
解决
既然是隔离性没有得到保障产生的并发一致性问题,那么就需要通过设置隔离级别来解决
隔离级别
级别 | 描述 | 存在问题 |
读未提交 | 可以读到其它事务未提交状态的数据 | 存在脏读、不可重复读、幻读问题 |
读已提交 | 只能读到已提交的事务的数据,读事务完成前,其它事务也可修改 | 解决了脏读问题,但还是有不可重复读、幻读问题 |
可重复读 | 只能读到已提交的事务的数据,读事务未完成前,其它事务不能修改 (也是Mysql数据库的默认隔离级别) | 解决了不可重复读问题,但还是有幻读问题 |
串行化 | 要求事务序列化执行,只能按序串行执行,不能并发执行 | 解决了幻读问题,但丧失了并发性 |
锁
以上这几种隔离级别听起来比较容易懂,但是它们又是怎么实现的呢?
在这里我们需要引入锁的机制,在操作系统线程并发的问题中,我们也对其有所了解
而在数据库中锁大致分为以下几种
类型 | 粒度 | 加锁开销 | 加锁速度 | 冲突概率 | 并发程度 |
行级锁 | 最小 | 最大 | 最慢 | 最小 | 最高 |
页面锁 | 适中 | 适中 | 适中 | 适中 | 适中 |
表级锁 | 最大 | 最小 | 最快 | 最大 | 最低 |
上面是按照锁的粒度进行分类,下面按照锁的行为进行分类
类型 | 再分类 | ||||
读写锁 (行级锁) |
| ||||
意向锁 (表级锁) |
|
锁机制对于隔离级别的实现如下描述
级别 | 描述 |
读未提交 | 读数据时不会检查是否有锁,或者使用锁,因此能读到其它事务未提交的状态,产生脏读问题 |
读已提交 | 只读取已经提交的数据,读锁在读操作完成后立即释放,因此读完后返回结果,读事务未提交,其它事务可以继续修改,产生不可重复读问题 |
可重复读 | 只读取已经提交的数据,读取数据的读锁在事务结束后释放,因此不会产生不可重复读状态,但读写锁是行级锁,产生幻读问题 |
串行化 | 对整张表加锁,要求事务只能按序串行执行,不能并发执行,解决了幻读问题,但丧失了并发性 |
幻读问题的解决
根据上述的介绍,我们可以发现,通过串行化的方式虽然解决了幻读问题,但同时也丧失了并发性,因此幻读的问题并没有真正地得到解决
在可重复读的隔离级别下,Mysql使用的InnoDB引擎通过MVCC + next-key lock的手段解决幻读,那么MVCC和next-key lock又是怎么回事呢?
多版本并发控制(MVCC——Multi-Version Concurrency Control)
要了解什么是MVCC,首先要弄清两个概念:当前读、快照读
描述 | |
当前读 | 读取的是当前最新版本,并且需要先获取对应记录的锁 |
快照读 |
不加锁的单纯的 select 操作,即不加锁的非阻塞读,如:select * from t_user where number = 1; 将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的 |
快照读的概念是工作在非串行级别下的,串行级别下的快照读会退化成当前读
InnoDB通过为每个数据行增加两个隐含值的方式来实现MVCC,记录了行的创建时间,以及它的过期时间(删除时间)
每一行都存储了事件发生时的系统版本号,用来替代事件发生时的实际时间,每新开始一个事务,版本号自动递增
-
InnoDB可重复读级别下MVCC的简化实现操作
select | 1.只能查找版本号小于等于当前事务的版本号的数据行——可以确保当前事务读取的行,都是在当前事务开始前存在的,或是由当前事务创建或修改的 2.数据行的删除版本号一定是未被定义或者大于当前版本号的——可以确保事务读取的行,在事务读取开始前未被删除 同时满足1.2的数据行才能被返回 总结:新的和未提交事务的版本号较高,我们读不到 |
delete | 为删除每一行保存当前版本号作为删除行标志 |
insert | 为新插入的每一行保存当前版本号作为行版本号 |
update | 插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前版本号到原来行,作为行删除标志 保持两个额外系统版本号,使得大多数读操作都可以不用加锁 总结:更新数据不是在原有基础上覆盖的,而是新插入一行,把原本的保存起来 |
具体操作详见:MySQL事务隔离之MVCC版本控制
通过以上这种方式,MVCC可以为数据库解决以下问题
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,不可重复读的问题