mysql事务与锁机制详解

时间:2023-03-10 06:02:40
mysql事务与锁机制详解

一、事务

1.事务简介

(1)事务的场景

转账:一个账户减少,另一个账户增加。两个动作同时成功或者同时失败。就要开启事务。

(2)事务定义

事务是数据库管理系统执行过程中的一个逻辑单元,由一个有限的数据库操作序列构成。

逻辑单元,是数据库最小的工作单元,不可以再分割。有限的数据库操作序列:指的DML语句(对于数据的增删改语句)

(3)哪些存储引擎支持事务?

Innodb、NDB存储引擎支持事务,Myisam不支持。

表类型 == 存储引擎

不同的表类型(存储引擎),它有不同的存储方式(disk,mem,csv(方便迁移)等)和使用方式(更高的查询速度|更快的插入删除操作|数据保持一致性等)。

2.事务4大特性

ACID

原子性 Atomicity

一致性 Consistency

隔离性 Isolation

持久性 Durablity

原子性:在事务中的语句要么都成功,要么都失败。

一致性:包含两方面:(1)数据库自身的完整性的约束不能破坏,即事务执行前后数据都是合法的状态(比如主键是唯一非空的,字段的类型和长度不能违反)。(2)用户自定义的完整性(在用户的代码里控制。比如借记卡a减去1000,而b只加上500,虽然满足原子性,但破坏了用户的一致性。)。原子性、隔离性、持久性最终都是为了保证一致性。

隔离性:多个事务对数据表或者数据行的操作是互相透明、互相不干扰的。

持久性:事务对数据的增删改操作只要提交成功,那么就是结果是永久性的(无论重启还是数据库崩溃)

隔离性怎样保证事务之间互不干扰?隔离级别。

3.事务场景

数据库什么时候会出现事务?

(1)自动事务

执行增删改操作,会自动执行事务。

增删改操作没有手动commit,也有个事务。因为通过show variables like ‘autocommit’发现autocommit是on的,就是自动开启的。

(2)手动事务

如果把autocommit设为off,那么增删改操作不会实时自动提交事务。set session autocommit=off.

手动开启事务:

start transaction 或者begin

事务结束两种方式:rollback(回滚)、提交(commit)。工具关掉,断开连接,事务默认是回滚。

事务持有的在事务结束时会释放。

4.事务并发带来的问题?

事务并发带来三个问题:

(1)脏读

一个事务读取到其他事务未提交的数据而造成在同一个事务里前后两次执行相同的查询得到不一样的结果。这个未提交的数据叫做脏数据,它是在内存里面的,没有同步到磁盘。这种读取叫脏读。

如果这个事务基于脏数据去修改,而另外一个事务又把脏数据进行了回滚,就会带来数据的不一致问题。

(2)不可重复读

一个事务读取到其他事务已经提交的数据而造成在同一个事务里前后两次执行相同的查询得到不一样的结果,这个读取叫做不可重复读

(3)幻读

在一个事务里前后两次执行相同的查询突然多了一条数据,叫幻读。

无论是修改update还是删除delete数据,都是不可重复读;只有新增insert插入数据(多了数据),才会产生幻读。

事务并发带来的三大问题都是数据库读一致性(在一个事务里面前后执行相同的查询语句得到相同的查询结果)问题,必须由数据库提供一定的事务隔离机制来解决。

5.事务的四种隔离级别

SQL92 ANSI/ISO标准

未提交读

已提交读(RC):解决脏读

可重复读(RR):解决脏读、不可重复读

串行化:解决事务并发的所有问题

未提交读:在这个隔离级别下,一个事务可以读取到其他事务未提交的事务的。出现三大问题,最低隔离级别。

已提交读:一个事务可以读取到其他事务已经提交的数据,不能读取到其他事务未提交的数据。解决脏读问题,但未解决其他问题。

可重复读:解决脏读和不可重复读。

