MySQL之事务和锁机制
Posted 一只咸鱼。。
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL之事务和锁机制相关的知识,希望对你有一定的参考价值。
文章目录
提示:以下是本篇文章正文内容,mysql 系列学习将会持续更新
一、事务
- 在数据库里面,我们希望有些操作能够以原子的方式进行,要么都能执行成功,要么就都不执行,也就是只能是一个整体的被执行,这样的一组具有原子性的操作我们就称之为事务。
- 我们的 MySQL 支持 9 种数据库引擎,但只有默认的
Innodb
引擎支持事务功能。
1.1 事务特征
- 原子性 (atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
- 一致性 (consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
- 隔离性 (isolation):当多个事务对同一资源同时操作时,一个事务的执行不能被其他事务干扰。这里的同时只是宏观上的表现,实际上也就是微观上同一时刻只有一个事务在执行,而其它事务是在等待中。
- 持久性 (durability):指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,即便系统故障也不会丢失。
1.2 隔离级别
更追求隔离性(数据更正确) | ----------------------------- | ------------------------------------------------ | ---------------------------------> | 更追求并发性(性能更高) |
---|---|---|---|---|
(可串行性)serializable | (快照读)snapshot_read | (可重复读)repeatable_read | (读已提交)read_committed | (读未提交)read_uncommitted |
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。 | 不是标准中存在的隔离级别,目前来说,没有副作用。 MySQL中的可重复读就是实际上的快照读。因为MVCC机制解决了幻读。 | 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。这会导致幻读:当用户修改某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有一条未修改的数据“幻影” 。 | 只能读取其它事务已经提交的内容,存在不可重复读问题:一个事务多次读取同一数据可能会得到多个不同的结果 。 | 能够读取到其它事务中未提交的内容,存在脏读问题。读取未提交的数据,也被称之为脏读 。 |
我们可以修改隔离级别:
set session transaction isolation level read uncommitted;
1.3 开启事务
①SQL开启事务
-- 开启事务
start transaction; / begin;
SQL1;
SQL2;
rollback; -- 主动回滚
-- 开启事务
start transaction; / begin;
SQL1;
SQL2;
SQL3;
commit; -- 提交事务,失败也会回滚
②JDBC使用事务
// 要使用事务,在同一个事务中,操作 sql1 和 sql2,意味着必须在一条 Connection 完成
try (Connection c = DBUtil.connection())
// connection 中有一个自动提交(autocommit)的属性,默认情况下是 true(开启)
// 开启状态下,意味着,每一条 sql 都会被独立的视为一个事务
// 我们要让 sql1 和 sql2 看作整体,只需要关闭 connection 的自动提交
c.setAutoCommit(false);
// 此时就可以手动的控制事务的结束位置,并且需要手动提交
try (PreparedStatement ps = c.prepareStatement(sql1))
ps.executeUpdate();
try (PreparedStatement ps = c.prepareStatement(sql2))
ps.executeUpdate();
// 由于我们关闭了自动提交了,所以,所有的修改还没有真正地落盘
c.commit(); // 只有加上这句话,才表示事务被提交了(数据真正落盘了)
二、锁机制
我们知道在可重复读的级别下,MySQL 在一定程度上解决了幻读问题:
- 在快照读(不加锁)读情况下,mysql 通过 MVCC (多版本并发控制) 来避免幻读。
- 在当前读(加锁)读情况下,mysql 通过 next-key 来避免幻读。
2.1 读锁、写锁
从对数据的操作类型上来说,锁分为读锁和写锁:
- 读锁:也叫共享锁,当一个事务添加了读锁后,其他的事务也可以添加读锁或是读取数据,但是不能进行写操作,只能等到所有的读锁全部释放。
- 写锁:也叫排他锁,当一个事务添加了写锁后,其他事务不能读不能写也不能添加任何锁,只能等待当前事务释放锁。
2.2 全局锁、表锁、行锁
从锁的作用范围上划分,分为全局锁、表锁和行锁:
①全局锁:锁作用于全局,整个数据库的所有操作全部受到锁限制。
flush tables with read lock;
②表锁:锁作用于整个表,所有对表的操作都会收到锁限制。
lock table 表名称 read; -- 读锁
lock table 表名称 write; -- 写锁
-- 除了手动释放锁之外,当我们的会话结束后,锁也会被自动释放。
unlock tables;
③行锁:锁作用于表中的某一行,只会通过锁限制对某一行的操作(仅InnoDB支持)
-- 添加读锁(共享锁)
select * from 表名 where ... lock in share mode;
-- 添加写锁(排他锁)
select * from 表名 where ... for update;
2.3 记录锁、间隙锁、临键锁
我们知道 InnoDB 支持使用行锁,但是行锁比较复杂,它可以继续分为多个类型,详细可查看文章:MySQL的锁机制 - 记录锁、间隙锁、临键锁
①记录锁(Record Locks): 仅仅锁住索引记录的一行,在单条索引记录上加锁。Record lock 锁住的永远是索引,而非记录本身。所以说当一条 sql 没有走任何索引时,那么将会在每一条聚合索引后面加写锁,这个类似于表锁,但原理上和表锁应该是完全不同的。
- id 列必须为唯一索引列或主键列,否则加的锁就会变成临键锁。
- 同时,查询语句必须为精准匹配(=),不能为 >、<、like等,否则也会退化成临键锁。
②间隙锁(Gap Locks): 仅仅锁住一个索引区间(开区间)。在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。比如在 1、2 中,间隙锁的可能值有 (-∞, 1),(1, 2),(2, +∞),间隙锁可用于防止幻读,保证索引间的不会被插入数据。
- 对于主键索引:精准查询存在列,只会产生记录锁;精准查询不存在列,会产生记录锁和间隙锁;范围查询会产生间隙锁。
- 对于普通索引:不管是何种查询,只要加锁,都会产生间隙锁。
③临键锁(Next-Key Locks): Record lock + Gap lock
,左开右闭区间。默认情况下,InnoDB 正是使用 Next-key Locks 来锁定记录(如select … for update
)。
它还会根据场景进行灵活变换:
场景 | 转换 |
---|---|
使用唯一索引进行精确匹配,但表中不存在记录 | 自动转换为 Gap Locks |
使用唯一索引进行精确匹配,且表中存在记录 | 自动转换为 Record Locks |
使用非唯一索引进行精确匹配 | 不转换 |
使用唯一索引进行范围匹配 | 不转换,但是只锁上界,不锁下界 |
总结:
提示:这里对文章进行总结:
本文是MySQL的学习,先学习了事务的四大特征、隔离级别,如何开启事务。又学习了锁机制,认识了读写锁、行表锁、记录锁等。之后的学习内容将持续更新!!!
深入了解MySQL的隔离级别和锁机制
简述:
我们的MySQL一般会并发的执行多个事务,多个事务可能会并发的对同一条或者同一批数据进行crud操作;可能就会导致我们平常所说的脏读、不可重复读、幻读这些问题.
这些问题的本质都是MySQL多事务并发问题,为了解决多事务并发问题,MySQL设计了锁机制、MVCC多版本并发控制隔离机制、以及事务隔离机制,用一整套机制来解决多事务并发所出现的问题.
1. 事务的四大特性
特性 | 特点 |
---|---|
Atomicity(原子性) | 事务是不可分割的,其对数据的修改,要么全都执行,要么全都不执行 |
Consistency(一致性) | 在事务提交的前后的状态和数据都必须是一致的 |
Isolation(隔离性) | 在多事务并发时,保证事务不受并发操作影响的"独立"环境执行,这就意味着事务处理过程中的中间状态对外部是不可见的,反之亦然 |
Druability(持久性) | 指事务一旦提交,数据就持久化保存到磁盘中不会丢失 |
2.多事务并发带来的问题
问题 | 现象 | 描述 |
---|---|---|
脏读 | A事务正在对一条记录做修改,在A事务完成并提交前,这条记录的数据就处于不一致的状态(有可能回滚也有可能提交),与此同时,B事务也来读取同一条记录,如果不加控制,B事务读取了这些"脏"数据,并据此作进一步处理,就会产生未提交的数据以来关系 | 一个事务中读取到另一个事务尚未提交的数据,不符合一致性要求 |
不可重复读 | 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变或某些记录已经被删除了 | 一个事务中多次读取的数据不一致,原因是收到其他事务已提交update的干扰,不符合隔离性 |
幻读 | 一个事务按相同的查询条件重新读取以前查询过的数据,却发现其他事务插入满足其查询条件的新数据 | 一个事务中多次读取的数据不一致,原因是受其他事务已提交insert/delete的干扰,不符合隔离性 |
3.事务的隔离级别
脏读、不可重复读和幻读,其实都是MySQL读一致性问题,必须由数据库提供一定的事务隔离机制来解决.
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted(读未提交) | √ | √ | √ |
Read committed(读已提交) | × | √ | √ |
Repetatble read(可重复读)(MySQL默认) | × | × | √ |
Serializable(串行化) | × | × | × |
查看当前数据库的事务隔离级别:show variables like ‘tx_isolation’;
设置事务隔离级别:set tx_isolation='隔离级别’
4.演示不同隔离级别出现的问题
mysql版本:5.7.34
涉及表:
两个MySQL客户端
客户端A <===================> 客户端B(下面每张图片两个客户端皆以第一张图命名为准
-
读未提交
1.1 设置事务隔离级别set tx_isolation=‘read-uncommitted’;
1.2 客户端A和客户端B各开启一个事务,
1.3 客户端A只做查询,客户端B对id = 1的记录做修改;
1.4 再两个事务都未提交的情况下,事务A读到了事务B修改后的数据
1.5 一旦客户端B的事务因为某种原因rollback,那么客户端A查询到的数据其实就是脏数据,不符合一致性的要求 -
读已提交
2.1 设置隔离级别读已提交:set tx_isolation=‘read-committed’;
2.2 客户端A和客户端B各开启一个事务,
2.3 客户端A只做查询,客户端B对id = 1的记录做修改;
2.4 客户端B未提交事务时,客户端A不能查询客户端B未提交的数据,解决了脏读的问题
2.5 当客户端B提交事务后,客户端A再次对表进行查询,结果与上一步不一致,即产生了不可重复读的问题,不符合隔离性
3. 可重复读
3.1 设置隔离级别可重复读:set tx_isolation=‘repeatable-read’;
3.2 客户端A和客户端B各开启一个事务,
3.3 客户端B修改表中数据然后提交;
3.4 客户端A查询表中数据,并未出现与上一步不一致的问题,解决了不可重复读的问题
3.5 在客户端A中执行update account set balance = balance - 100 where id = 1;blance并未有变成800-100=700;而是使用客户端B提交后的数据来算的,所以是600;数据的一致性并没有被破坏;可重复读的隔离级别下使用的是MVCC机制,select操作不会更新版本号,是快照读(历史版本),保证同一事务下的可重复读;insert/update/delete会更新版本号,是当前读(当前版本)保证数据的一致性
3.6 客户端B重新开启一个事务插入一条数据后提交
3.7 在客户端A中重新查询表数据,并没有出现客户端B刚才新增的数据,没有出现幻读
3.8 验证幻读:在客户端A中,对id = 4 的数据做修改;可以更新成功;再次进行查询就能查询出客户端B新增的数据,出现幻读问题,不符合隔离性
4. 串行化
4.1 设置隔离级别串行化:set tx_isolation=‘serializable’;
4.2 客户端A和客户端B各开启一个事务,
4.3 客户端A先查询表中id = 1的数据
4.4 在客户端A事务未提交时,客户端B对表中id = 1 的数据做更新;由于客户端A的事务并没有提交,客户端B的更新动作将会阻塞至到客户端A提交事务或者超时,超时SQL报错:Lock wait timeout exceeded; try restarting transaction
4.5 在客户端B中更新id = 2 的数据却可以成功,说明在串行化的隔离级别下,innodb的查询也会被加上行锁;
4.6 如果客户端A执行的是一个范围查询,那么该范围内的所有行包括每行记录所在的间隙区间范围(就算该行未被插入也会加锁,这种是间隙锁)都会被加锁,此时如果客户端B对该范围内的数据做任何操作都会被阻塞;所以就避免了幻读;
4.7 串行化这种隔离级别并发性极低,所以再真实的开发很少会遇到,这也是MySQL为什么使用可重复读作为默认的隔离级别的重要原因
5.锁机制
MySQL默认的隔离级别是可重复读,可是还是会出现幻读问题;间隙锁再某种情况下可以解决幻读问题;
-
间隙锁
概述:间隙锁,锁的就是两个值之间的空隙.
假设表中数据如下:
那么间隙就有(4,10)、(10,15)和(15,正无穷)三个间隙;
1.1 设置隔离级别可重复读:set tx_isolation=‘repeatable-read’;
1.2 客户端A和客户端B各开启一个事务,
1.3 在客户端A执行update account set balance = 1000 where id > 5 and id < 13 ;
1.4 在客户端A未提交的时候,客户端B是没有办法对这个范围包含的所有行记录(包括间隙行记录)以及行记录所在间隙里执行insert/update操作,即4<id<=15这个区间内都无法修改数据,id = 15 同样不能修改;
1.5 间隙锁只有在可重复读的隔离级别下才会生效 -
临建锁
概述:临建锁是行锁和间隙锁的结合,想上面那个4<id<=15就属于临建锁; -
无索引行锁会升级成为表锁
3.1 客户端A和客户端B各开启一个事务,
3.2 在客户端A执行update account set balance = 1000 where name = ‘李四’;
3.3 在客户端A未提交的时候,客户端B执行update account set balance = 800 where id = 15 ;同样会被阻塞至客户端A提交或者超时;
3.4 MySQL中的锁主要是加载索引字段上,如果使用再非索引字段上,行锁会升级成表锁; -
排他锁
4.1 客户端A和客户端B各开启一个事务,
4.2 在客户端A执行select * from account where id = 1 for update ;
4.3 在客户端A未提交的时候,客户端B执行update account set balance = 800 where id = 1 ;会被阻塞至客户端A提交或者超时;
结论:Innodb引擎实现了行锁,虽然行锁机制实现方面所带来的性能损耗可能比表级锁定会更高,但是再整体并发处理能力肯定要强于表级锁;当系统并发量高的时候,行级锁和表级锁相比就会有比较明显的优势;但是行级锁使用起来也比表级锁复杂,当我们使用不当的时候,可能会使行锁的性能不仅不比表级锁的性能高,甚至可能会更差.
为什么行锁锁定的粒度小,开销反而会比表级锁的开销大?
- 因为表级锁只需要找到当前表就可以进行加锁,行锁的话需要对表中记录进行扫描,直至扫描到需要加锁的行才可以进行加锁,所以行锁的开销是比表级锁的开销要来得大的.
真实开发情况下对锁优化的一些建议:
- 合理使用索引字段加锁,缩小锁的范围
- 尽可能让所有锁都加到索引字段上,避免无索引行锁升级成表锁
- 尽可能减少查询范围,避免间隙过大的间隙锁
- 尽可能低级别事务隔离
- 尽可能控制事务大小,减少锁定资源量,涉及事务加锁的sql尽量放在事务最后执行,减少加锁的时间
以上是关于MySQL之事务和锁机制的主要内容,如果未能解决你的问题,请参考以下文章