MySQL(05) mysql锁,MVCC、Innodb行锁
事务的隔离性由锁来实现
当多个线程并发访问某个数据的时候,尤其是一些敏感的数据(比如订单、金额),我们就需要保证这个数据在任何时刻“最多只有一个线程”在访问,保证数据的完整性 和 一致性。
1、并发事务访问相同记录
1.读-读情况
读-读情况,即并发事务相继读取相同的记录,这种情况非常安全。。。不需要考虑锁
======================
2.写-写情况
写-写情况,即并发事务相继对相同的记录做出改动,可能会发生脏写问题。
任何一种隔离级别都不允许脏写问题的发生。所以在多个未提交事务相继对这条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过锁来实现的。
这个所谓的锁其实是一个内存中的结构,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的。
注意:有几个事务,就会有几个锁结构
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联。比如,事务T1要对这条记录做改动,就需要生成一个事务T1的锁结构与之关联:
锁结构的属性解释:
trx信息:代表这个锁结构是哪个事务生成的。
is-waiting:代表当前事务是否在等待。
在事务T1提交或者回滚之后,就会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁, 发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is-waiting属性设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁了。
===================
3.读-写情况 (重点)
读-写或写-读,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生脏读、不可重复读、幻读的问题。
注意:MySQL在REPEATABLE READ隔离级别上就已经解决了幻读问题
2、并发问题的2种解决方案
脏写的问题,任何一种隔离级别都给解决掉了,这里的并发问题主要指脏读、不可重复读、幻读
方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁。
方案二:读、写操作都采用加锁的方式。
3、俩种方案对比
- 采用MVCC方式的话,读-写操作彼此并不冲突,性能更高。
- 采用加锁方式的话,读-写操作彼此需要排队执行,影响性能。
一般情况下我们当然愿意采用MVCC来解决读-写操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用加锁的方式执行。
4、共享锁和排它锁
共享锁(Shared Lock,S Lock)和 排他锁(Exclusive Lock,X Lock),也叫读锁(readlock)和写锁(write lock)。
需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。
对读取的记录加 S锁:
SELECT ... LOCK IN SHARE MODE; #或 SELECT ... FOR SHARE;(8.0新增语法)
对读取的记录加 X锁:
SELECT ... FOR UPDATE;
悲观锁的核心思想是在操作期间持有锁来保护数据的一致性,但也会降低并发性能。因此,在使用悲观锁时需要注意锁的粒度和持有时间,避免过度锁定导致性能问题。
读操作是可以加S锁或X锁的,演示思路如下:
1.开启事务1,加s锁,开启事务2,加s锁(成功),s锁之间是共享的
2.在1的基础上再开启事务3,加x锁(阻塞)提交事务1,事务3仍然阻塞,继续提交事务2,事务3结束阻塞
3.开启事务1,加X锁,开启事务2,加s锁/x锁(都会阻塞),因为X锁是排它的
8.0中的新特性
能查就查,查不了也不会去阻塞,会执行相应的行为
在8.O版本中,SELECT...FOR UPDATE,SELECT...FOR SHARE添加NOWAIT、SKIP LOCKED语法, 跳过锁等待,或者跳过锁定。
通过添加NOWAIT、SKIP LOCKED语法,能够立即返回。如果查询的行已经加锁:
1.那么NOWAIT会立即报错返回。
2.而SKIP LOCKED也会立即返回,只是返回的结果中不包含被锁定的行。
写操作指增删改,是一定加要X锁(排它锁)的
MVCC 多版本并发控制
MVCC更好的去处理 读写冲突,提高数据库的并发性能。MVCC的实现依赖于:隐藏字段、Undo Log、Read View。
1、快照读和当前读
快照读
快照读又叫一致性读,读取的是快照数据。不加锁的简单的SELECT都属于快照读,如下
SELECT * FROM player WHERE ...
快照读:读取到的并不一定是数据的最新版本,可能是之前的历史版本
当前读
当前读:读取的是记录的最新版本,最新数据。读取时会对读取的记录加锁,加锁的SELECT或者对数据进行增删改都会进行当前读,如:
2、行格式中的隐藏字段
3、ReadView
ReadView和事务是一对一的。
ReadView就是一个事务在使用MVCC机制进行快照读操作时产生的读视图。ReadView要解决的核心问题是:判断版本链中的哪个版本是当前事务可见的
ReadView中4个重要的内容如下
活跃指,已经启动但是没提交的事务,提交ReadView访问规则了的事务不在ids里边
ReadView访问规则
在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见,某个版本也就是下文的被访问的版本。
在隔离级别为读已提交(Read Committed)时,一个事务中的每一次SELECT查询都会重新获取一次Read View。
当隔离级别为可重复读的时候,一个事务只在第一次SELECT的时候会获取一次Read View,
而后面所有的SELECT都会复用这个Read View,如下表所示:
Innodb的行锁到底锁了什么
1、mysql锁级别
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
==========
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
页级锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
从操作粒度来说:表级锁>页级锁>行级锁
为了尽可能的提高并发度,每次锁定的数据范围越小越好
2、索引命中与没命中
索引没命中,行锁变表锁
案例一
1.在session一会话窗口,
BEGIN; UPDATE t_customer SET age=55 WHERE phone='13811112222'
2.在session2会话窗口,操作另外俩条记录
UPDATE t_customer SET age=55 WHERE id=5; #转圈等锁。 或者 UPDATE t_customer SET age=44 WHERE id=6; #转圈等锁。
会发现转圈现象,有了表锁???
3.对session1中的事务 commit/rollback;接着session就好使了
原因:在session1中操作数据时,phone字段上面我们没有建索引,不会命中索引,使得行锁变表锁
3、行锁通过锁住索引实现
InnoDB的行锁,是通过锁住索引来实现的,如果加锁查询的时候没有使用到索引,会将整个聚簇索引都锁住,相当于锁表了。
按照主键索引 id
id主键索引+聚簇索引+一级索引 都是一个意思
操作前
1.session1中,注意此时我们使用到了主键索引id,则会是行锁
BEGIN; UPDATE t_customer SET age=55 WHERE id=4
2.在session2中,只要你不跟人家抢那一行,都是OK的
UPDATE t_customer SET age=55 WHERE id=5; # OK 或者 UPDATE t_customer SET age=33 WHERE id=6; # OK 或者 UPDATE t_customer SET age=11 WHERE id=4; # 转圈圈
按照二级索引cname
辅助索引+非聚簇索引+二级索引 都是一个意思
1.在cname字段自建一个索引
CREATE INDEX idx_cname ON t_customer(cname);
此时t_customer表中数据如下:
2.按照我们自建的索引去命中
在session1中,使用到了我们自建的索引。所以会是行锁,只会把这一条记录锁住
BEGIN; UPDATE t_customer SET age=1 WHERE cname='z3'
===============
在session2中,这俩个SQL操作的是另外两条记录,所以可以。
UPDATE t_customer SET age=44 WHERE cNAME='z4'; #okUPDATE t_customer SET age=55 WHERE cNAME='z5'; #ok
在session2中,这俩个操作都是不行的,因为被session1行锁了。
UPDATE t_customer SET age=11 WHERE cNAME='z3' # 转圈圈 UPDATE t_customer SET age=11 WHERE id=4 # 转圈圈
在session1中,使用率commit/rollback,一切都回归正常