MySQL事务管理(三)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: MySQL事务管理

六、事务的一致性

事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态,当数据库只包含事务成功提交的结果时,数据库就处于一致性状态


事务在执行过程中若发生错误,则需自动回滚到事务最开始的状态,就像这个事务从来没有执行过一样,即一致性需原子性来保证

事务处理结束后,对数据的修改必须是永久的,即便系统故障也不能丢失,即一致性需持久性来保证

多个事务同时访问同一份数据时,须保证多个事务在并发执行时,不会因为由于交叉执行而导致数据不一致,即一致性需隔离性来保证

此外,一致性与用户的业务逻辑强相关,若用户本身的业务逻辑有问题,最终也会让数据库处于一种不一致的状态

一致性实际是数据库最终要达到的效果,一致性不仅需要原子性、持久性和隔离性来保证,还需上层用户编写出正确的业务逻辑


七、多版本并发控制

数据库的并发场景


读-读并发:不存在任何问题,也不需并发控制

读-写并发:有线程安全问题,可能会存在事务隔离性问题,可能遇到脏读、幻读、不可重复读

写-写并发:有线程安全问题,可能会存在两类更新丢失问题

注意:


写-写并发场景下的第一类更新丢失被称为回滚丢失,即一个事务的回滚把另一个已经提交的事务更新的数据覆盖了,第二类更新丢失被称为覆盖丢失,即一个事务的提交把另一个已经提交的事务更新的数据覆盖了

读-读并发不需要进行并发控制,写-写并发实际就是对数据进行加锁,最值得讨论的是读-写并发,读-写并发是数据库中最高频的场景,在解决读-写并发时不仅需要考虑线程安全问题,还需考虑并发的性能问题

多版本并发控制


多版本并发控制(Multi-Version Concurrency Control,MVCC)是一种用来解决读写冲突的无锁并发控制,主要依赖记录中的3个隐藏字段、undo日志和Read View实现

为事务分配单向增长的事务ID,为每个修改保存一个版本,将版本与事务ID相关联,读操作只读该事务开始前的数据库快照

MVCC保证读写并发时,读操作不会阻塞写操作,写操作也不会阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读、幻读和不可重复读等事务隔离性问题

7.1 记录中的3个隐藏字段

数据库表中的每条记录都会有如下3个隐藏字段:


DB_TRX_ID:6字节,创建或最近一次修改该记录的事务ID

DB_ROW_ID:6字节,隐含的自增ID(隐藏主键)

DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本

注意:


采用InnoDB存储引擎建立的每张表都会有一个主键,若用户没有设置,InnoDB就会自动以DB_ROW_ID产生一个聚簇索引

此外,数据库表中的每条记录还有一个删除flag隐藏字段,用于表示该条记录是否被删除,便于进行数据回滚

示例


创建一个学生表,表中包含学生的姓名和年龄


5ded87ba98bd4aaab0954eeaaec465a6.png


当向表中插入一条记录后,该记录不仅包含name和age字段,还包含三个隐藏字段


952f9f74d79347b0a4265cac1ff48a4e.png


假设插入该记录的事务的事务ID为9,那么该记录的DB_TRX_ID字段就是9

因为这是插入的第一条记录,所以隐式主键DB_ROW_ID字段就是1

由于这条记录是新插入的,没有历史版本,所以回滚指针DB_ROLL_PTR的值设置为null

MVCC重点需要的就是这三个隐藏字段,实际还有其他隐藏字段,但没有画出

7.2 日志

MySQL三大日志如下:


redo log:重做日志,用于MySQL崩溃后进行数据恢复,保证数据的持久性

bin log:逻辑日志,用于主从数据备份时进行数据同步,保证数据的一致性

undo log:回滚日志,用于对已经执行的操作进行回滚,保证事务的原子性

MySQL会为上述三大日志开辟对应的缓冲区,用于存储日志相关的信息,必要时会将缓冲区中的数据刷新到磁盘。MVCC的实现主要依赖三大日志中的undo log,记录的历史版本就是存储在undo log对应的缓冲区中的


