MySQL专栏链接:https://blog.csdn.net/shun_hua/category_12685814.html?spm=1001.2014.3001.5482
前言
在上一篇关于表操作的文章当中,介绍了关于表的相关操作,主要是表的结构和内容的操作,分属于DDL(数据定义语言)以及DML(数据操纵语言),不熟悉的同学可以通过专栏链接进行复习,此处就不再赘述了。本篇文章的内容就来给事务的内容画下一个句号,即学习事务,主要通过查询和写入进行体现,属于DCL(数据控制语言)。话不多说,马上开始!
正文
一、事务
概念
- 事务,即先后执行且存在逻辑的一批表操作,这批表操作被视为一个整体,要么被统一执行,要么失败进行回滚。
- 当这批操作执行过程中失败时,将数据恢复到没有对表进行操作的状态,这被称之为回滚。
- 这批表操作通常指的是DML(数据操纵语言),比如insert,delete,select,update,增删查改。
- 在多人协作的场景下,可能会存在对同一张表多次并发写入的类似情况,因此事务显得尤为重要。
- MySQL面向应用层和客户,其保证事务是为了解决在实际应用中不同客户端并发执行一批SQL语句的时间段内,发现表的数据跟操作对不上,导致执行错误或者执行的结果不符合预期。
性质
- 事务的保证通常是通过四个方面来保证的,将这其首字母提出来大写简称ACID。
- Atomicity,原子性,最终的执行结果要么成功,要么失败进行回滚,比较符合事务的概念。
- Consistency,一致性,表在事务执行前一个状态,执行后一个状态,执行中表的数据对其它用户保持不变。
- Isolation,隔离性,不同用户同时执行事务时,都看不到对方干了啥,即独立互不影响。
- Durability,持久性,执行成功之后的状态不发生改变,通常被永久保留。
应用场景
- 在并发度较高以及数据量庞大和对表操作的内容比较复杂的情况下,通常会使用事务来保证表数据的安全性。
- MySQL在5.5.8版本之前采用的默认存储引擎MyISAM不支持事务,在之后变为InnoDB支持事务。
补充:MyISAM采用非聚集索引,即数据和索引分开进行存储。InnoDB采用采用聚集索引,数据和索引放在一起。
二、隔离
概念
- 隔离,即保证事务在执行的过程中尽量不受到干扰。
- 对于隔离通常谈论是隔离级别,如下简称3R1S。
- 从隔离级别的程度由低到高分为RU,RC,RR,S,共四种级别。
- Read Uncommitted,读未提交,可以读到其它事务没有提交的结果。
- Read Committed,读提交,可以读到其它事务提交的结果。
- Repeatable Read,可重复读,重复读的是事务之前的表数据。
- Serializable,串行化,强制事务的执行是串行化的,由间隙锁(防插)和行锁(只读)进行实现。
图解:
补充:串行化指的是在事务前强制对事务进行排序,通常使用锁来讲进行实现,比如两个人同时执行一次事务,小红和小明,假如小红抢到了锁先执行事务,那么小红拿的是之前的表,小明拿的是小红执行事务后的表。
- 不同程度的隔离级别,适用于不同的场景,但也有着不同的缺陷。
- 读未提交,适用于内容动态的更新,比如金铲铲,在玩的过程中更新了一些活动,但是版本没有更新;但不适用于计算汇率,当通过表公式计算时,查找时表的数据发生了变化,导致通过公式计算有误,这被称之为脏读问题。
- 读提交,适用于游戏更新bug,比如金铲铲热门阵容削弱了,赌狗赌不成了,但幸好我不是赌狗^ _ ^;但不适用于证券交易,比如在进行交易前,股票跌了买入,但是交易中,股票又涨了,那么交易如果按最新的价格就会失败,这被称之为不可重复读问题。
- 可重复读,适用于游戏添加新英雄,比如金铲铲小小英雄^ _ ^。但不适用于医生进行病因诊断,比如你没给医生说你对抗生素过敏,然后医生给你开了抗生素的药,导致你GG,这被称之为幻读问题。
- 串行化,基本适用于所有场景,但如果对性能要求很高的话,还是灵活选择如上的几种隔离方案吧!
实操
- 版本支持
说明:如下的演示环境为Linux下Ubuntu系统,安装的是最新版本的mysql,大家跟我使用一样的进行实操同步性更好一些。
- 命令
show engines;
#查看搜索引擎的基本属性
- 从表中可以看到关于数据库引擎属性Transation,即是否支持事务,XA,即是否支持一致性,Savepoints,即是否支持保存点。
- 在事务中可能要执行的mysql语句可能过多,害怕出错,因此分批执行,给每一批打一个标记就是所谓的保存点了。
- 从以上所说的三栏来看,InnoDB全部支持了,而其它的要么就没有提供这些功能,要么就将三项默认设置为关闭状态。
- 综上,MySQL默认采用的是InnoDB,所以MySQL(最新版本)默认支持事务,一致性,保存点。其它引擎要么没有要么默认不支持。
关于如下实验操作的图解:
读未提交
- 金铲铲活动数据库
说明:请你想象左边是金铲铲的管理服务器的人员,而右边是玩家正在查看是否有白嫖的活动,^ _ ^。
补充:
- 这里还要设置一下会话隔离与全局隔离同步比较好,比如我的机器上就是会话隔离的优先级大于全局隔离,下面就不再赘述了。
- 每个会话的autocommit默认都为ON,得每个会话都把autocommit设置为0。
- 详解服务器管理员与玩家擦肩而过的过程——
此时读未提交不是问题,但是在比如说银行用公式计算时就会出大问题,用数据代入到公式里面计算汇率之类的金融数据,不能一个数据一个数据改,公式中所有的数据都要统一进行改,一旦别人要用就拿到了正在改动,即错误的数据,这种问题我们称之为脏读问题。解决其实也很简单,设置隔离模式为读提交即可,此时再代入上述场景使用公式计算的数据可能不是最新的,但也不是错误的。
读提交
说明:请你想象左边是金铲铲的管理服务器的人员,而右边是玩家。
- 金铲铲的修复热门补丁
此时读提交不是问题,银行把数据全部替换完毕之后,使用最新的数据用公式计算也没有问题;但是在证劵交易所进行股票交易的过程中,你的策略是低买高卖,如果你在交易中原本是低的,但在交易过程中如果按照隔离方式按照读提交,突然涨了,你的交易就会不符合预期,从而可能会导致亏本问题,这被称之为不可重复读问题。
可重复读
说明:请你想象左边是官方金融的管理服务器的人员,而右边是股民。
- 股市交易
此时进行股票交易就没有问题,但是如果要进行看病,假如你没给医生说你对啥过敏,医生开过药才知道,那你不是白去医院看了,弄不好,你就要GG重开了。
串行化,最高的隔离级别。
-
首先建立一张库存表,然后设立两个会话以及全局的隔离等级为序列化。
-
然后仓库进了两件潮牌男衣的衣服。
-
接着有两个客户过来要买衣服,先要看一下有没有,然后再考虑买或者不买的。
说明:此时读是可以并发访问的,我这里少写一个commit不是很完整,读者读到这里自己进行实验时要及时进行思考和补充。 -
最后两个客户考虑要买,于是库存是少买两件的。
网上购物的库存是必须要按照序列化的隔离程度进行实现的,而且我这里只是简单的介绍一个例子,帮助大家进行理解,望各位读者不要死扣逻辑是否完整,符合现实之类的,只是简单地帮助各位举例理解序列化而已。但是在其它场景下,比如同时对银行数据做更新操作,两个事务同时进行更新时,如果按照串行化就会出大问题,因为两个事物都是基于一个旧的数据进行更新的。
综上,我们四个隔离程度的例子就举好了,希望大家能够基于现实角度进一步地理解这些知识,其次,事务中的一些操作我这里就咱不赘述了,那张图其实表述的已经很明白了(下面会贴出来的),我们接着从事务的四个性质进一步理解不同隔离程度下的区别。
- 原子性,要么执行成功,要么失败进行回滚,在保存点操作以及四个隔离程度都有所体现。
- 持久性,在四个隔离程度下都有所体现,即数据都会在事务执行后永久保留。
- 一致性,表在事务的执行中状态的变化,其实在不同的隔离级别都有所不同。
- 隔离性,不同的隔离级别,事务之间彼此的影响程度是不一样的。
图解:
三、多版本控制
概念
- 用来解决读写冲突的无锁并发实现的一种机制。
- 并发场景一般来说有三种,读-读并发,读-写并发,写-写并发。
- 读-读,即都在读而不修改,一般来说是没有线程安全问题的。
- 读-写,即有在读表的,有在向表中写入数据,根据不同的隔离程度,会有脏读,不可重复读,幻读问题。
- 写-写,即都在向表中写入数据,会存在更新丢失,具体一点就是回滚丢失和覆盖丢失,下面会进行补充和说明。
- 机制的实现一般是采用undo日志中的快照实现的,具体涉及的对象为ReadView。
- 并发场景一般来说有三种,读-读并发,读-写并发,写-写并发。
补充:
- 回滚丢失,打个比方,比如我们要做一个实验创建了一张表,然后存放了一些数据,此时称为表1,然后启动两个事务,分别为事务A和事务B,事务A操作完成功,此时的表称为表2;事务B操作的时候失败,如果回滚到表1,而不是表2;此类问题被称为回滚丢失。
- 覆盖丢失,还是类似的例子,比如我们要做一个实验创建了一张表,然后存放了一些数据,此时称为表1,然后启动两个事务,分别为事务A和事务B,事务A操作完成功,此时的表称为表2;如果事务B操作成功,覆盖表2,此时最新的表为表3;此类问题被称为覆盖丢失。
- 机制的实现博主简单画一个图,方便先大家进行认识机制,具体地下面会详谈。
拓展:MySQL三大日志
- undo log,也叫做撤销日志,即主要负责数据的撤销,隔离等,主要保证事务的原子性,即执行失败回滚;隔离性,即对同一张表增删查改的结果是不一样的。
- redo log,也叫做重做日志,即主要负责数据库服务崩溃时进行数据的恢复等,从而保证事务的持久性。
- bin log,也叫做逻辑日志,即主要负责主从数据备份时进行数据同步等,从而保证事务的一致性。
补充一下,所谓的主从数据备份,打个比方,我们日常生活应该用过使用云服务的软件,比如说有道云笔记,moji日记等等,那么完整的讲有三端,即电脑端,云端,手机端,那么可以把云端看成主数据库,手机端和电脑端看成事从数据库,一旦主数据库更新,那么软件必须要让你马上看到手机端和电脑端的变化,这就很好的体现了数据库的一致性。
undo日志
- 隐藏字段,如下的第一个用来进行数据库索引,剩下三个主要与undo日志有关。
- DB_ROW_ID,隐藏主键,6字节,数据库中用来标识唯一行的id,如果没有设置默认主键,即索引ID的话,此ID会设置为默认主键。
- DB_TRX_ID,事务ID,6字节,用于标识每一个事务,基于undo日志中保存的最新ID自增一得来。
- DB_ROLL_PTR,事务回滚指针,7字节,主要用于进行回滚操作,一般使用链表的形式组织起来,方便直接回滚。
- Flag,标志,1字节,用于标识操作的状态以及事务的状态(begin or commit)等。
如下是关于隐藏字段的举例解释,方便大家看图解进行理解(实际还是要看源码),不在此处发挥作用的隐藏主键我就忽略不写了,大家知道它有啥作用就行。
- 首先,我们创建了一张关于股票价格的表,并已经在里面插入了数据,此会被undo日志进行记录在内。
- 其次,股票价格更新,事务开始,处于事务中的阶段,但是还没有提交。
- 然后,股票价格更新,事务结束,将操作记录到undo中,然后表中的DB_ROLL_PTR要进行清空。
- 接着,股票价格又更新了,事务开始,处于事务中的阶段,但是还没有提交。(往下有点冗余,看会的可以直接跳过)
- 最后,股票价格更新,事务结束,将操作记录到undo中,然后表中的DB_ROLL_PTR要进行清空。
补充:
- 在事务的执行期间,如果要删除数据,那么只需要将Flag设置为DELETE,表示为删除,然后写入到undo日志中,那么失败进行回滚,即将数据恢复的过程中,就是将Flag恢复到原来的UPDATE或者INSERT之类的状态。
- 对表的增删改查,即不断地修改属性信息,然后将其写入到undo日志中形成版本链的过程。
综上,undo日志关于事务记录的内容,就讲解完毕了,我们接下来进一步介绍关于读写并发的机制,即快照。
快照
- 概念, 快照是数据库在某一时刻的完整数据状态的副本,原理是通过Read View进行实现。
说明:
- Read View是一个结构体,里面记录者关于事务的相关信息,下面会把主要源码贴出来,方便大家进行理解。
- Read View中存放着关于时间的信息,一般来说为时间戳,提取快照一般来说是根据时间戳提取的。
- 上述的undo日志的例子只是简单的Read View的体现,而具体的体现我们如下细讲。
补充:
- Read View源码
class ReadView {
// 省略...
private:
/*
* 高水位:大于等于这个ID的事务均不可见
*/
trx_id_t m_low_limit_id;
/*
* 低水位:小于这个ID的事务均可见
*/
trx_id_t m_up_limit_id;
/*
*事务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;
// 省略...
};
- 隔离策略
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));
}
图解:
- 在不同的隔离程度下,形成的Read View会有所不同,基本上差别都在于活跃事务和新来事务的判断。
- 对于读未提交,那么已提交事务能看到,未提交的结果也会看到。
- 对于读提交,那么已提交事务能看到,未提交的结果不会看到。
- 对于可重复读,那么已提交事务不能看到,未提交的结果更不能看到。
- 对于序列化,那么已提交的事物能看到,除了读读场景,未提交的结果得等到提交之后才能看到,否则会陷入阻塞。
- 究其根本就在于对于不同隔离程度下m_up_limit_id以及 m_low_limit_id的划分,以及对应的技术的实现,比如锁之类的。
补充:
- 快照读,即定格在最开始的表数据中,而不对其它事务对表的修改做更新,其实就是隔离级别为可重复读的事务中的一次结果。
- 当前读,其实就是读取最新的事务的结果,即随时都可能被其它事务的修改而进行更新。
- 要在可重复读的事物中读取按照当前读的形式进行读表可以使用
select * from [表名] lock in share mode;
进行读。 - RR,即Repeatable Read,RC,即Read Committed两者的区别就在于一个在快照读一个在当前读。
尾序
最后,后续的MySQL文章可能没有时间进行更新了,因为博主要转入考研的准备工作,然后开始考研了,但是我会把考研总结的知识点以博客的形式全部发布出来的,敬请期待。如果觉得内容不完整的话,博主在这里推荐一位CSDN的一位大佬——2021dragon
,去看人家MySQL专栏的文章,个人觉得写的还是比较全的,我也是有一部分参考人家的文章进行思考写的。好了,本文到这里就结束了,我们下篇文章再见吧!我是理工小羊,期待你的关注!