串行化:排队执行,没有并行,就没有这三大问题。但并发效率降低。生产环境基本不用。

Innodb默认的隔离级别是可重复读级别(RR)

Innodb在可重复读级别下解决了幻读的问题。因此Innodb存储引擎的数据库采用可重复度级别,即能保证很好的并发性,又能防止并发性带来的问题。但是Innodb为什么在可重复读级别下解决幻读问题呢?

思考:要解决读一致性(在一个事务里)的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,应该怎么做?

(1)加锁

在读取数据前,对其加锁,防止其他事务对数据进行修改------》基于锁的并发控制(lock based concurrency control LBCC

这种方式造成并发数大大降低,影响数据库的性能

(2)建立一个备份(快照)

生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别的一致性读取--------》多版本并发控制(Muti Version Concurrency Control MVCC

根据业务不同场景协同使用LBCC和MVCC(加锁和使用快照)。

mysql里的两个事务日志:

回滚日志(undo log):比如事务前几个命令成功了,中间有个命令失败了,那么前面的命令要撤销,通过撤销日志实现。undo log有两个作用:提供回滚多个行版本控制(MVCC)。

重做日志(redo log):提供前滚操作。

https://www.cnblogs.com/DataArt/p/10209573.html

二、锁

1.锁的分类

innodb引擎里的锁:

行级别的锁:共享锁和排他锁。--》能锁定一行数据

表级别的锁:意向锁。

对应前面的行级别锁,由意向共享锁和意向排他锁

锁的算法:记录锁、间隙锁、临键锁

行或者表级别的锁理解为锁的类型或者模式,而这三个理解为锁的算法。比如排他锁既可以是记录锁,也可以是间隙锁,也可以是邻间锁。

插入意向锁

自增锁

 

 

补:

从锁的粒度来说:有表锁和行锁;

从锁的用法莱索:有乐观锁和悲观锁

从锁的类型或者基本模式来说:排他锁、共享锁、意向共享锁、意向排他锁

从锁的算法来说:间隙锁、记录锁、临键锁

从锁的问题来说:死锁

从锁的使用方式来分:乐观锁和悲观锁

2.行锁与表锁

Myisam支持表级别的锁;Innodb既支持表级别的锁,又支持行级别的锁。

(1)表锁与行锁的区别:

锁定粒度: 表锁 > 行锁

加锁效率:表锁 > 行锁

冲突概率:表锁 > 行锁

并发性能:表锁 < 行锁

??

(2)共享锁(行锁)

又称读锁。共享锁可以在多个事务之间共享的。一个事务给一行数据加上一个共享锁,另外一个事务也可以给这行数据加上一摸一样的共享锁。读锁:给这行数据加了读锁,是用来读数据的,不是用来修改数据的,如果加了读锁再去修改它,就会死锁。

加锁方式:select * from student where id = 1 LOCK IN SHARE MODE;

在查询语句后面加上LOCK IN SHARE MODE
释放共享锁:只要事务结束就释放锁了 rollback/commit

共享锁场景:先有订单信息表order_info,又有一张订单详细表order_detail。order_info和order_detail是一对多的关系。现在我在修改详细表的时候,不希望信息表的数据被修改,就可以给信息表加个共享锁。(对于逻辑上有从属关系的表,但修改从表时,不希望主表被修改,就给主表加共享锁)。

共享锁是用来读取数据的,不是用来修改数据的。

(3)排他锁(行锁)

如果一个事务获取一个数据行的排他锁,那么其他事务就不能再获取该行的锁(共享锁和排他锁)。又称写锁。获取排他锁的事务可以对数据进行读取和修改

加锁方式:

自动加锁:增删改(delete、update、insert)语句默认加上排他锁

手动加锁:select * from student where id=1 FOR UPDATE

(4)意向锁(表锁)

意向锁是由数据库引擎自己维护(加)的,用户无法手动操作意向锁。

一个事务成功的给一张表加上表锁(一次性获取表中任何资源)的前提:没有其他任何事务已经锁定了这张表的任意一行数据

但在加表锁时怎么知道没有其他事务已经锁定这张表的任意一行数据呢?要做全表的扫描。检测很耗时。解决方法:意向排他锁意向共享锁

直接看这个表上面有没有意向锁排他锁和意向共享锁,就可以判断能不能加表锁。意向锁的作用就是调高加表锁的效率。  把意向排他锁和意向共享锁当成标志,而不是真正锁定数据的锁。

意向共享锁(IS锁):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的意向共享锁

意向排他锁(IX锁):表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁之前必须先取得该表的意向排他锁

比如:在一个事务中先在查询中加入排他锁:select * from teacher where id=1 for update,这个时候student表自动出现意向排他锁(引擎自己加上),现在在第二个事务里给表加写锁:lock tables student write。这时直接判断这张表上有没有意向排他锁就行了,不需要全表扫描。

意向锁是提高加表锁效率的一个标志。

3.锁的作用

锁的本质是解决资源竞争的问题。你用一个资源,我也用这个资源,让大家互相排斥。在程序中,锁是锁住了一个对象,实现资源的并发访问;在数据库中,锁是锁住了索引(不是字段也不是一行记录)。

问题1:表里面没有索引或者查询没有用到索引,会锁表(锁住所有行)。为什么?

索引的本质是什么?索引的本质是B+树。聚集索引(索引的逻辑顺序和数据的物理顺序是一致,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的。)只有一个索引可以决定数据的物理顺序。(比如字典,索引可以有拼音、笔画、偏旁,但实际字典上是按照拼音的顺序排序的)。非聚集索引(叶子节点存的是字段的值,通过这个非聚集索引的键值找到对应的聚集索引字段的值,再通过聚集索引键值找到表的某行)

a.主键就是聚集索引。b.没有设置主键,有唯一索引并且是非空的,那么 会找到第一个非空的唯一索引,作为聚集索引。c.没有设置主键和非空唯一索引,就使用隐藏的字段_rowid作为聚集索引。

mysql中有三个隐藏字段:_rowid(行号)、tx_id(事务id)、roll_ptr(回滚指针)

因此,当表没有创建索引时,只能走全表的扫描,会把每行隐藏的_rowid聚集索引都锁定起来,造成锁表的现象。

一张表是不可能没有索引的(哪怕什么也没有创建,也有聚集索引,是隐藏的_rowid)

问题2:为什么通过唯一索引加锁会把主键索引锁住呢?

主键索引和唯一索引的关系:mysql中将索引分为两类:聚集索引(也就是主键索引),非聚集索引(二级索引,比如唯一索引,普通索引等)

聚集索引和二级索引的区别:

索引的本质是B+树,一个索引就是一个B+树。主键索引的叶子节点存储了完整的数据,这也是为什么innodb有个概念叫:索引就是数据,数据就是索引。二级索引的叶子节点会存索引字段的值,还存了这条数据对应的主键的值。当用二级索引去查询数据时,先扫描二级索引这颗B+树,然后根据主键的值到主键索引的B+树拿到完整的数据。----》回表

由于有回表这个操作,因此给唯一索引加锁,也会锁住主键索引。

4.锁的算法:记录锁、间隙锁、临键锁

区间定义:

主键索引id的值为1、4、7、10

用id这个主键索引来划分区间。

记录:id的值比如1、4、7、10称为record记录

间隙:record隔离开来的不存在的区间称为gap间隙。如(-oo, 1),(1,4),(4,7),(7,10),(10,+oo)。开区间

如果有n个record,会有n+1个gap。

临键(next-key):数据不存在的区间加上下一个索引记录。左开右闭的区间。

记录锁:

使用场景:唯一性索引(唯一/主键)等值查询,精准匹配。

比如:select * from student where id=4 for update

锁住id=4

间隙锁:

锁住的是数据不存在的区间的范围

条件:记录不存在。

比如:select * from student where id >4 and id < 7 for update;

锁住(4,7)这个间隙。注意:如果查询时是间隙的某个子集,也将锁住整个间隙。

这个时候在另一个事务在id=5或者6处插入数据不能成功。但查看就可以。因为记录本身也就不存在。所以间隙锁可以重复获取,之间不冲突。但记录锁就不行。

最主要的作用:阻塞插入

临键锁:

锁住的是不存在的区间和下一个记录值

条件:范围查询,包含记录和区间

比如:select * from student where id>5 and id<9 for update;

锁住:(4,7]  (7,10] 两个区间

临键锁 = 记录锁 + 间隙锁

5.为什么innodb的可重复读(RR)隔离级别中,解决了幻读问题?

关键:间隙锁。不让别人插入。

未提交读(read uncommited):不加锁。出现脏读、不可重复读和幻读。不会用

串行化(Seriablizable):让所有操作排队。具体方法:把所有的select语句都隐士的转化为select ... lock in share mode ,加了共享锁,这会和update、delete互斥(读写互斥)。解决了三个问题。不会用,并发效率太低。

可重复读(RR):(1)快照读(不加锁):不加锁的select(普通的select),用MVCC技术(快照),去读取undo log里面的快照(具体?),以前的数据,保证读一致性。在RR里面解决了幻读。(2)当前读(加锁):通过三种算法解决:记录锁、间隙锁和临键锁。

未提交读(RC):普通的select和RR一样。但在RC里快照不能解决幻读。和建立快照的时间有关系。RR建立快照的时间是第一次查询,所以可以看到第一次查询之前的数据。RC能够看到当前查询之前的数据。因此中间其他事务插入数据时,该事物查询也能看到插入的数据,没有解决幻读问题。

RC中没有间隙锁。间隙锁只在RR中存在。RC中有两个特例用到间隙锁:外键的约束检查,重复的键的检查。(但不常用)因此RC解决不了幻读。

6.乐观锁和悲观锁

(1)悲观锁

悲观锁:就是很悲观,每次去拿数据时都认为别人会修改,所以每次在拿数据或者修改数据时就会加锁,这样别人拿数据就会阻塞住直到自己释放锁。一旦加锁,只有一个线程执行,其他线程都要等待,直到锁释放。

场景:数据库的行锁(读锁、写锁)、表锁都是悲观锁,只有先拿到锁才能操作。

优点和缺点

优点:采取了先取锁再访问的保守策略,为数据处理的安全提供了保证。

缺点:(1)处理加锁机制会带来额外的开销(?比如)(2)增加了死锁的机会;(3)降低了并行性,一个事务如果锁定某行数据,那么其他事务就必须等待该事务处理完后才能操作这行数据。

(2)乐观锁

乐观锁:很乐观,认为数据一般情况下不会造成冲突,即不会有其他线程对数据进行修改,因此不会上锁。在数据进行提交更新时,会正式对数据的冲突进行检测,如果发生冲突,就返回错误信息,让用户决定如何去做。(使用版本号机制)

场景:适用于多读的应用类型,提高吞吐量

优点和缺点:乐观锁相信事务之间数据竞争的概率是比较小的,因此直到提交的时候才去锁定,所以不会产生任何锁和死锁,提高了效率。但是一旦锁的粒度掌握不好,更新失败的概率比较高,容易发生业务失败。

乐观锁的实现:

乐观锁一般使用版本号机制或CAS(compare and swap)算法实现

a.版本号机制

版本号:为数据增加一个版本标识。在数据库中,就是为数据库表增加一个version字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1.此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行对比,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据(中间有其他事务对数据进行了更新)。

b.CAS算法

即compare and swap(比较与交换)。CAS算法涉及到3个操作数:需要读写的内存值 V,进行比较的值 A和拟写入的新值 B。当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。