渐进理解MySQL事务并发控制:锁,MVCC,乐观锁与悲观锁,死锁。

MySQL事务并发控制问题

MySQL中对数据进行并发操作的时候(即多个事务对同一些数据进行操作),会出现一些并发问题,这些并发问题简单总结如下:

读-写并发问题:
1. 脏读: 读取了其他事务中未提交的数据。在读的时候,其他事务进行了修改。
2. 不可重复读: 同一事务多次读取的结果不一致。两次读取之间其他事务对该数据进行了修改。
3. 幻读: 由于其他事务添加了数据,导致读取的结果和实际的结果不一致,像产生了幻觉一样。举例说就是:事务读取了该数据不存在,于是基于这个不存在的情况进行下一步操作,如添加数据,但有另一事务在你读取和添加之间已经添加了该数据。你的添加操作就会报错。因为可重复读隔离级别的设置,你再次读取该数据依旧不存在,但实际上是存在的。即另一事务修改(添加)了数据。

写-写并发问题:
1. 更新丢失问题: 即两个事务对同一数据进行修改,后进行的回滚和提交操作都会对先进行的操作造成影响,即操作回滚和操作覆盖。

详细的事务知识介绍,并发问题,隔离级别等可以看我的其他内容。
MySQL事务:将并发问题与事务隔离级别串起来理解。-CSDN博客

而解决上述并发问题就需要靠锁,MVCC,乐观锁和悲观锁。其中:

  • :并发控制的基础。
  • MVCC:解决读-写并发控制问题。
  • 乐观锁和悲观锁:解决写-写并发控制问题。

锁的介绍

  1. 什么是锁:计算机协调多个进程或线程并发访问某一资源的机制。
  2. 锁不是MySQL独有的设计,需要并发控制的都可以应用锁机制。如:
    1. 传统的计算资源:CPU,RAM,I/O等
    2. 数据库:解决事务并发控制的问题。(本文聚焦的问题)

锁的分类

读锁与写锁

在对数据进行并发控制的时候,事务之间读和写,写和写需要进行控制。控制的方法就是设置两种锁。

  • 读锁(共享锁):当为数据添加读锁后,其他的读锁也可以同时进行。但写锁被阻塞。
  • 写锁(排他锁):当为数据添加写锁后,其他的读锁和写锁都会被阻断。即进行写操作的时候,不允许其他读写操作。

对锁的粒度分类

确定了需要建立两种锁来进行并发控制,下一步就需要考虑为哪些数据添加锁。我们可以为以下数据加锁:
1. 数据表 -> 表级锁
2. 具体到数据表中的指定行记录。-> 行级锁
3. 数据表中的某一页范围加锁,加锁范围介于上述两者之间 -> 页面锁

采取哪一种方式需要考虑以下几点:
1. 开销:管理锁也是需要消耗资源的,比如,加锁,检测锁,释放锁等,都会增加系统的开销。而为数据表加锁,我们就要定位到表,然后加锁。而如果为表中的部分行加锁,就需要定位到表中的这些行,产生更大的开销,同时加锁速度也会变慢。 所以开销:表级锁 < 页面锁 < 行级锁。
2. 并发度:对数据表加锁,那么就不能再对数据表进行任何操作,即使两个事务之间不影响。而为行加锁,那么如果两个事务对不同行进行写操作,也是可以同时操作的。所以并发度: 行级锁 > 页面锁 > 表级锁。

锁的具体实现

MyISAM和INnoDB是数据库的两种不同引擎, MySQL默认使用的是InnoDB

MyISAM

  • MyISAM支持表锁,不支持页锁和行锁。
  • 对应的有两种表锁(与上述读锁和写锁定义相同):
    • 表共享读锁:允许其他事务对同一表的读请求,但会阻塞同一表的写请求。
    • 表独占写锁:独占,不允许读请求和写请求。
  • 写锁比读锁优先级高。当一个锁被释放后,会优先给写锁,然后再给读锁。

InnoDB

InnoDB锁分类

InnoDB 支持行锁和表锁。

  • 对应有两种行锁:
    • 共享锁(S) :给数据行加锁,允许其他读操作,不允许写操作。
    • 排它锁(X) :独占,不允许其他读写操作。
  • 对应有两种表锁(又称为意向锁,即有意向该表加行锁):
    • 意向共享锁(IS) :事务在给某一数据行加共享锁前必须先取得该表的意向共享锁。
    • 意向排他锁(IX) :事务在给一个数据行加排他锁前必须先取得该表的排他锁。

