MySQL事务
一、事务简介
首先给大家举一个例子:我们有如下的销售业务,一个销售业务可能包含很多步骤,比如记录订单、添加积分、管理库存、扣减金额等等,每一个操作都可能对应一条或多条sql语句,但是这个业务却是不可分割的,不能下了订单,不扣减库存。此时我们就需要事务来统一管理这个业务当中的一系列sql语句了。
(1)在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
(2)事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
二、事务分类
1、显式事务和隐式事务
(1)mysql的事务可以分为【显式事务】和【隐式事务】。默认的事务是隐式事务,由变量【autocommit】控制。隐式事务的环境下,我们每执行一条sql都会【自动开启和关闭】事务,变量如下:
SHOW VARIABLES LIKE 'autocommit';
(2)显式事务由我们【自己控制】事务的【开启,提交,回滚】等操作,我们创建一个表,同时展示事务的基础语法,如下:
create database ydlTrx;
use ydlTrx;
-- UNSIGNED代表无符号数,不能是负数
create table user(
id int primary key auto_increment,
name VARCHAR(20),
balance DECIMAL(10,2) UNSIGNED
insert into user VALUES (1,'楠哥',200);
insert into user VALUES (2,'楠哥老婆',50000);
-- 转账业务,必须都成功,或者都失败,所以不能一句一句执行,万一执行了一半,断电了咋办
-- 所以要编程一个整体
-- 都成功
-- 开启事务;
start transaction;
UPDATE user set balance = balance - 200 where id = 1;
UPDATE user set balance = balance + 200 where id = 2;
-- 提交事务
commit;
-- 都失败
start transaction;
UPDATE user set balance = balance - 200 where id = 1;
UPDATE user set balance = balance + 200 where id = 2;
-- 回滚事务
rollback;
我们可以使用begin或start transaction开启一个事务,使用commit提交事务,使用rollback回滚当前事务。
2、只读事务和读写事务
我们可以使用read only开启只读事务,开启只读事务模式之后,事务执行期间任何【insert】或者【update】语句都是不允许的,具体语法如下:
start transaction read only
select * from ....
select * from ....
commit;
有人可能会问,这样和不开事务有什么区别呢?这个在下边学了隔离级别就知道了。
3、保存点
我们可以使用savepoint 关键字在事务执行中新建【保存点】,之后可以使用rollback向任意保存点回滚。
start transaction;
UPDATE user set balance = balance - 200 where id = 1;
savepoint a;
UPDATE user set balance = balance + 200 where id = 2;
rollback to a;
**注意:**Mysql是不支持嵌套事务的,开启一个事务的情况下,若再开启一个事务,会隐式的提交上一个事务:
start transaction;
UPDATE user set balance = balance - 200 where id = 1;
start transaction; -- 这里会自动将第一个事务提交
UPDATE user set balance = balance + 200 where id = 2;
commit;
-- 回滚事务
rollback;
三、事务四大特征(ACID)
1、原子性(Atomicity)
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。如果事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样,这个很好理解。
2、一致性(Consistency)
在事务【开始之前和结束以后】,数据库的完整性没有被破坏,数据库状态应该与业务规则保持一致。举一个例子:A向B转账,不可能A扣了钱,B却没有收到,也不可能A和B的总金额,在事务前后发生变化,产生数据不一致。其他的三个特性都在为他服务。
3、隔离性(Isolation)
数据库【允许多个并发事务同时对其数据进行读取和修改】,隔离性可以防止多个事务在并发修改共享数据时产生【数据不一致】的现象,这里要联想到我们学习过的多线程。
事务隔离级别分为不同等级,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable),后续会详细讲。
4、持久性(Durability)
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
四、事务隔离级别
对于数据库的四大特性中的【隔离级别】是比较难理解的,我们在本小结中详细介绍。
在多个事务【并发操作】相同的表数据时,为了让多个事务都可以得到正确的结果,不会因为互相的交叉操作产生干扰,同时还要保证一定的执行效率,故而提出了不同的隔离级别。
隔离级别分类如下,在不同的隔离级别下可能产生不同的问题,如脏读、不可重复度、幻读等,我们会在后边的课程中一一讲解:
隔离级别 脏读 不可重复读 幻读 解决方案 Read uncommitted(读未提交) √ √ √ Read committed(读已提交) × √ √ undo log Repeatable read(可重复读) × × √ MVCC版本控制+间隙锁(mysql的rr不存在幻读) Serializable(串行化) × × ×
注意:传统意义上的rr级别是存在幻读问题的,但是mysql的rr级别不存在。
在mysql中查看和设置【事务的隔离级别】,语法如下:
-- 查看全局和当前事务的隔离级别
SELECT @@global.transaction_isolation, @@transaction_isolation_isolation;
show variables like 'transaction_isolation';
--5.7 tx_isolation
--8.0 transaction_isolation
-- 设置下一个事务的隔离级别
SET transaction isolation level read uncommitted;
SET transaction isolation level read committed;
set transaction isolation level repeatable read;
SET transaction isolation level serializable;
-- 设置当前会话的隔离级别
SET session transaction isolation level read uncommitted;
SET session transaction isolation level read committed;
set session transaction isolation level repeatable read;
SET session transaction isolation level serializable;
-- 设置全局事务的隔离级别
SET GLOBAL transaction isolation level read uncommitted;
SET GLOBAL transaction isolation level read committed;
set GLOBAL transaction isolation level repeatable read;
SET GLOBAL transaction isolation level serializable;
其中,SESSION 和 GLOBAL 关键字用来指定修改的事务隔离级别的范围:
SESSION:表示修改的事务隔离级别将应用于当前 session(当前 cmd 窗口)内的所有事务;
GLOBAL:表示修改的事务隔离级别将应用于所有 session(全局)中的所有事务,且当前已经存在的 session 不受影响;
如果省略 SESSION 和 GLOBAL,表示修改的事务隔离级别将应用于当前 session 内的下一个还未开始的事务。
1、读未提交(RU)
【ru隔离级别】说的简单一点就是,一个事务可以读取其他【未提交的事务】修改的数据,这种隔离级别最低,一般情况下,数据库隔离级别都要高于该级别,该隔离级别下,可能会存在脏读、不可重复度,幻读的问题。
**脏读:**指的是一个事务读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,读到的数据不一定准确。
A发工资了,B让A把工资打到他B的账号上,但是该事务并未提交,就让B去查看,B一看真的打了钱了,高高兴兴关了网页,此时A急中生智进行回滚,钱瞬间回来,一次蒙混了一个月工资。所以B看到的数据我们称之为“脏数据”。
第一步:使用两个窗口,开启两个事务,且将隔离级别设置为RU。
use ydlTrx;
SET transaction isolation level read uncommitted;
第二步:A给B转账10000元,但是不提交。
start transaction;
UPDATE user set balance = balance - 10000 where id = 1;
UPDATE user set balance = balance + 10000 where id = 2;
第三部步:A通知B进行查账,确实读取到了A未提交事务修改的数据。
start transaction;
select * from user where id = 2;
commit;
第四步:B查账结束,A来一个回马枪,对自己的事务进行回滚操作。
rollback;
第五步:B某天查账,突然发现,哎,怎么少了一万。
start transaction;
select * from user where id = 2;
commit;
2、读已提交(RC)
【RC读已提交】说的是当前事务只能读到别的事物已经提交的数据,该隔离级别可能会产生不可重复读和幻读。
【不可重复读】的官方解释是:【一个事务】(A事务)修改了【另一个未提交事务】(B事务)读取过的数据。那么B事务【再次读取】,会发现两次读取的数据不一致。也就是说在一个原子性的操作中一个事务两次读取相同的数据,却不一致,一行数据不能重复被读取。主要是【update】语句,会导致不可重复读。
A拿着工资卡去消费,系统读取到卡里确实有10200元,而此时B也正好在网上转账,把A工资卡的2000元转到另一账户,并在A之前提交了事务,当楠哥扣款时,系统检查到A的工资卡和上次读取的不一样了,A十分纳闷,明明卡里有钱,为何......
第一步:将事务的隔离级别设置为RC。
SET transaction isolation level read committed;
第二步:A准备去消费了,检查存款显示有余额,贼高兴。
start transaction;
select * from user where id = 1;
第三步:B在A没有提交事务的时候,进行了一笔转账,并且提交了事务。
start transaction;
UPDATE user set balance = balance + 500 where id = 2;
UPDATE user set balance = balance - 500 where id = 1;
commit;
第四步:A再次查账,同一个事务里,发现钱少了。
select * from user where id = 1;
当隔离级别设置为Read committed 时,避免了脏读,但是可能会造成不可重复读。
大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。如何解决不可重复读这一问题,请看下一个隔离级别。
#3、可重复读(RR)
学习完不可重复读,理解【可重复读】就简单多了,他的意思是,同一个事务中发出同一个SELECT语句【两次或更多次】,那么产生的结果数据集总是相同的,在RR隔离级别中可能出现幻读。
**幻读:**一个事务按照某些条件进行查询,事务提交前,有另一个事务插入了满足条件的其他数据,再次使用相同条件查询,却发现多了一些数据,就像出现了幻觉一样。幻读主要针对针对delete和insert语句。
不可重复读强调的是两次读取的数据【内容不同】,幻读前调的是两次读取的【行数不同】。
B在银行部门工作,B时常通过银行内部系统查看A的账户A信息。有一天,B正在查询到楠哥账户信息时发现A只有一个账户,心想这家伙应该没有私房钱。此时A在另外一家分行又开了一个账户,准备存私房钱。同时,B在同一个事务中又一次查询,结果显示出的A账户居然多了一个,真实奇怪。
第一步:将数据库的隔离级别设置为RR。B
set transaction isolation level repeatable read;
第二步:A开启事务。
start transaction;
第三步:B查账户。
start transaction;
select * from user where name = '楠哥';
第四步:A趁机开户,并提交事务。
insert into user values(3,'楠哥',10000);
commit;
第五步:B再查询并打印,应该发现A多了一个账户,但是没有,并没有出现幻读。
select * from user where name = '楠哥';
**注意:**在mysql中的RR隔离级别中,innodb使用mvcc+锁帮我们解决了绝大部分的幻读情况,
上边的例子稍微修改一下,我们就能看到幻读现象了。
第一步:将数据库的隔离级别设置为RR。
set transaction isolation level repeatable read;
第二步:A开启事务。
start transaction;
第三步:B查账户。
start transaction;
select * from user where name = '楠哥';
第四步:A趁机开户,并提交事务。
insert into user values(3,'楠哥',10000);
commit;
第五步:B好心给所有的账户充值100,再查询并打印,结果发现A多了一个账户,出现幻读。
-- 先给所有的账户钱充值一百
UPDATE user set balance = balance + 100;
select * from user where name = '楠哥';
这里边的原理过于复杂,目前我们先卖个关子,等我们学完【锁和mvcc】以后回来在看,就能明白了。
4、串行化
事务A和事务B,事务A在操作数据库时,事务B只能排队等待
这种隔离级别很少使用,吞吐量太低,用户体验差
这种级别可以避免“幻读”,每一次读取的都是数据库中真实存在数据,事务A与事务B串行,而不并发。
第一步:修改数据库的隔离级别
SET transaction isolation level serializable;
第一步:A执行查询操作
begin;
select * from user;
第二步:B执行查询操作
begin;
select * from user;
第三部:B想执行一个删除操作,发现事务被阻塞了,需要等待
delete from user where id = 9;
第四部:A这边一提交,V那边就能操作了
commit;
复制代码