【MySQL】锁

1、MySQL 并发事务访问相同记录

1.1、并发过程分析

锁是一个内存中的结构,在事务执行前,本来是没有锁和记录关联的,当事务 T1要对这条记录做改动,就需要生成一个锁结构与之关联

锁-记录

  • trx 信息:这个锁是哪个事务生成的
  • is_waiting:当前事务是否在等待

假设之前没有别的事务为这条记录加锁,当事务T1改动了这条记录后,就生成了一个锁结构与该记录关联,is_waitingfalse,这个场景称为获取锁成功,或者加锁成功

在事务T1提交之前,事务T2也想对该记录做改动,锁结构的is_waiting值为true,表示当前事务需要等待,这个场景就称之为获取锁失败,或者加锁失败

T2加锁失败

事务T1提交后,把生成的锁结构释放,事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁

T2 加锁成功

1.2、并发解决方案

并发引发的问题包括脏读幻读不可重复度

脏读
事务 A在读取数据的时候读取到事务 B 未提交的数据

不可重复读
事务 A 中,第一次读取值为 1,此时事务 A 未结束,事务 B 将数值修改为 2并提交,事务 A再次读取时得到的数据是 2,在同一个事务 A 中两次读取的数据值不一样

幻读
只针对 insert操作
事务 A 按条件读取到 5 条记录,此时事务 A 未结束,事务 B 向表中插入两条符合条件的数据并提交,事务 A 再次读取时得到 7 条记录,在同一个事务中两次读取的数据条数不一样,后一次比前一次多

方式一:读操作利用多版本并发控制(MVCC),写操作进行加锁

MVCC

  • 生成一个ReadView,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建)
  • 查询语句只能到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的
  • 写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突

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

小结

  • 采用MVCC方式的话,读-写操作彼此并不冲突,性能更高
  • 采用加锁方式的话,读-写操作彼此需要排队执行,影响性能
  • 一般采用MVCC来解决读-写操作并发执行的问题,在某些特殊情况下,业务要求必须采用加锁的方式执行

2、锁的分类

锁的分类

2.1、数据操作的类型:读锁、写锁

数据库中并发事务的读-读情况并不会引起什么问题,对于写-写读-写写-读这些情况可能会引起一些问题,MySQL实现一个由两种类型的锁组成的锁系统来解决:

  1. 读锁:也称为共享锁、英文用S表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的
  2. 写锁:也称为排他锁、英文用X表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源

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

X锁S锁
X锁不兼容不兼容
S锁不兼容兼容

2.1.1、锁定读

对读取的记录加 S 锁

SELECT ... LOCK IN SHARE MODE;

SELECT ... FOR SHARE;#(8.0之后)

对读取的记录加X锁

SELECT ... FOR UPDATE;

