Mysql 事务与锁机制原理详细理解

一,ACID特性

I,原子性

原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。

修改---》Buffer Pool修改---》刷盘。可能会有下面两种情况:

  • 事务提交了,如果此时Buffer Pool的脏页没有刷盘,如何保证修改的数据生效? Redo
  • 如果事务没提交,但是Buffer Pool的脏页刷盘了,如何保证不该存在的数据撤销?Undo

每一个写事务,都会修改BufferPool,从而产生相应的Redo/Undo日志,在Buffer Pool 中的页被刷到 磁盘之前,这些日志信息都会先写入到日志文件中,如果 Buffer Pool 中的脏页没有刷成功,此时数据 库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写的数据不会丢 失。如果脏页刷新成功,此时数据库挂了,就需要通过Undo来实现了。

II,持久性

持久性:指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不 应该对其有任何影响,不会丢失。

一个“提交”动作触发的操作有:binlog落地、发送binlog、存储引擎提交,flush_logs,check_point、事务提交标记等。这些都是数据库保证其数据完整性、持久性的手段。

 MySQL的持久性也与WAL技术相关,redo log在系统Crash重启之类的情况时,可以修复数据,从而保 障事务的持久性。通过原子性可以保证逻辑上的持久性,通过存储引擎的数据刷盘可以保证物理上的持 久性。

III,隔离性

隔离性:指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并 发事务是隔离的。

InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、可串行化。锁 和多版本控制(MVCC)技术就是用于保障隔离性的(后面课程详解)。

IV,一致性

一致性:指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内 容,分别是约束一致性和数据一致性。

  • 约束一致性:创建表结构时所指定的外键、Check、唯一索引等约束,但在 MySQL 中不支持 Check 。
  • 数据一致性:是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是 单单依赖于某一种技术。

一致性也可以理解为数据的完整性。数据的完整性是通过原子性、隔离性、持久性来保证的,而这3个 特性又是通过 Redo/Undo 来保证的。逻辑上的一致性,包括唯一索引、外键约束、check 约束,这属 于业务逻辑范畴

 V,汇总

ACID 及它们之间的关系如下图所示,4个特性中有3个与 WAL 有关系,都需要通过 Redo、Undo 日志 来保证等。 WAL的全称为Write-Ahead Logging,先写日志,再写磁盘

 二,事务并发问题

I,更新丢失

当两个或多个事务更新同一行记录,会产生更新丢失现象。可以分为回滚覆盖和提交覆盖。

  • 回滚覆盖:一个事务回滚操作,把其他事务已提交的数据给覆盖了。
  • 提交覆盖:一个事务提交操作,把其他事务已提交的数据给覆盖了。

II,脏读

一个事务读取到了另一个事务修改但未提交的数据。

III,不可重复读

一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致。

IV,幻读

一个事务中多次按相同条件查询,结果不一致。后续查询的结果和面前查询结果不同,多了或少了 几行记录。

三,事务控制方式

I,排他锁

引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥 锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。

注意,在整个事务1结束之前,锁是不会被释放的,所以,事务2必须等到事务1结束之后开始。 

II,读写锁

读和写并发操作情况:读读、写写、读写、写读。 读写锁就是进一步细化锁的颗粒度,区分读操作和写操作,让读和读之间不加锁,这样下面的两个事务 就可以同时被执行了。

读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。 

III,MVCC(多版本控制)

多版本控制MVCC,也就是Copy on Write的思想。MVCC除了支持读和读并行,还支持读和写、写和读 的并行,但为了保证一致性,写和写是无法并行的。

 在事务1开始写操作的时候会copy一个记录的副本,其他事务读操作会读取这个记录副本,因此不会影 响其他事务对此记录的读取,实现写和读并行。

四,事务控制方式-MVCC

I,概念

MVCC(Multi Version Concurrency Control)被称为多版本控制,是指在数据库中为了实现高并发的 数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。 多版本控制很巧妙地将稀缺资源的独占互斥转换为并发,大大提高了数据库的吞吐量及读写性能。 

II,多版本控制

