Mysql事务和锁原理分析

本文详细介绍了MySQL中的事务特性,包括原子性、一致性、隔离性和持久性,并深入探讨了InnoDB存储引擎的锁机制,如表级锁、行级锁、共享锁、排他锁、意向锁、间隙锁和临键锁。解释了当前读和快照读的概念,以及如何通过MVCC解决脏读、不可重复读和幻读问题。此外,文章还讨论了死锁的产生和解决策略,以及MySQL在不同隔离级别下的行为。最后,分析了MySQL如何通过锁和MVCC来实现事务的隔离性,确保数据的一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是事务

事务,即一组数据库操作的集合,这组操作要么全部执行成功,任意一个操作失败那么所有操作全部回滚。

事务的特性

事务有四大特性,即原子性,一致性,隔离性,持久性。这四个特性通常称为ACID特性。

  1. 原子性(Atomicity):一个事务是一个不可分割的单位。一个事务中的操作要么全部执行,要么全不执行。
    例如一个账户A向另一个账户B转账,A账户的余额减少,B账户的余额就要增加,两个操作一定同时成功或者同时失败。

  2. 一致性(Consistency):事务使数据由一个状态变为另一个状态,数据的完整性保持稳定。
    例如转账操作中,A账户转账给B账户,A账户减少的金额与B账户增加的金额必须是相同的。

  3. 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务的内部操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
    事务互相干扰就会造成数据不一致,隔离性的最终目的是为了保证一致性。

  4. 持久性(Durability):一个事务一旦提交,它对数据库中的数据的改变应是永久的。
    只要事务提交成功,对数据库的修改就保存下来了,不会因为任何原因再回到修改前的状态。

https://zhuanlan.zhihu.com/p/279775508

锁?

为了实现读-读操作不受影响,写-写操作、读-写操作能够互相阻塞,MySQL使用了读写锁的思想,实现了共享锁与排他锁:
共享锁(并发读,读写互斥)阻塞其他事务修改,但是多个事务可以共享一把读锁。
排它锁(只能被一个事务持有)

  • 共享锁(S锁):
    用于不更改或不更新数据的操作,如SELECT语句。共享锁可以在同一时刻被多个事务持有。获得共享锁的事务只能读取数据,不能更改数据。我们可以通过SELECT … LOCK IN SHARE MODE手工加共享锁。需要注意的是如果一个事务对数据加上了共享锁,其他事务只能对这部分数据再加共享锁,不能加排它锁。

  • 排他锁(X锁):
    用于修改数据操作,如INSERT、UPDATE、DELETE。确保事务不会同时对同一部分数据进行多重修改,在同一时刻只能被一个事务持有。排他锁的加锁方式有两种,第一种是自动加锁,在对数据进行增删改时都会默认加上一个排他锁。另一种是手工加锁,使用SELECT … FOR UPDATE可以实现手工加排他锁。

  • 意向锁
    考虑一个场景,事务t1给某行数据加行级共享锁,让该行数据只能读不能写,之后事务t2申请表级排他锁,让整张表的数据只能写不能读。如果事务t2的锁申请成功,那么它可以修改表里的任意一行数据,这与t1持有的行锁是冲突的。那么数据库要如何判断这个冲突呢?
    首先需要判断表是否已被其他事务加了表锁;然后需要判断表中每一行是否加了行锁。这种判断方式需要遍历表中的每一行,效率低下。于是有了意向锁除(I锁)。

    意向锁可以认为是S锁和X锁在数据表上的标识,通过意向锁可以快速判断表中是否有记录被上锁,从而避免通过遍历的方式来查看表中有没有记录被上锁,提升加锁效率。意向锁是由数据库自己维护的。当我们给一行数据加上共享锁之前,数据库会自动先申请表的意向共享锁(IS锁);当我们给一行数据加上排他锁之前,数据库会自动先申请表的意向排他锁(IX锁)。例如,我们要加表级别的X锁,首先判断表上是否有被其他事务加了表锁,如果没有,再检查是否有意向锁,此时直接根据意向锁就能知道这张表是否有行级别的X锁或者S锁,这时候数据表里面如果存在行级别的X锁或者S锁的,加锁就会失败。

InnoDB中的锁

1.表级锁

InnoDB中的表级锁主要包括表级别的意向共享锁(IS锁),意向排他锁(IX锁)以及自增锁(AUTO-INC锁)。IX锁和IS锁已经介绍过了,下面重点介绍一下自增锁。
自增锁是特殊的表锁,用来防止自增字段重复,数据插入以后就会释放,不需要等到数据提交才释放。

