MySQL(05) mysql锁,MVCC、Innodb行锁

发布于:2025-07-25 ⋅ 阅读:(14) ⋅ 点赞:(0)

事务的隔离性由锁来实现

当多个线程并发访问某个数据的时候,尤其是一些敏感的数据(比如订单、金额),我们就需要保证这个数据在任何时刻“最多只有一个线程”在访问,保证数据的完整性 和 一致性。

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、共享锁和排它锁

共享锁和排他锁,也叫读锁(readlock)和写锁(write lock)。


容易误会的点:以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
 

关键规则:共享锁和排他锁是互斥的!

无 MVCC 的系统中(如纯锁机制),为了保证一致性:

读操作需要加共享锁(S Lock),会阻塞写操作(排他锁 X Lock)。
写操作需要加排他锁(X Lock),会阻塞读操作(S Lock) 和其他写操作。

共享锁阻塞排他锁,确保了在读取过程中,数据不会被其他未提交的事务修改,从而避免了脏读。

需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。


5、Select操作加S锁或X锁

SELECT ... 语句正常情况下为快照读,不加锁;
SELECT ... LOCK IN SHARE MODE 语句为当前读,加 S 锁;
SELECT ... FOR UPDATE 语句为当前读,加 X 锁;
常见的 DML 语句(如 INSERT、DELETE、UPDATE)为当前读,加 X 锁;
常见的 DDL 语句(如 ALTER、CREATE 等)加表级锁,且这些语句为隐式提交,不能回滚。

为什么读操作会SELECT ... FOR UPDATE,核心目的:实现“先锁后改”的原子操作

场景 锁类型 说明
普通 SELECT (快照读) 无锁 InnoDB 默认行为。通过 MVCC 读取历史快照,无需加锁。
SELECT ... LOCK IN SHARE MODE 共享锁 (S Lock) 显式要求加共享锁,阻塞其他事务的写操作(X Lock)。
SELECT ... FOR UPDATE 排他锁 (X Lock) 显式要求加排他锁,阻塞其他事务的读写操作(S/X Lock)。

对读取的记录加 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结束阻塞

开启事务1,加X锁,开启事务2,加s锁/x锁(都会阻塞),因为X锁是排它的


6、mysql8.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 ...

快照读:读取到的并不一定是数据的最新版本,可能是之前的历史版本


2、当前读

当前读:读取的是记录的最新版本,最新数据。加锁的SELECT或者对数据进行增删改都会进行当前读,如:


3、行格式中的隐藏字段


4、ReadView 

ReadView和事务是一对一的。

ReadView就是一个事务在使用MVCC机制进行快照读操作时产生的读视图。ReadView要解决的核心问题是:判断版本链中的哪个版本是当前事务可见的

ReadView中4个重要的内容如下

 活跃指,已经启动但是没提交的事务,提交ReadView访问规则了的事务不在ids里边


5、ReadView访问规则

在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见,某个版本也就是下文的被访问的版本。


6、ReadView(读已提交

在隔离级别为读已提交(Read Committed)时,一个事务中的每一次SELECT查询都会重新获取一次Read View。


7、ReadView(可重复读

当隔离级别为可重复读的时候,一个事务只在第一次SELECT的时候会获取一次Read View,
而后面所有的SELECT都会复用这个Read View,如下表所示:


Innodb锁的粒度

1、mysql表级锁

表级锁:锁整张表

  • 开销小,加锁快;
  • 不会出现死锁;
  • 锁定粒度大,发生锁冲突的概率最高,并发度最低。

2、mysql行级锁

行级锁:对一行或者多行记录加锁

  • 开销大,加锁慢;
  • 会出现死锁;
  • 锁定粒度最小,发生锁冲突的概率最低,并发度也最高

记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-key Lock)


3、mysql页级锁

页级锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

从操作粒度来说:表级锁>页级锁>行级锁

为了尽可能的提高并发度,每次锁定的数据范围越小越好


4、索引没命中(表锁)

当我们执行UPDATE、DELETE语句时,如果WHERE条件中字段没有命中索引的话,就会导致扫描全表对表中的所有记录进行加锁

索引没命中,行锁变表锁

1.在session1会话窗口,

BEGIN;
-- 加 X 锁(索引未命中时 锁粒度为表锁)
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字段上面我们没有建索引,不会命中索引,使得行锁变表锁


5、主键索引命中(行锁)

InnoDB的行锁,是通过锁住索引来实现的,如果加锁查询的时候没有使用到索引,会将整个聚簇索引都锁住,相当于锁表了。

按照主键索引 id

id主键索引、聚簇索引、一级索引  都是一个意思

操作前

1.session1中,注意此时我们使用到了主键索引id,则会是行锁

BEGIN;
-- 加 X 锁(索引命中时 锁粒度为行锁)
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; # 转圈圈

6、二级索引命中(行锁)

按照二级索引cname

辅助索引、非聚簇索引、二级索引 都是一个意思

1.在cname字段自建一个二级索引

CREATE INDEX idx_cname ON t_customer(cname);

此时t_customer表中数据如下:

2.按照我们自建的索引去命中

在session1中,使用到了我们自建的索引。所以会是行锁,只会把这一条记录锁住

BEGIN;
-- 加 X 锁(索引命中时 锁粒度为行锁)
UPDATE t_customer SET age=1 WHERE cname='z3'

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

在session2中,这俩个SQL操作的是另外两条记录,所以可以。

UPDATE t_customer SET age=44 WHERE cNAME='z4'; #ok
 
UPDATE 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,一切都回归正常


网站公告

今日签到

点亮在社区的每一天
去签到