在 MySQL5.7及以前的版本,select ... for update如果获取不到锁,会一直等到,直到innodb_lock_wait_timeout超时。
在 MySQL8.0之后,SELECT. FOR UPDATESELECT ...FOR SHARE添加NOWAITSKIP LOCKED语法,跳过锁等待,或者跳过锁定

  • NOWAIT会立即报错返回(等不到锁立即返回
  • KIP LOCKED也会立即返回,只是返回的结果中不包含被锁定的行
SELECT. FOR UPDATE NOWAIT

2.1.2、写操作

  • delete
    先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,再执行delete mark操作,可以看成是一个获取X锁锁定读

  • update

    • 情况1: 未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化
      先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作
    • 情况2∶未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化
      先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取×锁锁定读,新插入的记录由INSERT操作提供的隐式锁进行保护
    • 情况3∶修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETEINSERT的规则进行了
  • insert
    一般情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁的结构来保护这条新插入的记录在本事务提交前不被别的事务访问

2.2、数据操作的粒度:表锁、页锁、行锁

2.2.1、表锁

S锁、X 锁

该锁会锁定整张表,它是MySQL中最基本的锁策略,并不依赖于存储引擎,并且表锁是开销最小的策略

一般情况下,不会使用InnoDB存储引擎提供的表级别S锁X锁。只会在一些特殊情况下,比方说崩溃恢复过程中用到

在系统变量autocommit=0,innodb_table_locks = 1时,手动获取InnoDB存储引擎提供的表t 的S锁或者X锁的写法:

# 查看自动提交
mysql> show variables like '%autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

# 查看innodb表锁是否打开
mysql> show variables like '%innodb_table%';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| innodb_table_locks | ON    |
+--------------------+-------+
1 row in set (0.00 sec)

# 临时关闭自动提交
mysql> set @@autocommit=0;
show open tables where in_use > 0; # 查看哪些表被锁了

lock tables student read # 加写锁
lock tables student write;

unlock tables; # 表解锁
意向锁

InnoDB 支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,而 意向锁 就是其中的一种表锁
1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。

2、意向锁是一种不与行级锁冲突表级锁,这一点非常重要。

3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁”

  • 意向共享锁 (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 会先获取该数据行所在数据表的对应意向锁

意向锁要解决的问题

现在有两个事务,分别是T1和T2,其中T2试图在该表级别上应用共享或排它锁,如果没有意向锁存在,那么T2就需要去检查各个页或行是否存在锁;如果存在意向锁,那么此时就会受到由T1控制的表级别意向锁的阻塞。T2在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。简单来说就是给更大一级别的空间示意里面是否已经上过锁

如图:右边加上行级排它锁,左边试图加表级共享锁失败
行级排它锁+表级共享锁

分析:左事务检测右事务持有teacher表的意向排他锁,就可以得知右事务必然持有该表中某些数据行的排他锁,那么左事务对teacher表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排他锁

意向锁之间的兼容互斥性

意向共享锁(lS)意向排他锁(IX)
意向共享锁(IS)兼容兼容
意向排他锁(IX)兼容兼容

意向锁与 表级别S、X锁兼容互斥性

意向共享锁(lS)意向排他锁(IX)
表级共享锁(S)表兼容互斥
表级排他锁(X)表互斥互斥

意向锁总结:

  1. InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
  2. 意向锁之间互不排斥,但除了 IS 与 S 兼容外,意向锁会与 共享锁 / 排他锁 互斥
  3. IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
  4. 意向锁在保证并发性的前提下,实现了行锁和表锁共存满足事务隔离性的要求。
自增锁
元数据锁

2.2.2、行锁

行锁(Row Lock)也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要的注意的是,MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现

优点: 锁定力度小,发生锁冲突概率低,可以实现的并发度高

缺点: 对于锁的开销比大,加锁会比较慢,容易出现死锁

记录锁

LOCK_REC_NOT_GAP
记录锁也就是仅仅把一条记录锁上
如图:仅仅是锁住了id值为 8 的记录,对周围的数据没有影响
记录锁

间隙锁

gap锁的提出仅仅是为了防止插入幻影记录而提出的
LOCK_GAP
不允许别的事务在id值为 8 的记录前边的间隙插入新记录,其实就是id列的值( 3 , 8 )这个区间的新记录是不允许立即插入的

select *from student where id =5 lock in share mode;
select * from student where id =5 for update;

间隙锁

临键锁

Next-Key Locks
锁住某条记录,又阻止其他事务在该记录前边的间隙插入新记录
用区间表示就是:(3,8]

begin;
select * from student where id <= 8 and id > 3 for update;
插入意向锁

LOCK_INSERT_INTENTION
一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了gap锁next-key锁也包含gap锁),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是 InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构 ,表明有事务想在某个间隙插入新记录,但是现在在等待
插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁

2.2.3、页锁

页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。 **页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般

2.3、对待锁的态度:乐观锁、悲观锁

2.3.1、悲观锁

共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现

2.3.2 乐观锁

不采用数据库自身的锁机制,而是通过程序来实现

1.乐观锁的版本号机制

在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功

2. 乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突

2.4、 加锁方式:显式锁、隐式锁

2.5、 全局锁

全局锁就是对整个数据库实例加锁,让整个库处于只读状态

Flush tables with read lock

2.6、 死锁

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。

  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑

3、锁监控

innodb_row_lock

show status like 'innodb_row_lock%';

Innodb_row_lock_current_waits 	#当前正在等待锁定的数量
Innodb_row_lock_time 			#从系统启动到现在锁定总时间长度;(等待总时长)
Innodb_row_lock_time_avg 		#每次等待所花平均时间;(等待平均时长)
Innodb_row_lock_time_max		#从系统启动到现在等待最常的一次所花的时间;
Innodb_row_lock_waits			#系统启动后到现在总共等待的次数;(等待总次数)

查询正在被锁阻塞的sql语句

SELECT * FROM information_schema.INNODB_TRX\G;

查询锁等待情况

SELECT * FROM data_lock_waits\G;
*************************** 1. row ***************************
ENGINE: INNODB
REQUESTING_ENGINE_LOCK_ID: 139750145405624 :7:4:7:
REQUESTING_ENGINE_TRANSACTION_ID: 13845 #被阻塞的事务ID
REQUESTING_THREAD_ID: 72
REQUESTING_EVENT_ID: 26
REQUESTING_OBJECT_INSTANCE_BEGIN: 139747028690608
BLOCKING_ENGINE_LOCK_ID: 139750145406432 :7:4:7:
BLOCKING_ENGINE_TRANSACTION_ID: 13844 #正在执行的事务ID,阻塞了 13845
BLOCKING_THREAD_ID: 71
BLOCKING_EVENT_ID: 24
BLOCKING_OBJECT_INSTANCE_BEGIN: 139747028813248
1 row in set (0.00 sec)

查询锁的情况

SELECT * from performance_schema.data_locks\G;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值