MySQL中的锁事

一、概述

锁是计算机在执行多线程或线程时用于并发访问同一共享资源时的同步机制,MySQL中的锁是在服务器层或者存储引擎层实现的,保证了数据访问的一致性与有效性。

事务的隔离性是由的锁来实现。

二、MySQL并发事务访问的问题

我们已经知道事务并发执行时可能带来的各种问题,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据,尤其是一个事务进行读取操作,另一个同时进行改动操作的情况下。

一个事务进行读取操作,另一个进行改动操作,我们前边说过,这种情况下可能发生脏读、不可重复读、幻读的问题。怎么解决脏读、不可重复读、幻读这些问题呢?其实有两种可选的解决方案:

方案一:读操作MVCC,写操作进行加锁

事务利用MVCC进行的读取操作称之为一致性读,或者一致性无锁读,也称之为快照读,但是往往读取的是历史版本数据。所有普通的SELECT语句(plain SELECT)在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读。

一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动。

普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。

  • 在READ COMMITTED 隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象;
  • 在REPEATABLE READ 隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题。

方案二:读、写操作都采用加锁的方式

适用场景:

业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本,

比方在银行存款的事务中,你需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候也就需要对其进行加锁操作,这样也就意味着读操作和写操作也像写-写操作那样排队执行。

小结对比发现:

  • 采用MVCC 方式的话, 读-写操作彼此并不冲突, 性能更高。
  • 采用加锁方式的话, 读-写操作彼此需要排队执行,影响性能。

很明显,采用MVCC方式的话,读-写操作彼此并不冲突,性能更高采用加锁方式的话,读-写操作彼此需要排队执行,影响性能。一般情况下我们当然愿意采用MVCC来解决读-写操作并发执行的问题,但是业务在某些情下,要求必须采用加锁的方式执行。下属的所有内容讲MySQL的锁事:

三、锁的不同角度的分类

1723617449898-2

3.1、从数据操作的类型划分

共享锁(也称为读锁,英文名:Shared Locks,简称S锁):在事务要读取一条记录时,需要先获取该记录的S锁。假如事务E1首先获取了一条记录的S锁之后,事务E2接着也要访问这条记录:如果事务E2想要再获取一个记录的S锁,那么事务E2也会获得该锁,也就意味着事务E1和E2在该记录上同时持有S锁。

排他锁(也称为独占锁,写锁,英文名:Exclusive Locks,简称X锁):在事务要改动一条记录时,需要先获取该记录的X锁。如果事务E2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务E1提交之后将S锁释放掉。如果事务E1首先获取了一条记录的X锁之后,那么不管事务E2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务E1提交。

所以我们说S锁和S锁是兼容的,S锁和X锁是不兼容的,X锁和X锁也是不兼容的,画个表表示一下就是这样:

X 不兼容X 不兼容S

S 不兼容X 兼容S

数据环境准备:

CREATE DATABASE test_db;
USE test_db;

CREATE TABLE products
(
    id    INT AUTO_INCREMENT PRIMARY KEY,
    name  VARCHAR(100),
    price DECIMAL(10, 2)
);

-- 随机插入一些数据
INSERT INTO products (name, price)
VALUES ('Product A', 10.99),
       ('Product B', 20.49),
       ('Product C', 15.75);

演示共享锁(S锁)

-- 事务E1:获取S锁
START TRANSACTION;
SELECT * FROM products WHERE id = 1 LOCK IN SHARE MODE;

image-20240813145620940

-- 事务E2:再次获取S锁
START TRANSACTION;
SELECT * FROM products WHERE id = 1 LOCK IN SHARE MODE;

image-20240813145710122

此时,两个事务都可以读取 id = 1 的记录,因为S锁是互相兼容的。

COMMIT;

image-20240813145852342

image-20240813145902705

演示排他锁(X锁)

接下来,演示排他锁(X锁)。

-- 事务E1:获取X锁
START TRANSACTION;
SELECT * FROM products WHERE id = 1 FOR UPDATE;

image-20240813150019451

-- 事务E2:尝试获取S锁或X锁
START TRANSACTION;
-- 这个操作会被阻塞
SELECT * FROM products WHERE id = 1 LOCK IN SHARE MODE; 
-- 或者:这个操作也会被阻塞
SELECT * FROM products WHERE id = 1 FOR UPDATE; 

image-20240813150157022

当事务E1已经获取了 id = 1 的X锁时,任何其他事务E2尝试获取该记录的S锁或X锁时都会被阻塞,直到事务E1提交或回滚。

提交或回滚事务

当E1事务提交或回滚后,E2事务才能继续进行。

-- 事务E1提交
COMMIT;

image-20240813150312403

image-20240813150340826

-- 事务E2现在可以继续
COMMIT;

image-20240813150400964

3.2、从数据操作的粒度划分:表级锁

MySQL支持多种存储引擎,不同存储引擎对锁的支持也是不一样的。当然,我们重点还是讨论InnoDB存储引擎中的锁,其他的存储引擎只是稍微看看。

InnoDB存储引擎既支持表锁,也支持行锁。表锁实现简单,占用资源较少,不过粒度很粗,有时候你仅仅需要锁住几条记录,但使用表锁的话相当于为表中的所有记录都加锁,所以性能比较差。行锁粒度更细,可以实现更精准的并发控制。下边我们详细看一下。

3.2.1、表级别的S锁与X锁

在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的S锁或者X锁的。在对某个表执行一些诸如ALTER TABLE 、DROP TABLE 这类的DDL 语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL 语句也会发生阻塞。这个过程其实是通过在server层使用一种称之为元数据锁(英文名: Metadata Locks ,简称MDL )结构来实现的。

