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.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. 读已提交

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之事务和锁机制的主要内容,如果未能解决你的问题,请参考以下文章

深入了解MySQL的隔离级别和锁机制

RDB | MySQL的事务隔离级别和锁的机制

工作四年都没搞定的Mysql事务和锁机制,这篇一次讲清!

如何优雅地回答 MySQL 的事务隔离级别和锁的机制?

高频 | MySQL 事务隔离级别和锁的机制可以这么答

深入浅出MySQL事务处理和锁机制