每次事务修改操作之前,都会在Undo日志中记录修改之前的数据状态和事务号, 该备份记录可以用于其他事务的读取,也可以进行必要时的数据回滚

III,MVCC实现原理

MVCC最大的好处是读不加锁,读写不冲突。在读多写少的系统应用中,读写不冲突是非常重要的,极 大的提升系统的并发性能,这也是为什么现阶段几乎所有的关系型数据库都支持 MVCC 的原因,不过目前MVCC只在 Read Commited 和 Repeatable Read 两种隔离级别下工作。

在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read)与当前读 (Current Read)。

  • 快照读:读取的是记录的快照版本(有可能是历史版本),不用加锁,效率高。(select)
  • 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发 修改这条记录。(select... for update 或lock in share mode,insert/delete/update)

IV,版本控制与回滚

假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针

 如一条数据是刚 INSERT 的,DB_ROW_ID 为 1,其他两个字段为空。当事务 1 更改该行的数据值时,会进行如下操作

  • 用排他锁锁定该行;记录 Redo log;
  • 把该行修改前的值复制到 Undo log,即图中下面的行;
  • 修改当前行的值,填写事务编号,使回滚指针指向 Undo log 中修改前的行。

然后进行事务2操作,过程与事务 1 相同,此时 Undo log 中会有两行记录,并且通过回滚指针连在一 起,通过当前记录的回滚指针回溯到该行创建时的初始内容

 注意: 当undo log无限去增长时,purge Thread 会去释放Undo Page,阻止其无限增大

MVCC已经实现了读读、读写、写读并发处理,如果想进一步解决写写冲突,可以采用: 乐观锁 悲观锁

五,事务隔离级别

  •  Read Uncommitted 读未提交:解决了回滚覆盖类型的更新丢失,但可能发生脏读现象,也就是 可能读取到其他会话中未提交事务修改的数据。
  •  Read Committed 读已提交:只能读取到其他会话中已经提交的数据,解决了脏读。但可能发生 不可重复读现象,也就是可能在一个事务中两次查询结果不一致。
  •  Repeatable Read 可重复读:解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上会出现幻读,简单的说幻读指的的当用户读取某一范围的数据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。
  • Serializable 串行化:所有的增删改查串行执行。它通过强制事务排序,解决相互冲突,从而解决 幻度的问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。

数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差(代价)。读未提交隔离级别最低,并发问题多,但是并发处理能力好。以后使用时,可以根据系统特点来选择一个合适的隔离级别。


I,事务隔离级别与锁的关系:

1)事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使 用的封装,隐藏了底层细节。

2)锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,对相应操作加不同的锁,就可以防止其他事务同时对数据进行读写操作。

3)对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在 开发中手动的设置锁。

面试问题:

MySQL默认隔离级别:可重复读

Oracle、SQLServer默认隔离级别:读已提交

一般使用时,建议采用默认隔离级别,然后存在的一些并发问题,可以通过悲观锁、乐观锁等实现处理。


II,隔离级别控制相关命令

//  Mysql查看当前隔离级别:

show variables like 'tx_isolation'; 

select @@tx_isolation;

// 修改与设置隔离级别

set tx_isolation='READ-UNCOMMITTED';

set tx_isolation='READ-COMMITTED';

set tx_isolation='REPEATABLE-READ';

set tx_isolation='SERIALIZABLE';

六,锁分类

从操作的粒度可分为表级锁、行级锁和页级锁。

  • 表级锁:每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在 MyISAM、InnoDB、BDB 等存储引擎中。
  • 行级锁:每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应 用在InnoDB 存储引擎中。
  • 页级锁:每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表 锁和行锁之间,并发度一般。应用在BDB 存储引擎中。

从操作的类型主要可分为读锁和写锁。

  • 读锁(S锁):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响。
  • 写锁(X锁):排他锁,当前写操作没有完成前,它会阻断其他写锁和读锁。
  • IS锁、IX锁:意向读锁、意向写锁,属于表级锁,S和X主要针对行级锁。在对表记录添加S或X锁之 前,会先对表添加IS或IX锁。