7.3 快照

现在有一个事务ID为10的事务,要将刚才插入学生表中的记录的学生姓名改为"李四":


因为是要进行写操作,所以需先给该记录加行锁

修改前,先将该行记录拷贝到undo log中,此时undo log中就有了一行副本数据

然后再将原始记录中的学生姓名改为"李四",并将该记录的DB_TRX_ID改为10,回滚指针DB_ROLL_PTR设置成undo log中副本数据的地址,从而指向该记录的上一个版本

最后当事务10提交后释放锁,这时最新的记录就是学生姓名为"李四"的那条记录

db205200757b4e78bd2a19637645df3a.png


现在又有一个事务ID为11的事务,要将刚才学生表中的那条记录的学生年龄改为38:


因为要进行写操作,所以需先给该记录(最新的记录)加行锁

修改前,先将该行记录拷贝到undo log中,此时undo log中就又有了一行副本数据

然后再将原始记录中的学生年龄改为38,并将该记录的DB_TRX_ID改为11,回滚指针DB_ROLL_PTR设置成刚才拷贝到undo log中的副本数据的地址,从而指向该记录的上一个版本

最后当事务11提交后释放锁,这时最新的记录就是学生年龄为38的那条记录

664290854dfa457b892155010416a99c.png


此时就有了一个基于链表记录的历史版本链,而undo log中的一个个的历史版本就称为一个个的快照


注意:


回滚实际就是用undo log中的历史数据覆盖当前数据,而所谓的创建保存点就可以理解成是给某些版本做了标记,可以直接用这些版本数据来覆盖当前数据

这种技术实际就是基于版本的写时拷贝,当需要进行写操作时先将最新版本拷贝一份到undo log中,然后再进行写操作,和父子进程为了保证独立性而进行的写时拷贝是类似的

insert和delete的记录如何维护版本链?


删除记录并不是真的把数据删除了,而是先将该记录拷贝一份放入undo log中,然后将该记录的删除flag隐藏字段设置为1。回滚后该记录的删除flag隐藏字段变回0了,相当于删除的数据又恢复了

新插入的记录是没有历史版本的,但是一般为了回滚操作,新插入的记录也需拷贝一份放入undo log中,只不过被拷贝到undo log中的记录的删除flag隐藏字段被设置为1,回滚后就相当于新插入的数据就被删除了

即增加、删除和修改数据都是可以形成版本链


当前读 VS 快照读


当前读:读取最新的记录

快照读:读取历史版本

事务在进行增删查改的时候,并不是都需要进行加锁保护:


事务对数据进行增删改的时候,操作的都是最新记录,即当前读,需进行加锁保护

事务在进行select查询的时候,既可能是当前读也可能是快照读,若是当前读,那需进行加锁保护,但若是快照读,那就不需要加锁,因为历史版本不会被修改,即可以并发执行,提高了效率,这也就是MVCC的意义所在

而select查询时应该进行当前读还是快照读,则由隔离级别决定,在读未提交和串行化隔离级别下,进行的都是当前读,而在读提交和可重复读隔离级别下,既可能进行当前读也可能进行快照读


undo log中的版本链何时才会被清除?


在undo log中形成的版本链不仅仅是为了进行回滚操作,其他事务在执行过程中也可能读取版本链中的某个版本,即快照读

只有当某条记录的最新版本已经修改并提交,并且此时没有其他事务与该记录的历史版本有关了,这时该记录在undo log中的版本链才可以被清除

对于新插入的记录来说,没有其他事务会访问它的历史版本,因此新插入的记录在提交后就可以将undo log中的版本链清除了

因此版本链在undo log中可能会存在很长时间,尤其是有其他事务和这个版本链相关联的时候,但这也没有坏处,这说明其是一个热数据

7.4 Read View

事务在进行快照读操作时会生成读视图Read View,在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃的事务ID

