事务
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败
默认mysql的事务是自动提交的,也就是说,当执行一条DML语句,Mysql会立即隐式的提交事务
SELECT @@AUTOCOMMIT; # 1 开启自动提交事务 0 手动提交事务
SET @@AUTOCOMMIT = 0; # 设置为手动提交事务
- BEGIN 或 START TRANSACTION 显式地开启一个事务
- ROLLBACK 事务回滚
- COMMIT 事务确认
事务是对数据库执行的一批操作,要么全部成功,要么全部失败(就是回滚)。事务是一个原子操作,是最小执行单位。 事务有4大特性,ACID,即原子性,一致性,隔离性,持久性。
四个特点(ACID):
-
Atomicity: 原子性 单个事务,为一个不可分割的最小工作单位,整个事务中所有的操作要么全部commit,要么全部失败rollback。数据记录在undo log中,当前行的历史版本在undo log中。事务的原子性由undo log 来保证。 -
Consistency: 一致性 数据库总是从一个一致性的状态到另外一个一致性的状态。mysql通过原子性,隔离性,持久性,最终实现数据库的一致性。 -
lsolation: 隔离性 一个事务所做的修改在最终提交以前,对其他事务是不可见的。也就是说并发的事务之间不能互相干扰。事务的隔离性是由MVCC(多版本并发控制)+锁来实现。 -
Durability: 持久性 一旦事务提交,所做的修改会永久的保存到数据库中。主要由redo log实现。
事务隔离级别
数据库的隔离级别主要有以下四个:
1、可重复读RR(Repeatable Read默认):(一个事务的执行[增删改]影响不到另一个事务的执行[查],另一个事务的执行[增删改]也不会影响到当前的事务[查],也就是说,我开启了事务后,你另一个事务的操作[增删改]改变不了我开启事务时的数据信息{但会出现幻读(就是你另一个事务新增了一条数据,我查是查询不到的这条新增的数据的,但是我添加相同的数据提交事务的时候,却提交不了,数据已经存在【就是这个事务看不到那个事务的操作】)})一个事务执行过程中看到的数据,总是跟整个事务启动时看到的数据是一致的,在此隔离级别下,未提交的变更对其他事务同样不可见。不可重复读是mysql的默认隔离级别,解决脏读,不可重复读的问题(同一个事务多次读取同样的记录结果是一致的),但是有幻读的情况。 幻读:当事务A读取某个范围的数据时候,另一个事务B在这个范围插入了一条数据,事务A再次读取这个范围的数据时,会产生幻读。
2、读已提交 RC(Read Committed):一个事务提交之后(commit),才能被其他事务看到。当一个事务在执行过程中,数据被另一个事务修改,造成本次事务前后读取的信息不一样,这种情况为不可重读(与RR向反,另一个事务提交后,查到的不是刚开始事务的数据了,而是另一个事务提交的新数据)。
3、读未提交 RU(Read Uncommitted):一个事务还没提交,做的变更就能被其他事务读取到。(别的事务,值的是 同一时间进行crud的操作)事务能够读取到未提交的事务,这是脏读。
4、序列化(串行化)S(Serializable):串行执行,即同一时刻只能允许一个事务执行,读写都会加锁,读加读锁,写加写锁。当出现读写锁冲突的时候,后访问的事务要等前一个事务执行完才可。 最高的隔级别,完全服从ACID的隔离级别。所有的事务一次逐个执行,事务直接不会产生干扰。改级别防止脏读,幻读,不可重复读等问题。
事务并发带来的问题
问题 | 描述 |
---|
脏读 | 一个事务读到另一个事务还没有提交的数据 | 不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读 | 幻读 | 一个事务按照条件查询数据时,没有对应的数据行, 但是在插入数据时,又发现这行数据已经存在,好像出现了"幻影" |
- 脏写
- 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。
- 脏读(Dirty Read)
- 事务
A 读取到了事务B 已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B 事务回滚,A 读取的数据变得无效,属于脏数据,A 事务后续的操作其实是在操作虚假数据!不符合事务的一致性要求 - 读到另一个事务没有commit的数据
- 不可重复读(NonRepeatable Read)
- 事务
A 内部的相同查询语句在不同时刻读出的结果不一致。一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。不符合事务的隔离性 - 幻读(Phantom Read)
- 事务
A 读取到了事务B 提交的新增数据,不符合隔离性 。一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。不符合事务的隔离性
? 这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离级别 、锁机制 、MVCC多版本并发控制隔离机制 ,用一整套机制来解决多事务并发问题
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|
READ UNCOMMITTED 读未提交(安全性低、效率高) | 可能 | 可能 | 可能 | READ COMMITTED 读已提交 | 不可能 | 可能 | 可能 | REPEATABLE READ 可重复读(默认) | 不可能 | 不可能 | 可能 | SERIALIZABLE 可串行化(安全性高、效率低) | 不可能 | 不可能 | 不可能 |
# 查看事务隔离级别
SELECT @@TRANSACTION_ISOLATION;
# 设置事务隔离级别
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }
案例
数据
create database user_test;
create table account(
id int auto_increment primary key,
name varchar(11),
money int);
insert into account values
(null, '张三', 2000),
(null, '李四', 2000);
脏读
# 设置事务为 读未提交
set session transaction isolation level read uncommitted;
# 开启事务1
start transaction;
# 开启事务2
start transaction;
# 在事务1中查询表信息
select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | 张三 | 2000 |
| 2 | 李四 | 2000 |
+----+------+-------+
# 在事务2中更改数据,但未提交事务
update account set money = money-1000 where name='张三';
# 在事务1中查询表信息
select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 2000 |
+----+------+-------+
-- 这就出现了脏读 {一个事务读到另一个事务还没有提交的数据}
-- 然后在事务2 rollback ,也会应该到事务1的查询结果
-- 也就是说,另一个事务做什么更新,这个事务都会读取到
-- 解决方案:提升事务隔离在 read committed 以上
-- 解决脏读后的结果:
-- ------只有事务2提交事务后,事务1才能读取到所做的操作才是对的
不可重复读
# 设置事务为 读已提交
set session transaction isolation level read committed;
# 开启事务1
start transaction;
# 开启事务2
start transaction;
# 在事务1中查询表信息
select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | 张三 | 2000 |
| 2 | 李四 | 2000 |
+----+------+-------+
# 在事务2中更改数据,但未提交事务
update account set money = money-1000 where name='张三';
# 在事务1中查询表信息
select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | 张三 | 2000 |
| 2 | 李四 | 2000 |
+----+------+-------+
-- 虽然这里解决了脏读,但是。在事务2提交后
commit;
# 在事务1中查询
select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 2000 |
+----+------+-------+
-- 可以正确的读取到事务2更新提交事务后的结果
-- 但是问题是,在同一事务1中,查询的结果确实不一样的,这就是不可重复读
-- {一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读}
-- 解决 提升事务隔离级别为 repeatable read
幻读
# 设置事务为 可重复读
set session transaction isolation level repeatable read;
# 开启事务1
start transaction;
# 开启事务2
start transaction;
# 在事务2中插入一条数据并提交
insert into account(id, name, money) values (3, '王五', 2000);
commit;
# 由于解决了不可重复读的问题,所以在事务1中查询id为3的数据将查不到
select * from account where id = 3;
Empty set (0.34 sec)
# 但是在 事务1 插入同样的数据时就不能插入
insert into account(id, name, money) values (3, '王五00', 2000);
ERROR 1062 (23000): Duplicate entry '3' for key 'PRIMARY'
-- 这就是幻读,{一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了"幻影"}
序列化
# 设置事务为 序列化
set session transaction isolation level serializable;
# 开启事务1
start transaction;
# 开启事务2
start transaction;
# 在事务2中插入一条数据
insert into account(id, name, money) values (4, '赵六', 2000); ## 可以执行,因为事务1中没有正在操作数据
# 在事务1中查询数据,并且为提交事务时,在事务2中插入数据
select * from account where id = 5;
# 在事务2中插入一条数据
insert into account(id, name, money) values (7, '钱七', 2000);
-- 这时,事务2会阻塞住,因为是串行
-- 只有提交了事务1后,事务而才能执行
锁机制
mysql 中的数据也是一种共享的资源,当并发访问时可能会出现数据一致性问题,所以mysql 使用一些锁机制去应对,但锁机制也是影响数据库并发访问性能的一个重要因素。
锁分类
-
悲观锁 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 悲观锁认为当前环境并发量非常大,为了在高并发情况下保证数据一致性,每次操作数据时需要进行加锁。保证安全的同时降低效率!mysql的for update 就是一个悲观锁
- 应用:使用
synchronized 、Lock ,来处理高并发下产生线程不安全问题,这样会使其他线程进行挂起等待,从而影响系统吞吐量 - 悲观锁发生并发冲突,其他线程被挂起等待!
-
乐观锁 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。 乐观锁适用于多读的应用类型,这样可以提高吞吐量。 乐观锁i认为当前环境并发较少,或者很难出现并发,这时如果也使用synchronized 、Lock 等悲观锁去处理,为了偶尔的并发降低每一次请求的效率,显然有些得不偿失。乐观锁采用版本机制对比, 如果有冲突,返回给用户错误的信息。仅返回错误信息相比于悲观锁的用户态和内核态的切换来讲是很快的!
- 应用:无锁
CAS ,mybatis_plus 的乐观锁:@version 注解标记比较字段。mysql 更新时比对版版本号update ... where version = 1 ,如果比对成功才能修改,否则提示错误 - 乐观锁发生并发冲突,返回错误信息或者自旋!与悲观锁的区别就是这点:宁愿返回错误也不要挂起其他线程!
-
读锁 也叫共享锁,读锁会阻塞其他session 的写,但是不会阻塞其他session 的读,属于悲观锁 -
写锁 也叫排它锁,写锁则会把其他session 的读和写都阻塞。属于悲观锁
MyISAM 在执行查询语句SELECT 前,会自动给涉及的所有表加读锁,在执行update、insert、delete 操作会自动给涉及的表加写锁。InnoDB 在执行查询语句SELECT 时(非串行隔离级别),不会加锁。但是update、insert、delete 操作会加行锁。 -
表锁 每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。 -
行锁 行锁包括了共享锁、排他锁、间隙锁、记录锁、临键锁.,每次操作锁住一行数据。开销大,加锁慢;会出现死锁(事务修改完一行数据不提交,又去修改别的数据,两个事务相互持有锁不释放导致死锁);锁定粒度最小,发生锁冲突的概率最低,并发度最高。InnoDB支持行级锁,MYISAM不支持。 InnoDB是针对索引加的锁,不是针对记录加的锁。for update 就是mysql的行锁!表示锁住某一行,不允许其他事务读写数据。但for update 如果使用不当会升级成表锁
- 使用
for update 查询非索引字段锁会升级为表锁。例如:wherer id = 1 for update ,id 为索引,此时为行锁;wherer name = aaa for update ,name 不是索引,此时会升级成表锁,锁住整张表,导致其他sql都操作不了数据库! - 因为
InnoDB 的更新会默认加行锁,所以如果对非索引字段更新,行锁也会变表锁。例如:session1 执行update account set balance = 800 where name = 'lilei'; ,session2 对该表任一行操作都会阻塞住,并且该索引不能失效,否则都会从行锁升级为表锁 - 范围查询有时会升级为表锁。范围查询会锁上命中的所有间隙,
for update 也会升级为表锁 - 查全表也会导致
for update 升级为表锁,例如:select * from user for update -
间隙锁 (Gap Locks) 锁的就是两个值之间的空隙,在可重复读隔离级别下才会生效。间隙锁在某些情况下可以解决幻读问题。 假设account表里数据如下
- 那么间隙就有 id 为
(3,10) ,(10,20) ,(20,正无穷) 这三个区间 - 在事务A下面执行
update account set name = 'zhuge' where id > 8 and id <18; ,不提交事务 - 由于
id > 8 and id <18 处于(3,10) ,(10,20) 这两个区间内,那么由于事务A没有提交,mysql会使用间隙锁锁住(3,20] 这个区间内的所有数据。其他事务无法在(3,20] 这个区间内插入或修改任何数据。注意最后那个20 也是包含在内的,这就是间隙锁! -
临键锁(Next-Key Locks):是行锁与间隙锁的组合。像上面那个例子里的这个(3,20) 的这个区间是开区间,理论上其他事务操作id = 20 的数据是可以操作的,由于临键锁的存在,id = 20 的数据也变为不可操作。临键锁就是把(3,20) 的开区间变为闭区间(3,20] ;
|