从操作的性能可分为乐观锁和悲观锁。 

  • 乐观锁:一般的实现方式是对记录数据版本进行比对,在数据更新提交的时候才会进行冲突检测,如果发现冲突了,则提示错误信息。
  • 悲观锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定, 再修改的控制方式。

 七,行锁运行原理

在InnoDB引擎中,我们可以使用行锁和表锁,其中行锁又分为共享锁和排他锁。InnoDB行锁是通过对 索引数据页上的记录加锁实现的,主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。

  • RecordLock锁:锁定单个行记录的锁,也称记录锁
  • GapLock锁:间隙锁,也称范围锁,锁定索引记录间隙,确保索引记录的间隙不变,比如,现有索引为3,5,7的数据,锁住了索引为5的数据,也会同时锁住5两边索引为3和7的数据来保持间隙不变。然后去插入一个索引为4或者6的数据是无法执行插入的,只能插入范围以外,也就是索引间隙以外的数据,比如插入一条索引为8的数据。
  • Next-key Lock 锁:记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范 围锁,可重复读隔离级别支持)

I,Next-Key Lock机制:

在可重复读隔离级别,InnoDB对于记录加锁行为都是先采用Next-Key Lock,但是当SQL操作含有唯一索引时,Innodb会对Next-Key Lock进行优化,降级为RecordLock,仅锁住索引本身而非范围。

面试问题:

  1. select ... from 语句:InnoDB引擎采用MVCC机制实现非阻塞读,所以对于普通的select语句, InnoDB不加锁
  2. select ... from lock in share mode语句:追加了共享锁,InnoDB会使用Next-Key Lock锁进行处 理,如果扫描发现唯一索引,可以降级为RecordLock锁。
  3. select ... from for update语句:追加了排他锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫 描发现唯一索引,可以降级为RecordLock锁。
  4. update ... where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以 降级为RecordLock锁。
  5. delete ... where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降 级为RecordLock锁。
  6. insert语句:InnoDB会在将要插入的那一行设置一个排他的RecordLock锁。

II,加锁原理图解 

主键加锁,因为会降级到RecordLock,加锁行为上仅在id=10的主键索引记录上加X锁。

 唯一键加锁,加锁行为上会先在唯一索引id上加X锁,然后在id=10的主键索引记录上加X锁。同样会降级索只锁定单条数据。

 非唯一键加锁,加锁行为上首先对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)- (11,f)范围分别加Gap Lock。

 无索引加锁,加锁行为上表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎 锁机制是基于索引实现的记录锁定)。

 八,悲观锁的使用

悲观锁是指在数据处理过程,将数据处于锁定状态,一般使用数据库的锁机 制实现。从广义上来讲,前面提到的行锁、表锁、读锁、写锁、共享锁、排他锁等,这些都属于悲观锁 范畴。

I,表级悲观锁(表级锁)

表级锁每次操作都锁住整张表,并发度最低。

相关命令:

        对表加锁(可批量): lock table <tableName1> read|write,<tableName2> read|write;

        查看表上加过的锁 :show open tables;

        删除表锁:unlock tables;

表级读锁:当前表追加read锁,当前连接和其他的连接都可以读操作;但是当前连接增删改操作 会报错,其他连接增删改会被阻塞。

表级写锁:当前表追加write锁,当前连接可以对表做增删改查操作,其他连接对该表所有操作都 被阻塞(包括查询)。

表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞。

II,行级悲观锁

共享锁(行级锁-读锁):

共享锁又称为读锁、S锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改使用共享锁的方法是在select ... lock in share mode,只适用查询语句

事务使用了共享锁(读锁),只能读取,不能修改,修改操作被阻塞。

排他锁(行级锁-写锁):

排他锁又称为写锁、X锁排他锁就是不能与其他锁并存如一个事务获取了一个数据行的排他锁,其他事务就不能对该行记录做其他操作,也不能获取该行的锁。

使用排他锁的方法是在SQL末尾加上for updateinnodb引擎默认会在update,delete语句加上 for update。

