Mysql事务实现及锁的关系&实践分析一条sql加锁流程

本文详细介绍了MySQL事务的四大特性、隔离级别、MVCC机制以及不同SQL语句在不同隔离级别下的加锁行为。重点分析了行锁、表锁、共享锁和排他锁,提供实例演示,帮助开发者理解和应用这些概念以优化数据库性能。
摘要由CSDN通过智能技术生成

本篇文章主要简单描述一下Mysql事务的实现方式,MVCC机制,以及分析在不同事务隔离级别下,一条sql会加什么样的锁,如表锁,行锁,共享锁,排他锁等

一、事务的四大特性(ACID)

1.原子性(Atomicity)原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚
2.一致性(Consistency)一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态
3.隔离性(Isolation)多个并发事务之间要相互隔离。
4.持久性(Durability)持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的

二、事务的隔离级别

SQL标准定义了以下几种事务隔离级别

READ_UNCOMMITTED 读未提交:最低级别,一个事务可以读取另一个未提交事务的数据。幻想读、不可重复读和脏读都允许。

READ_COMMITTED 读已提交:一个事务要等另一个事务提交后才能读取数据。允许幻想读、不可重复读,不允许脏读。

REPEATABLE_READ 可重复读:在开始读取数据(事务开启)时,不再允许修改操作。允许幻想读,不允许不可重复读和脏读。

SERIALIZABLE 可串行化:最高级别,在该级别下,事务串行化顺序执行。幻想读、不可重复读和脏读都不允许。

事务不同隔离级别引发的问题

1  脏读,脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
2  不可重复读,不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
3  幻读,幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体。

事务隔离级别.jpg

MySQL数据库中,默认的隔离级别为Repeatable read (可重复读);

三、事务的实现方式

事务的实现可通过无锁的读和加锁两种方式。

首先要了解一下MVCC机制:

MVCC (Multiversion Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能

在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增

行记录.jpg

我们来看看不同操作时MVCC机制的体现:

Insert:InnoDB在MVCC机制中,会在创建时间隐藏列更新为当前的事务ID,删除时间列是undefined

Select:MVCC中只会返回创建时间小于或等于当前事务ID且删除时间要么是undefined要么大于当前事务ID的记录,两个条件都满足的记录才是正确的

Delete:MVCC会在删除时间列更新当前事务ID,然后真正删除是由MySQL后台运行的独立线程去定时清理的

Update:MVCC中的Update被拆分成了Insert和Delete操作,显示插入一个更新后的记录(主键可能重复),然后标记原始记录,等待其被删除

关于MySQL对于MVCC的实现想了解可以参考这篇文章,这里不做过多的赘述

通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。

对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)

快照读和当前读
  • 快照读:简单的Select操作,不加锁(在隔离级别为序列化时,MySQL会取消快照读,所有的读操作也会加读锁),也不会被其他加锁阻塞
例:select * from table where ?;

其中:
    Read Committed(读已提交)隔离级别:每次select都生成一个快照读。
    Read Repeatable(可重复读)隔离级别:开启事务后第一个select语句才是快照读的地方,
    而不是一开启事务就快照读,之后每次读取为同一个快照。
  • 当前读:特殊的读操作,Insert、Update、Delete操作,处理的都是当前的数据,需要加锁
例:
    select * from table where ? lock in share mode;(共享读锁)
    select * from table where ? for update(排他锁);
    insert into table values (…)(插入意向锁);
    update table set ? where ?(排他锁);
    delete from table where ?(排他锁);

为什么将插入/更新/删除操作,都归为当前读?我们看下一个Update操作的具体流程。

当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (共享锁)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,申请升级为排他锁,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查(插入意向锁),也会进行一个当前读。

四、MySQL中的锁

MySQL 锁分成两类:锁类型(lock_type)和锁模式(lock_mode),锁模式和锁类型通常结合使用
1.锁类型

描述的锁的粒度,也可以说是把锁具体加在什么地方,如行锁,表锁,间隙锁;

表锁: 操作对象是数据表。Mysql大多数锁策略都支持(常见mysql innodb),是系统开销最低但并发性最低的一个锁策略。事务对整个表加读锁,则其他事务可读不可写,若加写锁,则其他事务增删改都不行。

行级锁: 操作对象是数据表中的一行。是MVCC技术用的比较多的,但在MYISAM用不了,行级锁用mysql的储存引擎实现而不是mysql服务器。但行级锁对系统开销较大,处理高并发较好。

其中行锁细分为间隙锁(锁住两条记录的间隙,用于防止幻读),记录锁(普通的锁一条记录),Next-Key锁(间隙锁和记录锁的结合),插入意向GAP锁(插入时的锁,只与间隙锁冲突)

InnoDB存储引擎支持行锁和表锁,MyISAM只支持表锁

2.锁模式

描述的是到底加的是什么锁,譬如读锁或写锁。

读锁: 也叫共享锁、S锁,若事务T对数据对象A加上读锁,则事务T可以读A但不能修改A(修改需尝试升级为写锁),其他事务只能再对A加读锁,而不能加写锁,直到事务T释放A上的读锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

