为了账号安全,请及时绑定邮箱和手机立即绑定

InnoDB事务与锁


事务

可以理解为数据库执行的一个最基础的单位,其包含有限的操作命令(crud)。

事务的属性(ACID):事务必须满足四个属性,原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)

原子性:要么执行完全结束,要么全部不执行。避免数据执行不完全带来的错误数据,所以事务必须具有原子性(commit、rollback)。即在事务A提交之前,如果发生错误,则需要回退到事务执行前的状态。

一致性:指的是事务在执行的前后,数据库的数据一定会保持一致性的状态。可以理解为系统从一种状态转变为另一种状态。事务A在提交之后,对系统的改变,事务B一定会感知到相同的变化。

隔离性:指的是事务之间的执行相互独立。在并发多个事务执行的时候,每个事务内部的操作不会影响到其他的事务。事务的执行可以抽象为串行执行(这里是修改上的串行)。针对不同情况,事务的隔离会有不同的隔离级别。

持久性:事务一旦提交成功就会被更新到到数据库,不会再被回退。

=================================================================

事务的隔离性级别:

针对并发执行的事务,会出现以下问题:(脏读、不可重复读、幻读)

1.脏读:事务A读取事务B更新的数据,数据B进行了回滚操作,那么A读取到的数据是回滚前的数据是脏数据。

2.不可重复读:在一次事务中,前后两次查询的数据不一致。主要是在事务未提交前,有其他的事务进行了更新提交操作。

3.幻读:事务A在对系统进行更新以后,事务B对系统进行了插入或者删除操作,导致事务A发现仍有数据未更新,如同幻觉。

四种事务隔离级别:(处理并发问题)

隔离级别 脏读 不可重复读 幻读

读未提交 是 是 是

读已提交 否 是 是

可重复读 否 否 是

可串行化 否 否 否

读未提交(Read Uncommitted):在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。事务A可以读取事务B未提交的数据,如果事务B回滚,此时A读取的数据即为脏数据。

读已提交(Read Committed):事务A只能读取事务B,或者其他事务已经提交的数据。所以不会出现脏读的数据,但是会出现前后两次不一致的不可重复读,或者幻读。

可重复读 (Repeatable Read):这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。这是因为MVCC机制,select读取不会更改版本号,是快照读,而insert、update和delete会更新版本号,是当前读(当前版本)(注意,这里不会破坏数据的一致性)。无法解决幻读的现象。

可串行化(Serializable):当事务A开启此隔离的级别时候,当其他事务插入一条记录报错,表会被锁了插入失败,mysql中事务隔离级别为serializable时会锁整个表,防止幻读的出现,但是完全的串行换操作会导致效率极低,一般不会使用此隔离级别。

=================================================================

设置全局事务隔离级别:

>SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
>SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
>SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
>SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

设置当前会话事务隔离级别:

>set session transaction isolation level READ UNCOMMITTED;
>set session transaction isolation level READ COMMITTED;
>set session transaction isolation level REPEATABLE READ;
>set session transaction isolation level SERIALIZABLE;

查询事务隔离级别分析工具:

>show variables like '%iso%';
>select @@global.transaction_isolation;

事务隔离级别加锁分析

锁的类型:

锁模式

共享锁(share(S) Lock):

SELECT ... LOCK IN SHARE MODE
  • 排他锁(exclusive(X) Lock):

SELECT * FROM table_name WHERE … FOR UPDATE
  • 意向锁(Intention Locks):表级别的锁,自动施加,自动释放。

锁在细力度上又可以分为 表锁和行锁

共享锁(share(S) Lock):共享锁,顾名思义此对象锁是可以共享的,即事务A对数据加上共享锁以后,其它事务也可以加上共享锁,但是不能加排它锁。共享锁只能读,不能修改数据。(行锁)。

排他锁(exclusive(X) Lock):当且只有一个事务可以对数据加锁,而其他事务不能再加任何类型的锁,排他锁可以读,也可以写。(行锁)。

意向锁(Intention Locks):表级别的锁,是在数据库的某行加锁(s或者x)的获取时候,自动加上意向锁(IS或者IX),记录有此类(s或者x)锁。(表级别锁不会和行级别锁冲突)

为什么要使用意向锁:当事务要加入一个表级锁的时候,我们需要遍历每一行看一看是否有其他锁防止冲突,如果数据量较大那么性能将会极低,所以加上意向锁可以直接判断是否有意向锁即可。

获得锁

1.事务在获得某个数据的S锁,必须先获得一个IS或者更强的锁

2.事务在获得某个数据的X锁,必须先获得表的IX锁

加锁

事务在加入表级锁时,先判断是否有X、IX、S、IS锁,有则失败

X IX S IS

X Conflict Conflict Conflict Conflict

IX Conflict Compatible Conflict Compatible

S Conflict Conflict Compatible Compatible

IS Conflict Compatible Compatible Compatible

可以看出,共享锁S是兼容S和IS的。IS与IX意向锁是对表加锁相互兼容的。