InnoDB默认使用行锁,行锁是加在索引上的,所以在未使用索引字段查询就无法使用行锁,变更为使用表锁。
注意:MySQL会根据语句的执行,决定是否使用索引,如果它认为全表扫描效率更高,就不会使用索引。这时候就仅仅使用表锁,不会使用行锁。

实现行锁的算法

简单介绍

InnoDB有三种行锁的算法:

  • 记录锁(Record Locks):单个行记录上的锁,对索引项加锁,锁定符合条件的行。
  • 间隙锁(Gap Locks):在索引记录之间的间隙上加锁。左开右开的区间。
  • 临键锁(Next-key Locks):记录锁和间隙锁的组合。左开右闭的区间。
举例说明

注:以下如果是查询操作,添加共享锁,如果是更新操作,添加排它锁。

  • 记录锁:
    • 只有当使用唯一索引操作单行记录时,加记录锁。
  • 间隙锁:
    • 当使用非唯一索引,或使用唯一索引操作多行结果或没有结果时,会加间隙锁。
    • 举例说明:通过索引id操作数据,id值分别是5,10,15,20,25
      • 条件为where id >9 and id<18,添加锁结果: (5,10),(10,15),(15,20)
      • 条件为 where id = 7, 因为7不存在,所以加(5,10)的锁。
  • 临键锁: 默认使用的。
    • 记录锁加间隙锁。
    • 也就是添加间隙锁的情况下,再加记录锁。左开右闭,
    • 举例说明:通过索引id操作数据,id值分别是5,10,15,20,25
      • 条件为where id >9 and id<18,添加锁结果: (5,10],(10,15],(15,20]
      • 条件为 where id = 7, 因为7不存在,所以加(5,10] 的锁。
临键锁+可重复读隔离机制解决幻读问题原理。

可重复读隔离机制下会出现幻读的问题。

举例复习一下幻读问题:A事务 先查询某一数据,显示不存在,然后想要添加相应的数据,这个时候报错了。再查询还是不存在,但就是添加不成功。跟幻觉一样,查不到,但就是添加不成功。原因是B事务在A事务第一次查询后添加了数据,但因为可重复读隔离级别,事务A是查不到的。

而加入临键锁后,查询的这一范围就被锁住了,事务B添加不了操作,于是幻读问题就解决了。

MVCC-解决读写并发问题

为什么要有MVCC

上述我们讲了MySQL的锁,通过锁的定义我们可以知道,如果只使用锁 读和写是不能并行的。而本节讲的MVCC便可以让数据库并发执行读和写,进而提高数据库并发性能。

定义:MVCC,全称为 Multi-Version Concurrency Control ,即多版本并发控制。这是一种用来解决读写冲突的无锁并发控制方法

MVCC从字面意思进行理解,就是其维护了一个数据的多个版本。在这种情况下,读和写操作是针对同一数据的不同版本的,所以其可以没有冲突。这样便产生如下概念:

  • 当前读: 读取了数据的最新版本。
  • 快照读: 读取到的不一定是最新版本,也可能是历史版本。其避免了加锁操作,降低了开销。

什么时候进行当前读(对当前版本操作):

  • 给select语句手动加锁: select ···· lock in share mode; (添加共享锁)selct··· for update (加排它锁)
  • update; insert; delete (排它锁)

什么时候进行快照读:

  • 不手动加锁的select语句

MVCC分析说明

上述说明,MVCC可以让数据库并发执行读写操作,即解决读写并发问题。而读写并发问题开始说了有以下几个 脏读, 不可重复读幻读。那们我们来分析一下其是怎么解决的。

脏读,即读取另一事务中未提交的。

  • 只需要将未提交数据不更新到其他事务的可见版本中,就不会被读取了。

不可重复读:即同一事务中,读取结果不一致,原因是两次读取之间另一事务进行了修改了该数据。

  • 使用MVCC后,不手动加锁的select语句(即快照读操作),可以继续读取历史版本,其他事务的修改操作修改的只是最新版本,不会影响该事务中数据的读取。即多次读取结果一致。

