目录
事务
引入
在MYSQL中,多个客户端可以同时连接到连接mysqld,并访问同一个数据库
- 也就是多线程会对数据并发访问
- 在这个过程中可能会出现各种各样的问题
我们来举一些场景的例子:
买票
当客户端1判断成功后,进入买票流程
- 此时当前线程的时间片到了,另一个线程执行
客户端2来的时候,票还没有卖出去
- 所以也成功判断,进入买票流程
之后就会出现一张票卖了两次的情况
转账
当某银行进行同行转账,正常流程是:卡1扣钱,卡2增加同数目的钱
- 如果卡1成功扣款后,还没来得及给另一张卡加上,就出现了异常,导致操作中断
- 中断后可能操作无法继续,于是就产生了一个中间过程
如何解决
mysql如何解决此类问题呢?
将操作过程变成原子的
- 按照上面例子,就是要么不买,要么买到票 ; 如果转账不成功,就进行回滚,将数据恢复到初始状态(将扣掉的钱加回来),等待下一次再操作
操作之间不能互相影响
- 不能在我买的时候,你也来买
- 就和多线程访问同一份临界资源一样,在一个线程进行访问时,要保证其他线程不能打扰
购买成功是永久有效的
- 不能付钱了又说购买失败/这张票失效 或者 转账后不久钱又莫名消失了
买前/买后状态要是确定的
- 买前就是没买
- 买后就是已经买了/没有成功
- 没有其他中间状态,比如上面的转账,不能出现像上面那样预期之外的结果
以上是已经提出的事务方案定义的规则
概念
理解
要完成某个操作,一般是需要一批sql组合起来共同实现
- 只有把这些sql语句看作一个整体,才有意义
比如,转账的时候:
- 一定是先在一个账户上扣钱,再在另一个账户上加钱,需要两条sql语句
- 如果单拎出来一个,没有任何意义,只是一条sql语句
- 但如果看作整体,站在使用者的角度来说,这就是转账逻辑(两条语句之间存在某种逻辑关系)
这样组成的整体,是一组数据管理语言(DML),被称为事务
- 如何理解事务的本质 -- 需要站在mysql上层(使用者)来看待这些sql语句,他们需要完成某个具体业务的动作
- 某个业务的需求 --转化成-> 多条sql语句 = 事务
所以,事务就是由若干条sql语句构成的集合体,用于完成某个特定任务
例子
假设要删除某个已经毕业学生的全部信息
- 这个工作一定涉及到多张表,也就需要多条sql语句
站在程序员角度:
- 就只是多条del语句
但站在使用者来说:
- 这些del合起来 = "在系统中去除该学生的所有信息",于是,这些sql语句就被封装成了一个事务
所以,事务是mysql为我们提供的一种程序的术语
- 严格上来说不是程序员使用的术语,只是为了表示这一组sql语句是有逻辑关系的
问题
同一时刻,会有大量的sql语句被封装成事务,向mysql服务器发起事务处理请求
- 如果访问的是同一份资源,就注定会有多条sql交叉并发运行的情况
- 就可能会出现数据不一致的问题
除此之外,要是执行到一半,遇到异常无法正常继续执行 / 不想继续执行
- 怎么办?总不能数据修改到一半就不管了吧
所以,单纯只有事务的概念还不够,还需要满足4个属性来保证事务的正确运行
四个属性
原子性
保证事务中的所有操作没有中间状态,要么全部完成,要不全部不完成
不会结束在中间某个环节- 如果执行过程中发生错误,会被回滚至当前事务执行前的状态
注意,我们需要在外界看来,事务是原子的
- 但我们心里要清楚,肯定是会有[事务执行中的过程]的
- 我们要做的措施都是基于正在执行的事务的
一致性
事务开始之前和事务结束以后,数据库的完整性没有被破坏
- 从一个状态->另一个状态,结果是可预期的
在技术上,并没有设置策略来保证一致性
- 是通过另外三个属性,以及用户的配合来保证一致性的
隔离性
数据库允许多个并发事务同时对其数据进行读写和修改的能力
- 那么相应的,数据库就要提供技术支持 -- 隔离性,它可以防止多个事务并发执行时由于交叉执行而导致数据的不一致
隔离性分为多个级别,来应对不同的需求:
包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )
持久性
一旦将事务处理完成,对数据的修改不能因为故障等问题丢失
这里只是大概了解下概念,后续会详细介绍
总结
事务就是在ACID四条属性的加持下,由一条或多条sql语句组成的集合体
既然mysql中会存在大量事务,就需要将他们管理起来 -- 先描述,再组织
- 所以,多条sql语句到来后,将这些sql打包成一个事务对象 -> 放入事务处理列表中
为什么要有事务
事务不是mysql天然就有的,而是被设计出来的
- 本质上是为了让应用层编写程序时更方便,不需要应用程序来解决并发问题
就像是我们去银行柜台办服务
- 我们不需要考虑这笔钱在流通过程中会出现什么问题,出现问题该怎么办
- 操作都是由银行人员完成的,只需要告诉他们我要干嘛就行
这就是事务封装存在的意义
查看是否支持事务
不是所有的存储引擎都支持事务(transactions)
- 使用show engines\G查看,通过comment / transactions可以知道该存储引擎是否支持事务
这里的innodb就是支持事务的:
而myisam及其其他存储引擎,都是不支持事务的:
目前只有使用了innodb数据库引擎的数据库/表才支持事务
除了关系型数据库外,nosql,redis 等也支持事务
- 只不过实现的方式不同,也没有mysql设计的这么完善
事务操作
我们一边做测试,一边介绍事务操作
提交方式
查看
show variables like 'autocommit';
- 默认是将事务自动提交的
设置
SET AUTOCOMMIT=1/0;
- 设置该字段为1,自动提交事务
- 为0,关闭自动提交
我们这里就不修改了,为后面的测试做准备
隔离级别
为了之后的事务测试,我们先将隔离级别设置成读未提交(最低级别,方便我们查看操作结果)
set global transaction isolation level READ UNCOMMITTED;
- 设置好后,需要重启客户端才能生效
查看隔离级别
select @@tx_isolation;
- 可以看到,我们将隔离级别修改成功:
我们使用两个客户端来模拟两个并发访问mysql服务器的客户端,来制作出若干并发场景
测试表
员工工资表 :
create table if not exists account( id int primary key, name varchar(50) not null default '', blance decimal(10,2) not null default 0.0 )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
事务的开始和回滚
启动事务
start transaction / begin
- 这条语句执行后,后续输入的语句都属于同一个事务
当我们让两个客户端都启动事务,两边就开始并发执行:
设置保存点
savepoint + 保存点名称
- 支持定向回滚
- 就跟玩游戏存档一样,我们可以随时读档
我们在表1中插入每条数据前,都设置一个保存点:
当我们使用客户端1向表中插入数据后,另一个客户端可以看到数据的增加:
定向回滚
如果在事务进行中,我们突然后悔了,不想插入某条数据,我们可以进行定向回滚
- rollback to + 保存点名称
跟游戏读档一样,但有一点不同:
- 一旦回滚后,该保存点之后的操作全都没有了(相当于后续的存档也消失了)
客户端1执行回滚后,在客户端2中也能看见更新后的数据:
回滚
如果不设置保存点,还能回滚吗?
- 可以回滚,只是不能定向回滚了,会直接回滚至事务开始前
- 语法 -- rollback
提交事务
commit
- 一旦事务提交,修改的数据就被持久化了,即使再执行回滚操作也不会有变化
- 所以,执行回滚都是在事务运行期间进行的
执行期间发生异常
如果在事务执行期间发生异常怎么办?
按照我们前面在原子性里介绍的,异常后数据库会自动进行回滚,所以我们需要让客户端崩溃
- crtl \ 让客户端1在执行事务期间崩溃退出:
- 查看客户端2的表数据时,发现已经更新成事务执行前的状态了:
也就说明mysql已经完成了自动回滚
如果关闭终端
- 因为事务依然没有提交,所以还是自动回滚了
如果我们先提交事务,再崩溃:
- 客户端2的数据依然会保留下来,并不会因为崩溃而丢失:
事务的提交方式
与手动开启事务无关
我们之前查看过,事务的自动提交是开启的,那为什么在客户端崩溃后依然被自动回滚了呢?
- 当我们使用begin创建事务时,是需要手动提交的,因为手动开启,就要对应手动提交
- [手动开启事务]与[事务自动被提交],这是两个概念,互相并不冲突
如果我们把自动提交关闭,依然还是刚才的现象
- 说明自动提交是否设置,与begin操作无关
单条sql和事务的关系
我们先将自动提交关闭:
我们在客户端1直接执行单条sql,可以在客户端2看到数据确实被删掉了:
但当我们让客户端1异常退出时,再次查看2中的表数据,已经被删掉的数据竟然又回来了:
- 说明数据被自动回滚了
- 而这是在自动提交被关闭的情况下进行的
如果我们开启自动提交,重新上述步骤:
即使客户端1崩溃,客户端2中查看到的数据依然被删除了:
总结
事务自动提交是否设置会影响单sql的执行结果
因为每一条sql语句都会被mysql封装成一个事务
- 所以,如果我们没有设置自动提交,单条sql语句执行后,依然处于事务执行中
- 一旦客户端崩溃,就会自动回滚
而如果设置了自动提交
- 语句执行完,事务就自动提交了
- 所以即使发生异常,数据也不会丢失
因为自动提交是默认打开的
- 所以我们感知不到
- 即使我们不了解事务的概念,也不影响操作
如何验证这一点?
我们可以在关闭自动提交的情况下,执行完单sql后,进行手动提交(commit):
- 发现即使客户端崩溃,数据也没有被回滚
- 说明单sql语句确实是被包装成了事务
以上,我们可以看出事务的两大特性 -- 原子性(回滚),持久性(commit)
事务的隔离性
mysqld作为服务器,一定会被多个客户端并发访问 -- 这是前提认知
理解
在技术上,sql的执行过程一定是分为执行前,执行中,执行后(因为可能包含多条sql)
- 我们希望一个事务在[执行中的过程]不被其他因素影响,如果确实被影响了,那就直接回滚到执行前
- 上述过程在外层看来,它就是原子的
就像多线程中的锁一样
- 拿到锁的线程依然会在执行过程中被切走,但在其他线程看来,即使被切走,他们也无法访问那份临界资源
- 所以依然是原子的
要注意"实际情况"和"看上去"的区别
正因为实际会有执行中的状态,才会出现多个事务可以并发执行
- 才会有多条sql语句访问同一张表,甚至同一行数据
- 所以一定会出现多个事务之间互相影响的情况
- 比如,读着读着数据不一样了
为了保证一个事务尽量不被其他事务影响,就出现了隔离性
- 数据库允许事务受不同程度的干扰,就有了隔离级别
隔离性理解
每个事务到来时,应该只能看到它到来时的东西
- 所以,需要隔离性来保证 -- 一个事务在执行时,能在一个稳定的环境中进行,不被其他事务干扰,导致看见了不应该被看见的数据
事务的执行过程就像是时间轴上的一条线
- 因为是一条线,并且先到来的不一定先退出,就有可能会和后到来的产生交集:
- 即使[先到来的]在[后到来的]的执行过程中完成了,[后到来的]也不应该看见被更新的数据
- 这样才符合我们前面讨论的[每个事务只能看见到来时的数据]
- 只有当[后到来的]在[先到来的]执行完后才到来,才应该看见更新后的数据
- 也符合隔离性的目的 -- 保证事务不被打扰,只要看不见被其他事务更新的数据,就相当于没有被干扰
隔离级别理解
具体的隔离性,应该隔离到什么程度?
假如没有隔离性:
- 两个客户端在并行访问时,只要一方修改了数据,另一方立马能看到
如果添加隔离性:
- 就可以实现我们前面讨论的,在执行过程中,其他客户端更新的数据"不会被看到"
- 但这个"不会被看到"到什么程度,也就对应了不同的隔离级别
为了满足不同的应用场景,定义了不同的隔离级别
- 可以让我们在隔离性的前提条件下看到不同的内容
总结
事务场景中,隔离是必要的
- 多个事务中的多条sql可能会并发运行,为了保证数据的一致性和完整性,必须将这些事务隔离开来
- 为了保证事务运行中,不会/受到特定程度的干扰,所以有了隔离性
- 隔离,不是针对单条sql语句的隔离,而是对运行中的事务进行互相隔离
隔离级别
读未提交【Read Uncommitted】
在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果
- 相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等
我们前面做实验时,就是使用读未提交的方式
读提交【Read Committed】
该隔离级别是大多数数据库的默认的隔离级别
- 但不是mysql默认的
一个事务只能看到其他的已经提交的事务所做的改变
- 这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果
可重复读【Repeatable Read】
只有当 自己这端结束事务后,才能看见其他客户端对数据库操作后的数据
- 它确保同一个事务在执行中多次读取操作数据时,会看到同样的数据行
- 这是mysql默认的隔离级别
- 会有幻读问题
串行化【Serializable】
事务的最高隔离级别,它通过强制要求 所有语句按照到来顺序,一个一个执行
- 保证了数据的绝对安全
- 但会导致效率问题,因为是串行操作
总结
以上级别,前三个都与读有关
- 它们定义的是,在并发环境下,事务在读取数据时的可见性
- 如果是事务之间需要同时修改同一份数据,此时只能是串行操作
但如果所有操作都只是单纯的加锁,可能会影响mysql的效率
- 所以通过隔离性,可以保证在数据安全的情况下,让mysql做更多的读写并发工作
- 读写并发 -- 一方插入/修改/删除,一方读数据,而这也是mysql最常见的场景
查看隔离级别
全局
SELECT @@global.tx_isolation;
会以全局隔离级别作为当前会话的初始隔离级别
当前会话
- 当你拿着账户密码去登录mysql后,进入的命令行界面,就是一个会话
- 该会话的生命周期从登录成功开始,到退出登录为止
SELECT @@session.tx_isolation; SELECT @@tx_isolation;
第二个是第一个的缩写
设置隔离级别
set session / global transaction isolation level read uncommitted / read committed / repeatable read / serializable
设置全局
全局的修改了会影响后续所有客户端
- 但不会影响正在运行的会话(可以把这两个认为是两个变量,一个的改变并不会影响另一个)
- 只有在会话启动时,才会读取这个全局隔离级作为初始值
一般修改隔离级别,需要保持所有会话遵循相同的规则
- 所以一般都会修改全局的
设置当前会话
当前会话的修改了只影响当前这个客户端,并且退出后又恢复了
- 在使用时,根据就近原则,会使用当前会话的隔离级别