一般情况下,不会使用InnoDB存储引擎提供的表级别的S锁和X锁。只会在一些特殊情况下,比方说崩溃恢复过程中用到。比如,在系统变量autocommit=0,innodb_table_locks = 1 时, 手动获取InnoDB存储引擎提供的表t 的S锁或者X锁可以这么写:

  • LOCK TABLES t READ :InnoDB存储引擎会对表t 加表级别的S锁。
  • LOCK TABLES t WRITE :InnoDB存储引擎会对表t 加表级别的X锁。

不过尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES 这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁,关于InnoDB表级别的S锁和X锁大家了解一下就可以了。

生活中的实例:

为了更好的理解这个表级别的S锁和X锁和后面的意向锁,我们举一个现实生活中的例子。我们用曾经很火爆的互联网风口项目共享Office来说明加锁:

共享Office有栋大楼,楼自然有很多层。办公室都是共享的,客户可以随便选办公室办公。每层楼可以容纳客户同时办公,每当一个客户进去办公,就相当于在每层的入口处挂了一把S锁,如果很多客户进去办公,相当于每层的入口处挂了很多把S锁(类似行级别的S锁)。

有的时候楼层会进行检修,比方说换地板,换天花板,检查水电啥的,这些维修项目并不能同时开展。如果楼层针对某个项目进行检修,就不允许客户来办公,也不允许其他维修项目进行,此时相当于楼层门口会挂一把X锁(类似行级别的X锁)。

上边提到的这两种锁都是针对楼层而言的,不过有时候我们会有一些特殊的需求:

image-20240814132849149

A、有投资人要来考察Office的环境。

投资人和公司并不想影响客户进去办公,但是此时不能有楼层进行检修,所以可以在大楼门口放置一把S锁(类似表级别的S锁)。此时:

来办公的客户们看到大楼门口有S锁,可以继续进入大楼办公。

修理工看到大楼门口有S锁,则先在大楼门口等着,啥时候投资人走了,把大楼的S锁撤掉再进入大楼维修。

B、公司要和房东谈条件。

此时不允许大楼中有正在办公的楼层,也不允许对楼层进行维修。所以可以在大楼门口放置一把X锁(类似表级别的X锁)。此时:

来办公的客户们看到大楼门口有X锁,则需要在大楼门口等着,啥时候条件谈好,把大楼的X锁撤掉再进入大楼办公。

修理工看到大楼门口有X锁,则先在大楼门口等着,啥时候谈判结束,把大楼的X锁撤掉再进入大楼维修。

3.2.2、意向锁 (intention lock)

但是在上面的例子这里头有两个问题:

如果我们想对大楼整体上S锁,首先需要确保大楼中的没有正在维修的楼层,如果有正在维修的楼层,需要等到维修结束才可以对大楼整体上S锁。

如果我们想对大楼整体上X锁,首先需要确保大楼中的没有办公的楼层以及正在维修的楼层,如果有办公的楼层或者正在维修的楼层,需要等到全部办公的同学都办公离开,以及维修工维修完楼层离开后才可以对大楼整体上X锁。

我们在对大楼整体上锁(表锁)时,怎么知道大楼中有没有楼层已经被上锁(行锁)了呢?依次检查每一楼层门口有没有上锁?那这效率也太慢了吧!于是InnoDB提出了一种意向锁(英文名:Intention Locks):

意向共享锁 ,英文名:Intention Shared Lock,简称IS锁。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。

意向独占锁 ,英文名:Intention Exclusive Lock,简称IX锁。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。

视角回到大楼和楼层上来:

image-20240814133109056

如果有客户到楼层中办公,那么他先在整栋大楼门口放一把IS锁(表级锁),然后再到楼层门口放一把S锁(行锁)。

如果有维修工到楼层中维修,那么它先在整栋大楼门口放一把IX锁(表级锁),然后再到楼层门口放一把X锁(行锁)。

之后:

如果有投资人要参观大楼,也就是想在大楼门口前放S锁(表锁)时,首先要看一下大楼门口有没有IX锁,如果有,意味着有楼层在维修,需要等到维修结束把IX锁撤掉后才可以在整栋大楼上加S锁。

如果有谈条件要占用大楼,也就是想在大楼门口前放X锁(表锁)时,首先要看一下大楼门口有没有IS锁或IX锁,如果有,意味着有楼层在办公或者维修,需要等到客户们办完公以及维修结束把IS锁和IX锁撤掉后才可以在整栋大楼上加X锁。

注意: 客户在大楼门口加IS锁时,是不关心大楼门口是否有IX锁的,维修工在大楼门口加IX锁时,是不关心大楼门口是否有IS锁或者其他IX锁的。IS和IX锁只是为了判断当前时间大楼里有没有被占用的楼层用的,也就是在对大楼加S锁或者X锁时才会用到。

下面我们详细的来讲述意向锁:

概念

意向锁是表锁,为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。

InnoDB 支持多粒度锁(multiple granularity locking) ,它允许行级锁与表级锁共存,而意向锁就是其中的一种表锁。

意向锁分为两种:

  • 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)

    -- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
    SELECT column FROM table ... LOCK IN SHARE MODE;
    
  • 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)

    -- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
    SELECT column FROM table ... FOR UPDATE;
    

即:意向锁是由存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前InooDB 会先获取该数据行所在数据表的对应意向锁。

作用

当有事务A有行锁时,MySQL会自动为该表添加意向锁,事务B如果想申请整个表的写锁,那么不需要遍历每一行判断是否存在行锁,而直接判断是否存在意向锁,增强性能。

为什么意向锁是表级锁

当我们需要加一个排他锁时,需要根据意向锁去判断表中有没有数据行被锁定(行锁);<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值