• MySQL事务管理


    目录

    认识事务

    事务的概念

    事务的四大特性

    支持事务的数据库引擎

    事务的提交方式

    事务的相关演示

    事务的隔离级别

    查看与设置隔离级别

    读未提交(Read Uncommitted)

    读提交(Read Committed)

    可重复读(Repeatable Read)

    串行化(Serializable)

    隔离级别总结

    一致性的理解

    多版本并发控制

    记录中的3个隐藏字段

    undo日志

    画图理解MVCC

    Read View

    RR与RC的本质区别


    认识事务


    事务的概念

    概念:

    事务是一组操作,这些操作要么全部执行成功,要么全部失败回滚。

    解释:

    1.操作的意思就是一条或多条SQL语句组成,这些语句在逻辑上存在相关性,共同完成一个任务。

    2.而操作要么全执行成功,要么全失败回滚的属性就是事务的原子性,除此之外事务还有持久性、隔离性、一致性。

    为什么出现事务?

    解决用户访问数据库的潜在问题,比如网络异常,服务器宕机,数据不一致,并发冲突,数据丢失。

    事务的四大特性

    原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中如果发生错误,则会自动回滚到事务开始前的状态,不会对数据库产生任何影响。

    持久性(Consistency):事务提交后,对数据库的修改就是永久的,即使系统崩溃也将保存在数据库中。

    隔离性(Isolation):数据库允许多个事务同时访问同一份数据,隔离性保证事务在并发执行和串行结果一致。

    一致性(Durability):在事务的开始和结束,数据库的完整性约束必须得到保持。

    支持事务的数据库引擎

    MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
    使用 show engines; 以行显示数据库引擎:
    使用 show engines \G(不用加;) 以列显示数据库引擎:

    说明:

    Engine:存储引擎的名称。

    Support:服务器对存储引擎的支持级别,YES表示支持,NO表示不支持,DEFAULT表示数据库默认使用的存储引擎。

    Comment:存储引擎的简要说明。

    Transaction:表示存储引擎是否支持事务,可以看到MySQL中只有InnoDB存储引擎支持事务。

    XA:表示存储引擎是否支持XA事务。

    Savepoints:表示存储引擎是否支持保存点。

    事务的提交方式

    事务常见的提交方式有两种,分别是自动提交和手动提交。 

    通过show variables like 命令查看全局变量 autocommit,可以查看事务的自动提交是否被打开:

    ON表示自动提交打开,OFF表示自动提交关闭。

    用 SET 来改变 MySQL 的自动提交模式。

    SET AUTOCOMMIT=0 禁止自动提交 :
    SET AUTOCOMMIT=1 开启自动提交 :

    事务的相关演示

    演示1:事务的开始和回滚

    执行下面SQL:

    1. //1.建表
    2. //2.查看提交方式
    3. //3.开始事务
    4. //4.设置保存点1
    5. //5.插入数据1
    6. //6.设置保存点2
    7. //7.插入数据2
    8. //8.查看数据
    9. //9.回滚到保存点2
    10. //10.查看数据,数据2消失了
    11. //11.回滚到最初
    12. //12.查找数据,数据1也消失了
    13. create table if not exists account(
    14. id int primary key,
    15. name varchar(50) not null default '',
    16. blance decimal(10,2) not null default 0.0
    17. )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
    18. show variables like 'autocommit';
    19. start transaction;
    20. savepoint save1;
    21. insert into account values (1, '张三', 100);
    22. savepoint save2;
    23. insert into account values (2, '李四', 10000);
    24. select * from account;
    25. rollback to save2;
    26. select * from account;
    27. rollback;
    28. select * from account;

    演示结果:rollback可以回滚到指定保存点或事务最开始的状态。

     演示2:证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)

    执行SQL:

    1.查看当前会话隔离级别

    2.设置当前会话隔离级别为读未提交

    3.查看表

    4.开始事务

    5.插入数据

    6.提交事务

    7.使用ctrl + 、异常终止MySQL

    8.在终端B上查看表

    1. select @@session.tx_isolation;
    2. set session transaction isolation level READ UNCOMMITTED;
    3. select * from account;
    4. begin ;
    5. insert into account values(1,'张三',100);
    6. commit;
    7. //终端B
    8. select * from account;

    输出:

    异常终止终端A后在终端B仍然可以看到数据,可见commit的数据持久化了:

    演示3:begin操作会自动更改提交方式(事务中的所有的SQL语句都需要显示使用COMMIT语句进行提交),不受MySQL是否自动提交影响

    执行SQL:

    修改自动提交为OFF,开启事务,往表插入数据,使用ctrl + \异常终止MySQL,在终端B查看数据是否回滚。

    1. select * from account;
    2. show variables like 'autocommit';
    3. set autocommit=0;
    4. show variables like 'autocommit';
    5. begin;
    6. insert into account values(2,'李四',10000);
    7. select * from account;
    8. //终端B
    9. //终端A崩溃前
    10. select * from account;
    11. //终端A崩溃后
    12. select * from account;

    输出:

    演示4:证明单条SQL与事务的关系

    执行SQL:

    1. //实验一
    2. select * from account;
    3. show variables like 'autocommit';
    4. set autocommit=0;
    5. insert into account values(2,'李四',10000);
    6. //终端B
    7. //终端A崩溃前
    8. select * from account;
    9. //终端A崩溃后
    10. select * from account;
    11. //实验二
    12. //终端A
    13. show variables like 'autocommit';
    14. set autocommit=1;
    15. show variables like 'autocommit';
    16. select * from account;
    17. insert into account values(2,'李四',10000);
    18. select * from account;
    19. //终端B
    20. //终端A崩溃前
    21. select * from account;
    22. //终端A崩溃后
    23. select * from account;

    做两个实验,一个是自动提交模式,一个是手动提交,验证数据的持久性。

    实验一(手动提交),终端A:

    实验一,终端B:(可见手动提交下,单条SQL不具有持久性)

    实验二(自动提交),终端A:

    实验二(自动提交),终端B:

    结论总结:

    1.begin或者start transaction开始的事务,事务便必须要通过commit提交,才会持久化,与是

    否设置set autocommit无关。
    2.事务可以手动回滚,同时,当操作异常,MySQL会自动回滚。
    3.InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。

    4.如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交) 。

    5.如果一个事务被提交了(commit),则不可以回退(rollback) 。
    6.事务执行中可以选择回退到哪个保存点。
    7.InnoDB 支持事务, MyISAM 不支持事务。

    事务的隔离级别

    基本概念:

    脏读:一个事务在执行过程中读取到了另一个事务未提交的数据。

    不可重复读:事务在执行过程中,读取到的某个数据后,另一个事务修改了该数据,导致第一个事务重新读取数据的结果不一致。

    幻读:一个事务在读取某个范围的数据时,另一个事务插入了新的数据,导致第一个事务重新读取该范围的数据时,发现有新的数据出现。

    读未提交【Read Uncommitted】:所有的事务都可以看到其他事务没有提交的执行结果。相当于没有隔离性,会导致脏读、幻读、不可重复读问题。

    读提交【Read Commited】:该隔离级别的大多数数据库的默认隔离级别,在事务中只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,多次select可能结果不一样。

    可重复读【Repeatable Read】:MySQL默认的隔离级别,确保同一个事物在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。

    串行化【Serializable】事务的最高隔离级别,通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。它在每个读的数据行上加上共享锁,可能会导致超时和锁竞争(效率低)。

    隔离级别如何实现:通过锁实现,常见锁有表锁、行锁、读锁、写锁、间隙锁(GAP)、Next-Key锁(GAP+行锁)等。

    查看与设置隔离级别

    查看全局隔离级别:

    SELECT @@global.tx_isolation;

    查看当前会话全局隔离级别:

    SELECT @@session.tx_isolation;

    查看隔离级别(默认同上):

    SELECT @@tx_isolation;

    设置当前会话或全局隔离级别:

    SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERALIZABLE}

    读未提交(Read Uncommitted)

    设置全局隔离级别为读未提交,在终端A开启事务并更新指定行,在终端A提交事务前,在终端B看到了未commit的数据(即脏读)。

    终端A:

    终端B:

    终端B的事务执行过程中读到了终端A更新但未commit的数据,属于脏读。

    读提交(Read Committed)

    设置全局隔离级别为读提交,在终端A开启事务并更新数据,在终端B开启事务并在终端A事务提交前后读取同一数据读到了不同的数据,即发生了不可重复读的现象。

    终端A:

    终端B:

    在同一事务内,在不同时间段,读取到了不同的值,即不可重复读。

    可重复读(Repeatable Read)

    设置全局隔离级别为可重复读,在终端A开启事务,并更新数据,在终端B开启事务,并在终端A事务提交前后读取同一数据读到了相同的数据,即可重复读。将更新数据改成插入数据,再实验一次,发现没有发生读取数据也是读到了相同的数据,即没有发生幻读。

    注意:设置隔离级别后要退出mysql客户端重新启动。

    终端A:

    终端B:

    可见在终端A的事务提交前后,在终端B的事务中读取到的数据都是一致的,也即具有可重复读的性质。

    倘若将update数据变成插入数据会如何:

    终端A:

    终端B:

    可见还是满足了可重复读,没有出现幻读现象,这是因为MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁 (GAP+行锁)解决的)。

    注意:Next-Key锁 就是间隙锁和记录锁的组合。(源自MySQL官网)

    串行化(Serializable)

    设置全局隔离级别为串行化,其中终端A更新数据后,在终端B开启事务,并读取数据,发现阻塞住了,当终端A事务提交后,终端B才看到数据。

    终端A:

    终端B:

    隔离级别总结

    总结:

    较低的隔离级别可以提高并发性能,但可能会引入数据一致性问题;而较高的隔离级别可以保证数据一致性,但可能降低并发性能。

    不可重复读关注的是修改和删除:读取过的数据,再次读取处理就发现值不一样了。

    幻读的重点在于新增:同样的条件,第一次和第二次读出来的记录数不一样。

    MySQL默认的隔离级别是可重复读,也是兼顾数据一致性和并发性考虑的较优选择。

    一致性的理解

    事务在执行过程中,将数据库从一个一致的状态转换为另一个一致的状态。

    一致性和用户业务逻辑强相关,MySQL提供技术支持,一致性还需要用户业务逻辑做支撑,也就是一致性由用户决定。技术上,有原子性、隔离性、持续性保证一致性。

    多版本并发控制

    数据库的并发场景有三种:

    读-读:没有问题,不用并发控制。

    读-写:有线程安全问题,可能会造成事务隔离性问题,即可能遇到脏读、幻读、不可重复读。

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

    数据库的更新丢失问题是指在并发环境下,多个事务同时对同一数据进行更新操作时可能出现的问题。 

    MVCC(Multi-Version Concurrency Control):多版本并发控制       

    多版本并发控制是一种用来解决读 - 写冲突的无锁并发控制。

    为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照,所以MVCC可以为数据库解决下面问题:

    并发读写时,读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。

    解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题。

    记录中的3个隐藏字段

    DB_TRX_ID:6byte,最近修改事务ID,记录创建这条记录/最后一次修改该记录的事务ID。

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

    DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引(数据行按照索引的顺序存储在磁盘上,具有相邻的物理位置)。

    还有一个flag隐藏字段,记录这数据被更新或删除,记录删除只需标记flag即可。

    undo日志

    MySQL中的一段内存缓存区,用来保存日志数据。

    画图理解MVCC

    假设表中刚插入了一条记录如下,因为不知道创建该记录的事务ID,隐式主键,首条记录也没有回滚指针,所以将他们设置成默认值:

    现在执行事务1,对表记录进行修改,修改张三名字为李四。

    修改记录前,对记录加行锁,将原本记录拷贝到undo log中,然后写入新的数据,事务ID为1,回滚指针指向副本记录,表示新记录的上个版本就是它。

    然后提交事务,释放锁。

    事务1执行后:

    现在再执行事务2,对表中记录进行修改,将age改成19.

    修改记录前,对记录加行锁,将原本记录拷贝并头插入undo log中。

    然后修改记录,事务ID为2,回滚指针指向上一个版本的记录。

    提交事务2,释放锁。

    事务2执行后:

    于是我们就有了一个基于链表记录的历史版本链,回滚就是用版本链中的版本覆盖当前数据。

    这个历史版本链不仅用于数据回滚,还用于其他事务读取历史版本,所以只有当前记录提交且其他事务与版本链无关的时候,该历史版本链才能删除。

    插入和删除的时候如何维护版本链?

    删除:将数据拷贝一份放入undo log中,并将该记录的隐藏flag删除字段置为1,这样回滚后flag字段又变成0,相当于删除的数据又恢复了。

    插入:新插入的数据没有历史版本,为了回滚操作,新插入的数据拷贝一份放到undo log中,且undo log中的记录的flag标记为1,回滚后就相当于数据被删除了。

    上面的一个个版本,我们可以称之为一个个快照。

    概念:

    快照读:读取历史版本的数据。

    当前读:读取当前版本的数据。

    事务对数据进行增删改的时候,操作的都是最新记录,即当前读,需要进行加锁保护。
    事务在进行select查询的时候,既可能是当前读也可能是快照读,如果是当前读,那也需要进行加锁保护,但如果是快照读,那就不需要加锁,因为历史版本不会被修改,也就是可以并发执行,提高了效率,这也就是MVCC的意义所在。
     

    Read View

    Read View就是事务进行快照读操作时产生的读视图(Read View)。

    在该事务执行的快照读的那一刻,会生成数据库系统当前的快照,记录并维护系统当前活跃事务的ID。(每个事务开启,都分配一个递增的ID,越新的事务,事务ID越大)

    Read View在MySQL源码中是一个类,用来可见性判断的,判断当前事务能看到哪个版本的数据。

    其基本组成如下:

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

    那么MVCC是如何实现的?

    结合源码和画图理解:

    画图理解:其中id就是下图的DB_TRX_ID

    当进行快照读的时候就对比DB_TRX_ID和Read View中的水位线id即可。

    当DB_TRX_ID即id等于创建该快照的id或小于up_limit_id那么该记录是可见的。(小于up_limit_id表示事务已提交)。

    当id>=low_limit_id说明事务是快照生成后才提交的事务,则事务是不可见的。

    当事务id在活跃事务id列表中时,表示事务在进行,也是不可见的。
     

    RR与RC的本质区别

    RR是可重复读,RC是读提交,前面提到,读提交是可以读到已经提交的事务的,是不可重复读的,而可重复读的隔离级别是能在一个事务中保证读取数据的一致性的。

    RR和RC级别的不同,本质是Read View生成的时机不同。

    在RR级别下某个事务对某条记录的第一次快照读会创建一个快照及Read View,将当前系统活跃的其他事务记录起来,此后再进行快照读的时候,使用的是同一个Read View,所以对之后的修改不可见,实现了读取数据的可重复读。

    而在RC级别下,在事务中每一次快照读都会生成并获取最新的Read View,所以别的事务提交了,新的快照读也就马上能看到,所以RC具有不可重复读的问题。

  • 相关阅读:
    从 几 个应用入手 了解为什么灵魂绑定代币将为 DeFi 带来大规模采用
    辅助驾驶功能开发-功能算法篇(1)-ACC-多目标选择
    Golang: Store Query Result in a Map
    Oracle RAC集群---基础维护常用语句总结
    Win10文件资源管理器卡顿不流畅的解决方法
    使用二手 gopro 做行车记录仪
    图像数据增强2_标注框同时修改(VOC、YOLO)
    《代码随想录》三刷,与诸君共勉,秋招见!
    leetcode 53 最大子数组和
    LeetCode19.删除链表的倒数第N个结点
  • 原文地址:https://blog.csdn.net/u014801954/article/details/133325514