幻读:读取数据后,其他事务添加了数据。读取的结果和实际结果不一致。

  • 如果是快照读,那么也完全不影响,其修改了当前数据,不改变历史版本数据。
  • 如果是在事务中想进行当前读操作,那么是不行的。当前读情况下的幻读需要通过next-key lock(临键锁)+ 可重复读隔离级别来控制

乐观锁与悲观锁-解决丢失更新问题。

在事务并发期间,会出现读-写并发问题,和写-写并发问题。上述通过MVCC解决了读-写并发问题,而写-写并发问题就需要乐观锁和悲观锁去解决。

写-写并发问题有一种:丢失更新

丢失更新问题

有这样一个流程:事务A修改数据,然后事务B也修改该数据,之后事务A提交了修改,最后B再操作该数据(提交或回滚)。

可以看到上述流程如果不加以控制,那么事务A所作的更新就会丢失掉。这种丢失根据事务B最后的操作分为两种:

  • 事务B最后执行回滚操作:那么事务A提交的数据也被回滚了,这就叫操作回滚
  • 事务B最后执行提交操作:那么事务A提交的数据就会被覆盖,这就叫操作覆盖

悲观锁

解决丢失更新问题第一种解决方案就是悲观锁。

顾名思义:在读取数据的时候(先读取后修改),悲观的认为其他事务会修改它。那么就在读取数据的时候给数据加上锁,在结束事务之前,不允许其他事务对该数据修改。

这种解决办法开销比较大。

MySQL中实现悲观锁的方法是添加排他锁。

乐观锁

解决丢失更新问题另一种解决方案就是乐观锁。

顾名思义:在读取数据的时候(先读取后修改),乐观的认为其他事务不会修改它。只在进行更新操作的时候检查冲突。

乐观锁的实现就变成了如何检查冲突:

  • 检查冲突通常是基于数据版本记录机制实现的。一般可以为数据添加一个version版本号字段。
  • 对数据的更新操作会使得version字段变化。
  • 当读取数据的时候,会读取到一个版本号。而要进行更新的时候,会检查这个版本号是否一致,如果不一致说明在此之前有别的事务修改了该数据,就要重新读取再修改。

死锁

死锁的定义

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

从使用锁的层面来解释:存在两个或两个以上的并发事务,事务之间持有对方所需要的锁,造成了死循环。即都不释放自己已有的锁,又想得到别人的锁。

死锁的解决和避免

死锁的解决:

  • InnoDB通常会自动检测死锁。并会回滚其中代价最小的事务,解除这个死循环。
  • 同时你也可以手动检查死锁然后去解决。
set global innodb_print_all_deadlocks=on;   --设置innodb_print_all_deadlocks属性

show variables like 'innodb_print_all_deadlocks'; -- 查看是否开启。

show engine innodb status; -- 查看数据库状态,会包含锁信息

避免死锁的方式:

  • 操作完立即提交事务(例如:避免在事务中等待用户输入)。尽量不要有长事务减少事务的持续时间。
  • 如果有多个事务需要锁定多个资源时,确保它们总是以相同的顺序。这样就不会出现互相等待对方的锁的情况。
  • 考虑使用低隔离级别:降低事务的隔离级别可以减少锁冲突。
  • 索引优化:可以减少行锁的数量和持续时间,避免不必要的全表扫描和锁定。
  • 优化查询语句:尽量使用更精确的条件进行查询,减少锁的数量。
  • 尽量使用乐观锁,而不是悲观锁。因为乐观锁通常使用版本号等方式进行冲突检测,而不是实际的锁。

参考与感谢

MySQL 三万字精华总结 + 面试100 问,和面试官扯皮绰绰有余(2021 年 6 月更新) - 知乎 (zhihu.com)
一文弄懂MySQL锁机制【记录锁、间隙锁、临键锁,共享锁、排他锁,意向锁】_并发控制里面排它锁记录锁、间隙锁-CSDN博客
【MySQL笔记】正确的理解MySQL的MVCC及实现原理_mysqlmvcc实现原理-CSDN博客
MySQL事务之丢失更新问题-CSDN博客
彻底搞懂MySQL死锁-CSDN博客
MySQL InnoDB锁机制详解与实践-百度开发者中心 (baidu.com)
MySQL锁定:死锁及其避免方法-阿里云开发者社区 (aliyun.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值