Read View在MySQL源码中就是一个类,本质是用来进行可见性判断的,当事务对某个记录执行快照读的时候,对该记录创建一个Read View,根据这个Read View来判断,当前事务能够看到该记录的哪个版本的数据

ReadView类的源码:

class ReadView {
  // 省略...
private:
  /** 高水位:大于等于这个ID的事务均不可见*/
  trx_id_t m_low_limit_id;
  /** 低水位:小于这个ID的事务均可见 */
  trx_id_t m_up_limit_id;
  /** 创建该 Read View 的事务ID*/
  trx_id_t m_creator_trx_id;
  /** 创建视图时的活跃事务id列表*/
  ids_t m_ids;
  /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
  * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
  trx_id_t m_low_limit_no;
  /** 标记视图是否被关闭*/
  bool m_closed;
  // 省略...
};



部分成员说明:


m_ids: 一张列表,记录Read View生成时刻,系统中活跃的事务ID

m_up_limit_id: 记录m_ids列表中事务ID最小的ID

m_low_limit_id: 记录Read View生成时刻,系统尚未分配的下一个事务ID

m_creator_trx_id: 记录创建该Read View的事务的事务ID

由于事务ID是单向增长的,因此根据Read View中的m_up_limit_id和m_low_limit_id,可以将事务ID分为三个部分:


事务ID小于m_up_limit_id的事务,一定是生成Read View时已经提交的事务,因为m_up_limit_id是生成Read View时刻系统中活跃事务ID中的最小ID

事务ID大于等于m_low_limit_id的事务,一定是生成Read View时还没有启动的事务,因为m_low_limit_id是生成Read View时刻,系统尚未分配的下一个事务ID

事务ID位于m_up_limit_id和m_low_limit_id之间的事务,在生成Read View时可能正处于活跃状态,也可能已经提交了,这时需通过判断事务ID是否存在于m_ids中来判断该事务是否已经提交

3f06cff596364b17b31e9b1e944a3df6.png


一个事务在进行读操作时,只应该看到自己或已经提交的事务所作的修改,因此可以根据Read View来判断当前事务能否看到另一个事务所作的修改

版本链中的每个版本的记录都有各自的DB_TRX_ID,即创建或最近一次修改该记录的事务ID,因此可以依次遍历版本链中的各个版本,通过Read View来判断当前事务能否看到这个版本,若不能则继续遍历下一个版本

bool changes_visible(trx_id_t id, const table_name_t& name) const 
  MY_ATTRIBUTE((warn_unused_result))
{
  ut_ad(id > 0);
  //1、事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见
  if (id < m_up_limit_id || id == m_creator_trx_id) {
    return(true);
  }
  check_trx_id_sanity(id, name);
  //2、事务id大于等于m_low_limit_id(生成Read View时还没有启动的事务),则不可见
  if (id >= m_low_limit_id) {
    return(false);
  }
  //3、事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见
  else if (m_ids.empty()) {
    return(true);
  }
  const ids_t::value_type* p = m_ids.data();
  //4、事务id位于m_up_limit_id和m_low_limit_id之间,如果在活跃事务id列表中则不可见,如果不在则可见
  return (!std::binary_search(p, p + m_ids.size(), id));
}



注意:使用该函数时将版本的DB_TRX_ID传给参数id,该函数的作用是根据Read View,判断当前事务能否看到这个版本


八、RR与RC的本质区别

现象演示


启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据


4d1fdecf7cfe4ce28477026b90a11bdc.png


在两个终端各自启动一个事务,在左终端中的事务操作之前,先让右终端中的事务查看一下表中的信息


8bdd7393653b40f1a2c0029490268558.png


左终端中的事务对表中的信息进行修改并提交,右终端中的事务看不到修改后的数据

25ed06d6a2404476b06c5f482acc7899.png



在右终端中使用select ... lock in share mode进行当前读,可以看到表中的数据确实是被修改了,只是右终端的事务看不到而已


8c4c09ca4bf34d008a12315c090a4a63.png