2.行级锁(或者叫行锁算法:记录锁、间隙锁、临建锁)

在了解InnoDB的行级锁之前,我们先简单了解一下当前读和快照读。

什么是当前读,什么是快照读(MVCC)

https://blog.youkuaiyun.com/mingtiannihaoabc/article/details/107018110

1)当前读:即加锁读。读取记录的最新版本,会加锁保证其他并发事务不能修改当前记录,直至获取锁的事务释放锁。使用当前读的操作主要包括:显示加锁的读操作与插入、更新、删除等写操作。
2)快照读:即不加锁读。读取记录的快照版本而非最新版本,通过MVCC实现。InooDB在可重复读隔离级别下,如果不显示的加LOCK IN SHARE MODE、FOR UPDATE的SELECT操作都属于快照读,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见。
innodb默认隔离级别是RR, 是通过MVVC来实现了,读方式有两种,执行select的时候是快照读,其余是当前读(比如一个事务插入数据了,另一个事务里面虽然不能select ,但是可以update),所以,mvvc不能根本上解决幻读的情况

综上可知,通过MVCC可以解决脏读、不可重复读、幻读这些读一致性问题,但是这只是解决了普通SELECTD的数据读取问题,即快照读的读取问题。在当前读,即加锁读的情况下依然要解决脏读、不可重复读、幻读问题。这个时候需要在读取的记录上加锁,由于都是在行记录上加锁,这些锁都称为行级锁。
InnoDB的行锁是通过锁住索引来实现的,如果加锁查询时没有使用索引,会进行全表扫描,将整个表的聚簇索引锁住,相当于锁住整个表。

InnoDB的行级锁定同样分为两种类型,共享锁和排他锁

共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE

InnoDB行锁实现方式

InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表
  在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。
  (1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
  (2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
  (3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
  (4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

间隙锁(Next-Key锁)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;
  对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
  例:
  假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:

mysql> select * from emp where empid > 100 for update;

是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
  InnoDB使用间隙锁的目的:
  (1)防止幻读,以满足相关隔离级别的要求。对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;
  (2)为了满足其恢复和复制的需要。

很显然,在使用范围条件检索并锁定记录时,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。
  除了间隙锁给InnoDB带来性能的负面影响之外,通过索引实现锁定的方式还存在其他几个较大的性能隐患
  (1)无法利用索引时候,会使用表级锁,造成并发性能的降低;
  (2)当Query使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所只想的数据可能有部分并不属于该Query的结果集的行列,但是也会被锁定,因为间隙锁锁定的是一个范围,而不是具体的索引键;
  (3)当Query在使用索引定位数据的时候,如果使用的索引键一样但访问的数据行不同的时候(索引只是过滤条件的一部分),一样会被锁定。
因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件
还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁

根据锁定范围不同,行锁可分为:

  • 记录锁(Record Lock):唯一性索引等值查询,精准匹配到一条记录。
  • 间隙锁(Gap Lock):查询记录不存在,锁定一个范围。
  • 临键锁(Next-Key Lock):使用范围查询,不仅匹配到了记录,而且还命中了间隙,会锁定一个范围,是记录锁和间隙锁的结合,是MySQL的默认行锁。

间隙锁和临键锁都是用来解决幻读的
锁定范围

  • 对主键索引或唯一索引来说,当锁定一条记录时,会产生记录锁;当锁定一个区间时,会产生间隙锁和记录锁,即临键锁。
  • 对普通索引来说,会产生临键锁。
  • 对无索引列来说,会锁住整张数据表。
  • 记录锁(行级锁):对于唯一性索引(包括唯一索引和主键)使用等值查询的时候,精准匹配到一条记录的时候,这个时候使用的就是记录锁。
  • 间隙锁(行级锁):当我们查询的记录不存在,没有命中任何一条记录的时候,使用的是间隙锁。间隙锁阻塞的是insert,相同的间隙锁之间不冲突。(防止幻读)
  • 临键锁:当我们使用了范围查询,不仅命中了record记录,而且还包含了GAP间隙,在这种情况下我们使用的是临键锁,他是mysql里面默认的行锁算法,相当于记录锁加上间隙锁。
    唯一性索引,等值查询匹配到一条记录的时候,会退化成记录所锁 没有匹配到任何数据的时候,退化成间隙锁

什么时候使用表锁

对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁。
第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
当然,应用中这两种事务不能太多,否则,就应该考虑使用MyISAM表。

死锁

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。死锁示例

事务一和事务二同时执行完第一个update语句,接着准备执行第二条update语句,却发现记录已被对方锁定,然后2个事务都等待对方释放资源,同时持有对方需要的锁,这样就会出现死循环。

为了避免死锁问题,数据库实现了各种死锁检测和死锁超长机制,InnoDB处理死锁的方式是:将持有最少行级排他锁的事务进行回滚

死锁的避免:

  1. 程序中操作多张表时候,尽量以相同的顺序来访问
  2. 使用等值查询而不是范围查询,命中纪录,避免间隙锁对并发的影响
  3. 尽量使用索引访问数据,避免锁表
  4. 批量操作单张表的数据时,先对数据进行排序(避免环形等待)
  5. 如何可以,将大事务分解成小事务

mysql 如何解决脏读 (read commit)

修改时加排他锁(写锁),直到事务提交后才释放,读取时加共享锁(读锁),其他事务只能读取,不能再有更新操作 。防止脏读。
答:普通的select 都是使用快照读,使用MVCC实现
加锁的select 都是使用记录锁,因为没有间隙锁,所以会出现幻读问题。

mysql如何实现可重复读的?(repeatable read)

innodb引擎采用了mvcc(多版本并发控制)来解决不可重复读问题。mvcc是利用在每条数据后面加了隐藏的两列(创建版本号和删除版本号)当执行查询的时, 当前查询版本号>= 创建版本号 并且 >删除版本号 , MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销。

可重复读是InnoDB的默认事务隔离级别,且已能够达到了SQL标准的可串行化

答: 普通的select 使用的MVCC多版本并发控制来实现
加锁的读,比如for update \ in share mode (排它锁\共享锁)使用的是当前读,底层使用的是记录锁、间隙锁、临界锁

MySQL是如何解决幻读的?(serializable )

  1. 多版本并发控制(MVCC)(快照读)
    原理:将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的。
  2. 采用next-key锁的方式解决问题
    next-key 锁包含两部分:记录锁(行锁)+间隙锁
    记录锁是加在索引上的锁,间隙锁是加在索引之间的。
    原理:将当前数据行与上一条数据和下一条数据之间的间隙锁定,保证此范围内读取的数据是一致的

在Seriable隔离级别下,mysql的读要加共享锁阻止其他人的修改,读和写不能并发。mysql利用间隙锁来防止幻读。

在快照读读情况下,mysql通过mvcc来避免幻读。
在当前读读情况下,mysql通过next-key来避免幻读。
select * from t where a=1;属于快照读
select * from t where a=1 lock in share mode;属于当前读

mysql默认隔离级别以及原因

默认隔离级别是RR (repeatable read)

通过上述的实践,可以发现在RR级别下,binlog为任何格式均不会造成主从数据不一致的情况出现,但是当低版本MySQL使用RC+STATEMENT组合时(MySQL5.1.5前只有statement格式)将会导致主从数据不一致。当前这个历史遗漏问题以及解决,大家可以将其设置为RC+ROW组合的方式(例如ORACLE等数据库隔离级别就是RC),而不是必须使用RR(会带来更多的锁等待),具体可以视情况选择。

事务的特性原子性、隔离性、持久性,是通过什么技术实现的?

  • 原子性:当事务需要回滚的时候,通过undo log实现
  • 隔离性:
    对于普通的select 是通过使用快照实现,底层使用的MVCC 机制(多版本并发控制)
    对于加锁的select …in share mode / select … for update 以及更新操作,使用的当前读,底层使用的记录所、间隙锁、临界锁
  • 持久性: 当出现落盘失败或者异常的情况,通过redo log和双写缓冲(double write buffer)实现

innodb 实现了哪些事务隔离级别,每种隔离级别带来的问题是什么?

四种隔离级别。。

锁的原理?

InnoDB的行锁是通过锁住索引来实现的,如果加锁查询时没有使用索引,会进行全表扫描,将整个表的聚簇索引锁住,相当于锁住整个表。**
根据锁定范围不同,行锁可分为:

  • 记录锁(Record Lock):唯一性索引等值查询,精准匹配到一条记录。
  • 间隙锁(Gap Lock):查询记录不存在,锁定一个范围。
  • 临键锁(Next-Key Lock):使用范围查询,不仅匹配到了记录,而且还命中了间隙,会锁定一个范围,是记录锁和间隙锁的结合,是MySQL的默认行锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EmineWang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值