写锁: 又称排他锁、X锁。若事务T对数据对象A加上写锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁(共享锁和排他锁都不行,快照读可以),直到事务T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再修改A。

各类锁之间的互斥关系:
  1. 行锁与表锁冲突
  2. 行锁与同一记录行锁冲突
  3. 行锁与间隙锁不冲突
  4. 间隙锁只与插入意向间隙锁冲突
  5. 间隙锁与间隙锁不冲突
  6. 共享锁与共享锁不冲突
  7. 共享锁与排他锁冲突
  8. 排他锁与排他锁冲突

这里是锁冲突指的是一个事务获取一种锁,另一个事务尝试获取锁时的冲突,同一个事务内部的锁不会冲突

五、实践分析任一条sql加锁类型

首先MyISAM存储引擎不支持事务,且只支持表锁,故一般用于主从集群中的从库(读库),此文不做分析,InnoDB存储引擎支持事务,且支持表锁,行锁,间隙锁,意向锁等

在InnoDB存储引擎下, RC(读已提交)隔离级别不支持间隙锁,RR(可重复读)隔离级别支持间隙锁,Next-key 锁,我们这里分析RR隔离级别,同样的SQL在RC隔离级别只需去除间隙锁即可。

前提:所有的锁都是在关闭自动提交事务并设置手动提交事务的情况下(@Transactional),没有手动事务每次更新是独立一个事务,几乎不会发生冲突

在具体分析之前,先说一下结论:在MySQL中,InnoDB存储引擎下,RR(可重复读)隔离级别情况下,加锁种类最多,也是安装MySQL后默认的配置,故以此场景下分析,其次加锁的粒度需要找到对应的记录才能加行锁,故需要使用索引,在没有索引或有索引但是没有生效的情况下会锁住整张表(即表锁),引发线上灾难,严禁UPDATE或DELETE数据时WHERE条件中未使用索引。

关于查看一条sql是否使用索引可以使用Explain关键字查看,使用教程参考链接

场景一:select * from table where id = 1;

不加锁,简单的查询语句是快照读,自身不加锁,也不会被其他加锁的事务阻塞。

场景二:select * from table where id = 1 lock in share mode;id是主键

对id为1的这条记录加共享锁,其他事务可也加共享锁,不可加排他锁。

场景三:select * from table where id = 1 for update;id是主键

先对id为1的这条记录加共享锁,然后尝试升级为排他锁,若升级为排他锁时被其他事务加了共享锁则被阻塞,加锁成功后其他事务不可加共享锁,不可加排他锁。

场景四:update user set username = 2 where id = 1;ID是主键

与场景三相同,先加共享锁,后升级为排他锁,delete from 同理。

场景五:update user set username = 2 where name = 1;name不是主键,是唯一索引

先通过name=1在唯一索引上加锁,后找到name=1的主键 ID,再去聚簇索引(即主键索引)上加锁。

关于MySQL聚族索引,二级索引的区别可参考此文档

场景二到场景五中:若通过主键或唯一索引查找这条记录不存在时,则对此位置加间隙锁,此时插入对应条件的的数据会尝试加插入意向GAP锁会被阻塞,这防止了幻读的发生

场景六:update user set name = 2 where username = 5;username为普通索引且只有一条记录

先在普通索引上找到对应的记录,加排他锁,并在其索引附近间隙加GAP锁(间隙锁),然后找到对应的主键索引加排他锁,其中间隙锁的范围需实际分析,若数据库数据为,username: 2,3,5, 5, 8,9,则间隙锁范围为[3,8], 左闭右闭

场景七:update user set name = 2 where username > 5;username为普通索引且扫描出多条记录

与场景六的区别在于,范围扫描,当范围扫描结果很多时,mysql索引优化器会放弃使用索引进行全表扫描,若数量较少会使用索引,需用Explain关键字实测,若使用了索引,则会扫描符合条件的记录,扫描到第一条后执行场景六的操作,之后继续扫描下一条记录执行相同操作直到扫描到不满足条件的记录停止加锁。

场景八:update user set name = 2 where username = 5;username没有索引

若username没有索引或执行计划中未使用索引,则进行全表扫描,加表锁以及全表间隙锁,其他事务不能更新数据,不能插入数据,不能删除数据,只能进行快照读(无锁操作),直至此事务执行完毕释放锁。

总结:本篇文章描述了Mysql事务基本概念,MVCC机制,各种类型锁,以及实战分析一条sql会加什么样的锁,可供使用者实际使用过程对自己写的代码有更深入的理解,并根据加锁的方式解决相应的问题,求点赞关注

版权声明:本文为博主原创文章,转载请附上原文出处链接

原文链接:https://juejin.cn/post/7033796549821857822

参考文章:
https://www.cnblogs.com/zhaoyl/p/4121010.html

https://blog.csdn.net/Sugar_Rainbow/article/details/77926216

https://tech.meituan.com/2014/08/20/innodb-lock.html

https://www.cnblogs.com/chenpingzhao/p/5065316.html

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值