MySQL 四种事务隔离级别 + 锁
一、事务的基本要素(ACID)
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
二、事务的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读(修改):事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。【需要行锁就可以解决】
3、幻读(新增/删除):系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。【需要表锁才能解决】
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
三、MySQL事务隔离级别,其实很好记,其实就是读的范围不同而已,看后面的标注就很好记了
1.可以读到未提交的数据
2.可以读到已提交的数据
3.可以重复的读同一条数据没问题
4.串行化
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(read-uncommitted)可以读到未提交的数据 | 是 | 是 | 是 |
读已提交(read-committed)可以读到已提交的数据 | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
并发性能也是从上到下,越来越差,因为越来越严格
mysql默认的事务隔离级别为repeatable-read
show VARIABLES like '%ISOLATION%'
transaction_isolation REPEATABLE-READ
四、用例子说明各个隔离级别的情况
1、读未提交:
(1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
(4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:
(5)在客户端A执行更新语句update account set balance = balance - 50 where id =1,lilei的balance没有变成350,居然是400,是不是很奇怪,数据不一致啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别
2、读已提交
(1)打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的所有记录:
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
(4)客户端B的事务提交
(5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
3、可重复读
(1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交
(3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题
(4)在客户端A,接着执行update balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。
(5)重新打开客户端B,插入一条新数据后提交
(6)在客户端A查询表account的所有记录,没有 查出 新增数据,所以没有出现幻读
下面来一个更直观的例子:
- 准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,准备一张测试表 test 并调整隔离级别为
REPEATABLE READ
,任意一个终端执行即可。SET @@session.transaction_isolation = 'REPEATABLE-READ'; create database test; use test; create table test(id int primary key,name varchar(20));
- 登录 mysql 终端 1,开启一个事务。
begin; select * from test; -- 无记录
- 登录 mysql 终端 2,开启一个事务。
begin; select * from test; -- 无记录
- 切换到 mysql 终端 1,增加一条记录并提交。
insert into test(id,name) values(1,'a'); commit;
- 切换到 msyql 终端 2。
select * from test; --此时查询还是无记录
通过这一步可以证明,在该隔离级别下已经读取不到别的已提交的事务,如果想看到 mysql 终端 1 提交的事务,在 mysql 终端 2 将当前事务提交后再次查询就可以读取到 mysql 终端 1 提交的事务。我们接着实验,看看在该隔离级别下是否会存在别的问题。
- 此时接着在 mysql 终端 2 插入一条数据。
insert into test(id,name) values(1,'b'); -- 此时报主键冲突的错误
也许到这里您心里可能会有疑问,
明明在第 5 步没有数据,为什么在这里会报错呢?其实这就是该隔离级别下可能产生的问题,MySQL 称之为幻读。
注意我在这里强调的是 MySQL 数据库,Oracle 数据库对于幻读的定义可能有所不同
补充:
1、事务隔离级别为读提交时,写数据只会锁住相应的行
2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
3、事务隔离级别为串行化时,读写数据都会锁住整张表
4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
锁:[二者关系:锁是实现事务隔离级别的手段]
锁也是数据库管理系统区别文件系统的重要特征之一。
锁机制使得在对数据库进行并发访问时,可以保障数据的完整性和一致性。
对于锁的实现,各个数据库厂商的实现方法都会有所不同。
本文讨论 MySQL 中的 InnoDB 引擎的锁
锁的类型
InnoDB 实现了两种类型的行级锁:
-
共享锁(也称为 S 锁)【读锁】:允许事务读取一行数据。
可以使用 SQL 语句
select * from tableName where … lock in share mode;
手动加 S 锁。 -
独占锁(也称为 X 锁)【写锁】:允许事务删除或更新一行数据。
可以使用 SQL 语句
select * from tableName where … for update
; 手动加 X 锁。
行锁的算法
InnoDB 存储引擎使用三种行锁的算法用来满足相关事务隔离级别的要求。
-
Record Locks
该锁为索引记录上的锁,如果表中没有定义索引,InnoDB 会默认为该表创建一个隐藏的聚簇索引,并使用该索引锁定记录。
-
Gap Locks
该锁会锁定一个范围,但是不括记录本身。可以通过修改隔离级别为
READ COMMITTED
或者配置innodb_locks_unsafe_for_binlog
参数为ON
。 -
Next-key Locks
该锁就是 Record Locks 和 Gap Locks 的组合,即锁定一个范围并且锁定该记录本身。InnoDB 使用 Next-key Locks 解决幻读问题。需要注意的是,如果索引有唯一属性,则 InnnoDB 会自动将 Next-key Locks 降级为 Record Locks。举个例子,如果一个索引有 1, 3, 5 三个值,则该索引锁定的区间为
(-∞,1], (1,3], (3,5], (5,+ ∞)
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
InnoDB 引擎采取的是 wait-for graph
等待图的方法来自动检测死锁,如果发现死锁会自动回滚一个事务。
- 准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,分别登入 mysql,再准备一张测试表
test
写入两条测试数据,并调整隔离级别为SERIALIZABLE
,任意一个终端执行即可。SET @@session.transaction_isolation = 'REPEATABLE-READ'; create database test; use test; create table test(id int primary key); insert into test(id) values(1),(2);
- 登录 mysql 终端 1,开启一个事务,手动给 ID 为
1
的记录加 X 锁。begin; select * from test where id = 1 for update;
- 登录 mysql 终端 2,开启一个事务,手动给 ID 为
2
的记录加 X 锁。begin; select * from test where id = 2 for update;
- 切换到 mysql 终端 1,手动给 ID 为
2
的记录加 X 锁,此时会一直卡住,因为此时在等待第 3 步中 X 锁的释放,直到超时,超时时间由innodb_lock_wait_timeout
控制。select * from test where id = 2 for update;
此时,通过 show engine innodb status\G
命令可以看到 LATEST DETECTED DEADLOCK
相关信息,即表明有死锁发生;或者通过配置 innodb_print_all_deadlocks
(MySQL 5.6.2 版本开始提供)参数为 ON
将死锁相关信息打印到 MySQL 的错误日志。
锁的优化建议
锁如果利用不好,会给业务造成大量的卡顿现象,在了解了锁相关的一些知识点后,我们可以有意识的去避免锁带来的一些问题。
- 合理设计索引,让 InnoDB 在索引键上面加锁的时候尽可能准确,尽可能的缩小锁定范围,避免造成不必要的锁定而影响其他 Query 的执行。
- 尽可能减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定的记录。
- 尽量控制事务的大小,减少锁定的资源量和锁定时间长度。
- 在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少 MySQL 因为实现事务隔离级别所带来的附加成本。
上一篇: 浅谈fastjson的常用使用方法