=================================================================

锁的类型

记录锁(Record Locks):innoDB的行锁是对索引的加锁,innoDB是一个聚簇索引使用的是B+树来进行实现的,所以innoDB的锁只有在使用索引的条件时候,才会加锁,即便是不同行但是相同索引也会有冲突。当不使用索引时,则会使用表锁。

间隙锁(Gap Locks):对索引项之间的"间隙"加锁,不包括索引项本身(左右开区间)。e.g. SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; GAP锁会阻止其他事务进行的插入操作,不会阻止其他事务获取相同间隙的gap锁并且gap S-lock和gap X-lock不冲突。

Next-Key Locks : Next-Key 锁 = 记录锁+间隙锁。锁定一个范围并且锁定记录本身索引。InnoDB的默认加锁方式。

插入意向锁(Insert Intention Locks):是间隙锁的一种,在插入一条记录之前,需要先拿到插入间隙的插入意向锁。当其他事务持有目标间隙的Gap锁时会阻塞。

=================================================================

MVCC(Multiversion Concurrency Control)多版本并发控制

作用:实现了读不加锁不会导致阻塞,写加锁,读写不冲突,在读多写少的场景下极大的提高了其效率,增加了其并发性。RC和RR级别使用。

实现:在每一行记录的后面增加两个隐藏列,记录创建版本号和删除版本号。每个事务都有唯一的递增版本号,那么每一操作的创建版本号或者删除版本号都会使用事务的版本号。

=================================================================

  • INSERT:InnoDB为每个新增行记录当前事务编号作为创建ID

  • UPDATE:记录修改当前行的值,写事务编号,回滚指针指向undo log中的修改前的行

  • DELETE:将删除位置为删除

  • SELECT:查询出数据行的版本小于等于当前事务的版本号的数据,如删除位为删除则表示已删除

1.创建一个name=sql的数据,事务号为1

>insert into test (name) values(1);

id name create version delete version

1 sql 1

2.接着更新 这个数据,使得name=innodb,其事务版本号为2。

具体操作 先把以前这条数据删除,在插入新的数据,使用版本号来标记

>update test set name= 'innodb' where id=1;

id name create version delete version

1 sql 1 2 (代表 这条数据已经被删除)

1 innodb 2

3.删除操作,事务版本号为3

>delete test where id = 1;

id name create version delete version

1 sql 1 2 (代表 这条数据已经被删除)

1 innodb 2 3 (代表 这条数据已经被删除)

4.查询条件: 创建版本号 < 当前版本号 < 删除版本号

RR:在RR的时候,读的是当前的快照表,读取的是固定版本(第一次select的版本)的数据,所以一个事务内的读是一致的。但是 insert update delete操作的是当前读,是最新版本的数据。(快照读)

RC:读事务每次都读取最近的版本,因此两次对同一字段的读可能读到不同的数据(幻读),但能保证每次都读到最新的数据。(当前读)

快照读:读到的数据可能是历史版本数据。

当前读:读到的数据是最近版本数据,特殊的读操作,插入/更新/删除操作,属于当前读需要加锁。

=================================================================

针对隔离级别加锁分析

RU: Select不加锁,写加X锁

RC: Select快照读不加锁,写加X锁

RR: Select快照读不加锁,写加X Next-key锁

Serializable: Select加S Next-key锁,写加X Next-key锁

=================================================================

RU与RC:区别在于快照读与无快照读

RC与RR:一致性读肯定是读取在某个时间点已经提交了的数据,RC的时间点为当前时间点,RR的时间点是第一次select的时间点

RR与Serializable:RR通过快照读,读取的都是过去某个时间点的快照,而Serializable级别下都是加了S锁的读,督导的都是当前时间点的,这意味着在提交前,其他事务无法进行提交。

=================================================================

死锁

5acb3c8700013dc501600160.jpg

从图中,可以看到一个Update操作的具体流程:当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁(current read).待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录.一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止.因此,Update操作内部,就包含了一个当前读.

注:根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的.先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕.

单个SQL组成的事务,从宏观上来看,锁是在这个语句上一次获得的,但从底层实现上来看,是逐个记录行查询,得到符合条件的记录即对该行记录的索引加锁.,而加锁的过程是边查边加、逐行获得。

这个一个数据库的设计原则,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。在一个事务中先加锁执行完操作,再统一在commit前释放所有的锁。

RC与RR的区别,RC级别下不会加GAP锁,RR级别下会加GAP锁

5c8131750001192c07200520.jpg

两个表、两行记录,交叉获得和申请互斥锁,相同表记录行锁冲突:事务A按照一定顺序加锁,事务B按照相反的顺序加锁就会出现死锁

如何避免死锁

  • 在程序中添加对死锁的重试

  • 完成相关变动后尽早提交事务

  • 修改多表或同表的多行数据时,按照一定的顺序

  • 添加合适的索引

  • 一切都没用则可以使用表锁

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消