行级锁的实现其实是依靠其对应的索引,所以如果操作没用到索引的查询,那么会锁住全表记录。事务使用了排他锁(写锁),当前事务可以读取和修改,其他事务不能修改,也不能获取记录 锁(select... for update)。

九,乐观锁的使用 

 乐观锁是相对于悲观锁而言的,它不是数据库提供的功能,需要开发者自己去实现。在数据库操作时, 想法很乐观,认为这次的操作不会导致冲突,因此在数据库操作时并不做任何的特殊处理,即不加锁, 而是在进行事务提交时再去判断是否有冲突了。那么乐观锁实现的关键点:冲突的检测。

对于乐观锁的选择:(相比较悲观锁)悲观锁和乐观锁都可以解决事务写写并发,在应用中可以根据并发处理能力选择区分,比如对并发率要 求高的选择乐观锁;对于并发率要求低的可以选择悲观锁

乐观锁通常实现原理:

I,版本控制

使用版本字段(version) 先给数据表增加一个版本(version) 字段,每操作一次,将那条记录的版本号加 1。version 是用来查看被读的记录有无变化,作用是防止记录在业务处理期间被其他事务修改。

 II,追加最后操作时间戳

与使用version版本字段相似,同样需要给在数据表增加一个字段,字段类型使用timestamp 时间戳。也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳 进行对比,如果一致则提交更新,否则就是版本冲突,取消操作。

十,死锁原因排查

死锁分为表级死锁与行级死锁

I,表级死锁

表死锁概念: 用户A访问表A(锁住了表A),然后又访问表B;另一个用户B访问表B(锁住了表B),然后企图 访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要 等用户A释放表A才能继续......这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分 析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个 资源

II,行级死锁

情况I:如果在事务中执行了一条没有索引条件的查询,引发全表扫描把行级锁上升为全表记录锁定(等 价于表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或死锁......SQL语句中不要使用太复杂的关联多表的查询;使用explain“执行计划"对SQL语句进行分析,对于 有全表扫描和全表锁定的SQL语句,建立相应的索引进行优化。

情况II:两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁。如下图,在事务执行的过程中进行二次查询,发现添加了写锁无法进行访问,另一边事务也是如此,相互请求......该情况下应该做到开启事务时候尽可能一次性锁定所有需要访问的资源,按照id对资源排序,然后按顺序进行处理

情况III: 事务A 查询一条纪录,然后更新该条纪录;此时事务B 也更新该条纪录,这时事务B 的排他锁由于 事务A 有共享锁,必须等A 释放共享锁后才可以获取,只能排队等待。事务A 再执行更新操作时, 此处发生死锁,因为事务A 需要排他锁来做更新操作。但是,无法授予该锁请求,因为事务B 已经 有一个排他锁请求,并且正在等待事务A 释放其共享锁。

事务A: 1, select * from dept where deptno=1 lock in share mode; //共享锁

            3, update dept set dname='java' where deptno=1;//排他锁

事务B: 2, update dept set dname='Java' where deptno=1;//由于1有共享锁,没法获取排他锁,需等待

该情况下需要对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操 作;  或使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量 下的系统性能。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用 户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中;

III,死锁的排查

1,查看死锁日志: show engine innodb status;

2,查看锁状态

  •         show status like'innodb_row_lock%‘命令检查状态变量,分析系统中的行锁的争夺情况
  •         Innodb_row_lock_current_waits:当前正在等待锁的数量
  •         Innodb_row_lock_time:从系统启动到现在锁定总时间长度
  •         Innodb_row_lock_time_avg: 每次等待锁的平均时间
  •         Innodb_row_lock_time_max:从系统启动到现在等待最长的一次锁的时间
  •         Innodb_row_lock_waits:系统启动后到现在总共等待的次数

如果等待次数高,而且每次等待时间长,需要分析系统中为什么会有如此多的等待,然后定制优化。


资料来源:

《Mysql海量数据存储与优化》

《Mysql技术内幕:Innodb存储引擎》

猜你喜欢

转载自blog.csdn.net/qq_42773863/article/details/121198170