但若修改一下SQL的执行顺序,在两个终端各自启动一个事务后,直接让左终端中的事务对表中的信息进行修改并提交,然后再让右终端中的事务进行查看,此时右终端中的事务就可直接看到修改后的数据


6b0c250b21e54e8e9eda6d75ac733a26.png


在右终端中使用select ... lock in share mode进行当前读,可以看到刚才读取到的确实是最新的数据


433eaae3cc1044b197b014af1179244d.png


上面两次实验的唯一区别在于,右终端中的事务在左终端中的事务修改数据之前是否进行过快照读

由于RR级别下要求事务内每次读取到的结果必须是相同的,因此事务首次进行快照读的地方,决定了该事务后续快照读结果的能力

RR与RC的本质区别


正是因为Read View生成时机的不同,从而造成了RC和RR级别下快照读的结果的不同

在RR级别下,事务第一次进行快照读时会创建一个Read View,将当前系统中活跃的事务记录下来,此后再进行快照读时就会直接使用这个Read View进行可见性判断,因此当前事务看不到第一次快照读之后其他事务所作的修改

在RC级别下,事务每次进行快照读时都会创建一个Read View,然后根据这个Read View进行可见性判断,因此每次快照读时都能读取到被提交了的最新的数据

RR级别下快照读只会创建一次Read View,所以RR级别是可重复读的,而RC级别下每次快照读都会创建新的Read View,所以RC级别是不可重复读的


相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
7月前
|
监控 关系型数据库 MySQL
《MySQL 简易速速上手小册》第8章:事务管理和锁定策略(2024 最新版)
《MySQL 简易速速上手小册》第8章:事务管理和锁定策略(2024 最新版)
67 1
|
7月前
|
SQL 关系型数据库 MySQL
【MySQL】15. 事务管理(重点) -- 1
【MySQL】15. 事务管理(重点) -- 1
46 0
|
SQL 关系型数据库 MySQL
MySQL操作之事务管理
MySQL操作之事务管理
62 0
|
7月前
|
SQL 关系型数据库 MySQL
【MySQL】16.事务管理(重点) -- 2
【MySQL】16.事务管理(重点) -- 2
46 0
|
6月前
|
存储 关系型数据库 MySQL
深入浅出MySQL事务管理与锁机制
MySQL事务确保数据一致性,ACID特性包括原子性、一致性、隔离性和持久性。InnoDB引擎支持行锁、间隙锁和临键锁,提供四种隔离级别。通过示例展示了如何开启事务、设置隔离级别以及避免死锁。理解这些机制对优化并发性能和避免数据异常至关重要。【6月更文挑战第22天】
419 3
|
7月前
|
缓存 关系型数据库 MySQL
【专栏】提升MySQL性能和高可用性的策略,包括索引优化、查询优化和事务管理
【4月更文挑战第27天】本文探讨了提升MySQL性能和高可用性的策略,包括索引优化、查询优化和事务管理。通过合理使用B-Tree和哈希索引,避免过度索引,以及优化查询语句和利用查询缓存,可以改善性能。事务管理中,应减小事务大小并及时提交,以保持系统效率。主从或双主复制可增强高可用性。综合运用这些方法,并根据实际需求调整,是优化MySQL的关键。
235 2
|
7月前
|
关系型数据库 MySQL 测试技术
【MySQL】事务管理 -- 详解(下)
【MySQL】事务管理 -- 详解(下)
|
7月前
|
SQL 关系型数据库 MySQL
【MySQL】事务管理 -- 详解(上)
【MySQL】事务管理 -- 详解(上)
|
7月前
|
关系型数据库 MySQL 数据库
【MySQL】:数据库事务管理
【MySQL】:数据库事务管理
116 0
|
7月前
|
关系型数据库 MySQL 测试技术
【MySQL】16. 事务管理( 重点 | 选学 ) -- 3
【MySQL】16. 事务管理( 重点 | 选学 ) -- 3
53 0