MySQL锁

MySQL锁

锁机制:数据库为了保证 共享资源被并发访问时 数据的安全性和一致性,所设计的一种规则

一、锁的内存结构

加锁的本质,就是在内存中创建一个 锁结构 与之关联。InnoDB 存储引擎中的 锁结构 如下图所示:

在这里插入图片描述

那么是不是对多条记录加锁,就要创建多个 锁结构 呢?

答案当然是否,不然一个事务要获取10000条记录的锁,生成10000个锁结构也太崩溃了!

所以决定在对不同记录加锁时,如果符合下边这些条件的记录,会放到同一个锁结构中。

  • 在同一个事务中进行的加锁操作
  • 加锁的记录在同一个页面中。
  • 加锁的类型是一样的。
  • 锁等待状态是一样的

1、锁所在的事务信息

不论是 表锁 还是 行锁 ,都是在事务执行过程中生成的,哪个事务生成了这个锁结构 ,这里就记录这个事务的信息。

锁所在的事务信息 在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比如事务id等。

2、索引信息

对于 行锁 来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针

3、表锁结构 & 行锁结构

表锁结构 和 行锁结构 在这个位置的内容是不同的

  • 表锁结构

    • 对哪个表加的锁

    • 其他的一些信息

  • 行锁结构

    • Space ID :记录所在的表空间

    • Page Number :记录所在的页号

    • n_bits :哪一条记录加了锁(为此在行锁结构的末尾放置了一堆比特位)

      对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁

      n_bits的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后也不至于重新分配锁结构。

4、type_mode

这是一个32位的数,被分成了 lock_modelock_typerec_lock_type 三个部分,如图所示:

在这里插入图片描述

1)lock_mode

锁的模式( lock_mode ),占用低4位,可选的值如下:

  • LOCK_IS (十进制的 0 ):表示共享意向锁,也就是 IS锁
  • LOCK_IX (十进制的 1 ):表示独占意向锁,也就是 IX锁
  • LOCK_S (十进制的 2 ):表示共享锁,也就是 S锁
  • LOCK_X (十进制的 3 ):表示独占锁,也就是 X锁
  • LOCK_AUTO_INC (十进制的 4 ):表示自增锁,也就是 AUTO-INC锁 。

在InnoDB存储引擎中,IS锁、IX锁、AUTO-INC锁 都算是表级锁的模式;S锁、X锁 既可以算是表级锁的模式,也可以是行级锁的模式。

2)lock_type

锁的类型( lock_type ),占用低5~8位,不过现阶段只有第5位和第6位被使用:

  • LOCK_TABLE (十进制的 16 ),也就是当第5个比特位置为1时,表示表级锁
  • LOCK_REC (十进制的 32 ),也就是当第6个比特位置为1时,表示行级锁

3)rec_lock_type

行锁的具体类型( rec_lock_type ),使用其余的位来表示。

只有在 lock_type = LOCK_REC 时,也就是只有在锁的类型是行级锁时,才会被细分为更多的类型:

  • LOCK_ORDINARY (十进制的 0 ):表示 next-key锁
  • LOCK_GAP (十进制的 512 ):也就是当第10个比特位置为1时,表示 间隙锁
  • LOCK_REC_NOT_GAP (十进制的 1024 ):也就是当第11个比特位置为1时,表示正经 记录 锁 。
  • LOCK_INSERT_INTENTION (十进制的 2048 ):也就是当第12个比特位置为1时,表示插入意向锁
  • 其他的类型:还有一些不常用的类型我们就不多说了。

4)is_waiting

基于内存空间的节省,所以把 is_waiting 属性 放到了 type_mode 的低9位

  • LOCK_WAIT (十进制的 256 )
    • 当第9个比特位置为 1 时,表示 is_waitingtrue ,也就是当前事务尚未获取到锁,处在等待状态;
    • 当第9个比特位置为 0 时,表示 is_waitingfalse ,也就是当前事务获取锁成功

5、其他信息

为了更好的管理系统运行过程中生成的各种锁结构,设计了各种哈希表链表

6、一堆比特位

如果是 行锁结构 的话,在该结构末尾还放置了一堆比特位,比特位的数量是由 行锁结构 的 n_bits 属性表示的。

InnoDB 数据页中的每条记录 在 记录头信息 中都包含一个 heap_no 属性

  • 伪记录 Infimumheap_no 值为 0Supremumheap_no 值为 1
  • 之后每插入一条记录, heap_no 值就 +1

锁结构 最后的一堆比特位 就对应着 一个页面中的记录,一个比特位 映射一个 heap_no ,即 一个比特位 映射到 页内的一条记录

二、读锁 & 写锁

对数据操作的类型 可以分为 读锁 写锁

  • 读锁,也叫 共享锁S锁。一个事务加了读锁,其他事务依然能获取该数据的读锁,但是无法获取该数据的写锁。
  • 写锁,也叫 排他锁X锁,一个事务加了写锁,其他事务就不能再获取该数据的其他锁(包括读锁和写锁)
读锁写锁
读锁兼容冲突
写锁冲突冲突

三、表锁 & 行锁 & 页锁

对数据操作的粒度 可以分为 表锁 行锁页锁

锁类型锁定的数据粒度开销锁冲突并发度
表锁整个表概率高
行锁操作行概率低
页锁整个页两者之间两者之间可能会死锁一般

MySQL中,不同的存储引擎支持不同的锁机制:

存储引擎表锁行锁
InnoDB✓(默认)
MyISAM×
Memory×
  • InnoDB默认是行锁更新操作会自动加写锁查询操作不会自动加任何锁,可以手动加锁。
  • MyISAM默认是表锁更新操作会自动加写锁查询操作会自动加读锁。(MyISAM的锁调度是写锁优先

1、表锁

MyISAM默认是表锁更新操作会自动加写锁查询操作会自动加读锁。(MyISAM的锁调度是写锁优先

InnoDB也支持表锁,但是一般不会使用,只会在一些特殊情况下,比方说崩溃恢复过程中用到。

InnoDB在系统变量 autocommit=0,innodb_table_locks=1 时,可以手动获取表级别S锁X锁

lock table tableName read;		# 表级别的S锁
lock table tableName write;		# 表级别的X锁
unlock tables;					# 释放全部锁

1)意向锁 (Intention Lock)

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

  • 意向锁的存在是为了协调行锁表锁的关系,实现了 行锁与表锁共存满足事务隔离性 的要求。
  • 意向锁读写锁表级锁,不会和行级别读写锁发生冲突,只会和表级别读写锁发生冲突。
    • 因此,意向锁 并不会影响到 对 不同数据行 加锁 时的并发性。
  • 意向锁会告诉 其他事务 已经有人锁定了表中的某些记录。

意向锁的分类

  • 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁
    • 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
  • 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁
    • 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。

意向锁的作用

现在有两个事务,分别是T1和T2,其中T2试图在表级别上应用 共享锁 排它锁

  • 如果不存在意向锁,那么T2就需要检查各个页或行是否存在锁;
  • 如果存在意向锁,那么T2在锁定该表前,只需要检查表上的意向锁,而不必检查各个页或行是否存在锁

简单来说就是给更大一级别的空间示意里面是否已经上过锁。

意向锁的使用

意向锁由 存储引擎 自己维护,用户无法手动操作。

在为 数据行读写锁之前,InooDB自动给该数据行所在的更大一级的空间,比如数据页或数据表加上对应的意向锁

  • 事务想要获得数据表中某些行记录的共享锁,就需要在 数据表 上添加 意向共享锁
  • 事务想要获得数据表中某些行记录的排他锁,就需要在 数据表 上添加 意向排他锁

这样,当有其他事务想要获取数据表的锁时,意向锁会告诉其他事务已经有人锁定了表中的某些记录。

2)自增锁(AUTO-INC Lock)

我们可以为表的某个列添加 AUTO_INCREMENT 属性,如果在插入语句中没有给该列显示赋值,系统会自动为它赋上递增的值

CREATE TABLE `teacher` ( 
    `id` int NOT NULL AUTO_INCREMENT, 
    `name` varchar(255) NOT NULL, 
    PRIMARY KEY (`id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

所有插入数据的方式总共分为三类:

  1. Simple inserts(简单插入)

    预先确定要插入的行数。例如:INSERT INTO ... VALUES() 语句

  2. Bulk inserts(批量插入)

    预先不知道要插入的行数。例如: INSERT ... SELECTREPLACE ... SELECTLOAD DATA 语句。

    InnoDB 每处理一行,为 AUTO_INCREMENT 列分配一个新值。

  3. Mixed-mode inserts(混合模式插入)

    • 是 Simple inserts 语句,但是只指定了部分新行的自动递增值。

      例如: INSERT INTO xxx (id,name) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');

    • 存在则更新,不存在则新增

      例如: INSERT ... ON DUPLICATE KEY UPDATE

自增锁 AUTO-INC :向含有AUTO_INCREMENT列的表中插入数据时,需要获取的一种特殊的表级锁。

  1. 在执行插入语句时,就在表级别加一个AUTO-INC锁
  2. 然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值
  3. 在该插入语句执行结束后,再把AUTO-INC锁释放掉。

事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。但是当我们向一个含有 AUTO_INCREMENT关键字的主键插入值的时候,每条语句都要对这个表锁进行竞争,这样的并发潜力其实是很低下的

InnoDB通过 innodb_autoinc_lock_mode 的不同取值来提供不同的锁定机制,来显著提高SQL语句的可伸缩性和性能。

  1. innodb_autoinc_lock_mode = 0(“传统”锁定模式)

    每当执行insert的时候,都会得到一个表级锁 AUTO-INC,保证语句中生成的auto_increment为顺序的,且在binlog中重放的时候,可以保证 masterslave中数据的auto_increment是相同的。

    因为是表级锁,当在同一时间多个事务中执行insert的时候,对于AUTO-INC锁的争夺会 限制并发能力

  2. innodb_autoinc_lock_mode = 1(“连续”锁定模式)

    Bulk inserts 仍然使用 表级锁 AUTO-INC,并保持到语句结束,同一时刻只有一个语句可以持有AUTO-INC锁

    Simple inserts 则通过在 mutex轻量锁 的控制下 获得所需数量的自动递增值 来避免AUTO-INC表级锁。它只在分配过程的持续时间内保持,而不是直到语句完成。

  3. innodb_autoinc_lock_mode = 2(“交错”锁定模式)

    在此锁定模式下,自动递增值 保证 在所有并发执行的所有类型的insert语句中是 唯一且单调递增 的。但是,由于多个语句可以同时生成数字(即,跨语句交叉编号), 为任何给定语句插入的行生成的值可能 不是连续的。

在 MySQL 8.0 之前,默认使用连续锁定模式;从 MySQL 8.0 开始,默认使用交错锁定模式

3)元数据锁(MDL Lock)

MDL(MetaData Lock)的作用是,在读写时发生表结构的变更,也能保证读写的正确性。

在MySQL 5.5版本中引入了MDL:

  • 对一个表进行 增删改查 操作的时候,自动加 MDL读锁
  • 对一个表进行 结构变更 操作的时候,自动加 MDL写锁

MDL锁通过以下机制,保证表结构变更时 读写的正确性:

  • 读锁 和 读锁 之间不互斥,因此可以有多个线程同时对一张表增删改查。
  • 读锁 和 写锁之间、写锁 和 写锁之间是互斥的,因此表结构变更操作只能在没有任何MDL锁的时候执行。

在这里插入图片描述

执行流程如下:

  1. session A先启动,这时候会对表t加一个MDL读锁
  2. session B需要的也是MDL读锁,读锁之间不互斥,因此可以正常执行。
  3. session C需要MDL写锁,而session A的MDL读锁还没有释放,因此只能被阻塞。
  4. 之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。

所有对表的增删改查操作都需要先申请MDL读锁,就都被锁住,等于这个表现在完全不可读写了。

那么如何安全的给表加字段呢?

  1. MySQL的 information_schema 库的 innodb_trx 表中,可以查到当前执行中的事务。

    如果你要做DDL变更的表刚好有长事务在执行,要考虑先暂停DDL,或者kill掉这个长事务。

  2. 如果要变更的表是一个热点表,这时候kill可能未必管用,因为新的请求马上就来了。

    这时可以在alter table语句里面设定等待时间,如果在等待时间内拿不到MDL写锁,就先放弃,之后再重试。

# MariaDB已经合并了AliSQL的这个功能,所以这两个开源分支目前都支持DDL NOWAIT/WAIT n这个语法。
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ... 

2、行锁

只有InnoDB存储引擎支持行锁。可以通过一下命令手动 获取 和 释放 行级别S锁X锁

select * from tableName where ... lock in share mode; 	# 行级别的S锁
select * from tableName where ... for update;			# 行级别的X锁			
commit;													# 提交事务即解锁

1)记录锁(Record Locks)

记录锁也就是仅仅把一条记录锁上,对周围的数据没有影响。官方的类型名称为 LOCK_REC_NOT_GAP

在这里插入图片描述

记录锁是有S锁和X锁之分的,称之为 S型记录锁 和 X型记录锁 。

  • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁
  • 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁

2)间隙锁(Gap Locks)

顾名思义,间隙锁,锁的就是两个值之间的空隙。例如下列的insert语句,插入了6个记录,这就产生了7个间隙。

insert into t values (0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

在这里插入图片描述

数据行是可以加上锁的实体,数据行之间的间隙,也是可以加上锁的实体。当前读在一行行扫描的过程中,不仅给行加上了行锁,还给行两边的空隙,也加上了间隙锁

注意:间隙锁在可重复读级别下才会生效。所以,如果把隔离级别设置为读已提交的话,就没有间隙锁了。

1、行锁行锁之间是存在冲突的。

SessionASessionB
T1begin;
select * from t where d=5 for update; # 行锁
T2begin;
select * from t where d=5 for update; # 会阻塞
update t set c=6 where d=5; # 会阻塞

2、间隙锁间隙锁之间是不存在冲突关系的。

SessionASessionB
T1begin;
select * from t where d=7 for update; # 间隙锁 (5, 10)
T2begin;
select * from t where d=7 for update; # 不会阻塞
  • 因为表t里并没有c=7这个记录,因此session A加的是间隙锁 (5,10)
  • session B 加的也是间隙锁 (5,10),但是它们之间是不冲突的。

3、跟间隙锁存在冲突关系的,是 往这个间隙中插入一个记录 这个操作。

SessionASessionB
T1begin;
select * from t where d=4 for update; # 间隙锁 (0, 5)
T2begin;
insert into t values (2,2,2); # 会阻塞
insert into t values (7,7,7); # 不会阻塞

4、间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的,还有可能会导致死锁

在这里插入图片描述

两个间隙锁形成了死锁,按语句执行顺序来分析一下:

  1. session A 执行select … for update语句,由于id=9这一行并不存在,因此会加上间隙锁 (5,10);
  2. session B 执行select … for update语句,同样会加上间隙锁 (5,10),间隙锁之间不会冲突,因此执行成功;
  3. session B 试图插入一行 (9,9,9),被session A的间隙锁挡住了,只好进入等待;
  4. session A试图插入一行 (9,9,9),被session B的间隙锁挡住了。

至此,两个session进入互相等待状态,形成死锁。InnoDB的死锁检测发现了这对死锁关系,让session A的insert语句报错返回了。

3)临键锁(Next-Key Locks)

间隙锁行锁 合称 next-key lock。(Gap Locks开区间next-key lock左开右闭区间

# 锁住整张表
select * from t for update;

# 形成了7个next-key lock
(-,0](0,5](5,10](10,15](15,20](20, 25](25, +suprenum]

因为+∞是开区间,实现上,为了满足左开右闭区间的规则,InnoDB给每个索引加了一个不存在的最大值suprenum

4)插入意向锁(Insert Intention Lock)

一个事务在执行 INSERT 操作时,如果即将插入的间隙已经被其他事务加了间隙锁,那么本次 INSERT 操作会阻塞。而 InnoDB
规定:事务在等待的时候,也需要在内存中生成一个锁结构,所以此时会在该间隙上加一个插入意向锁

插入意向锁 Insert Intention Locks是一种特殊的间隙锁 ,不是意向锁,是在插入一条记录行前由 INSERT 操作产生的

插入意向锁之间互不排斥,即使多个事务在同一区间插入多条记录,只要记录本身(主键、唯一索引)不冲突,事务之间就不会冲突

在这里插入图片描述

从图中可以看到:

  • 由于T1持有间隙锁,所以T2和T3需要生成一个插入意向锁的锁结构并且处于等待状态(is_waiting=true
  • T1提交后会释放间隙锁,这样T2和T3就能获取到对应的插入意向锁了 (即插入意向锁对应锁结构的is_waiting=false
  • T2和T3之间不会相互阻塞,它们可以同时获取到id值为8的插入意向锁,然后执行插入操作

所以,插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。

3、页锁

页锁就是在 页的粒度 上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。

  • 页锁的开销介于表锁和行锁之间,会出现死锁
  • 页锁的粒度介于表锁和行锁之间,并发度一般

当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。

每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量超过了这个层级的阈值时,就会进行 锁升级 (就是用更大粒度的锁替代多个更小粒度的锁),比如InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。

4、两阶段锁协议

两阶段锁协议加锁解锁 分为两个阶段进行

  • InnoDB事务中,行锁在需要的时候加上,但不是立即释放,而是要等事务结束时才释放

  • 如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。

5、无索引,行锁升级为表锁

  • 使用无索引的列,行锁升级为表锁(因为没有索引,不知道锁定哪一行,就锁定整张表)
  • 如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样。

6、行锁优化建议

InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面带来的性能损耗可能比表锁会更高一些,但在整体并发处理能力方面要远远优于MyISAM存储引擎的表锁。当系统并发量较高的时候,InnoDB性能较高。

但是,InnoDB的行级锁同样也有其脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差。

优化建议:

  • 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁。【表锁的并发度低】

  • 合理设计索引,尽量缩小锁的范围。【提高并发度】

  • 尽可能减少索引条件,及索引范围,避免间隙锁。【并发度低,容易出现异常】

  • 尽量控制事务大小,减少锁定资源量和时间长度。【事务大,加锁时间长,并发度低】

  • 在满足业务层面需求的情况下,尽可使用低级别的事务隔离。

四、显示锁 & 隐式锁

加锁的方式 可以分为 显示锁隐式锁

1、显示锁

显式锁就是通过特定的语句显示加锁。

# 手动加表锁
lock table tableName read;		# 表级别的S锁
lock table tableName write;		# 表级别的X锁
unlock tables;					# 释放全部锁

# 手动加行锁
select * from tableName where ... lock in share mode; 	# 行级别的S锁
select * from tableName where ... for update;			# 行级别的X锁			
commit;													# 提交事务即解锁

2、隐式锁

隐式锁是一种 延迟加锁 的机制,从而来减少加锁的数量。

一个事务对新插入的记录,可以不显示的加锁,但是由于 事务id 的存在,相当于加了一个隐式锁。别的事务在对这条记录加 S锁X锁 时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构并进入等待状态。

实际内存对象中,并不含有隐式锁的相关信息。只有产生锁等待时,才会将隐式锁转化为显式锁,此时才可以通过命令查看锁信息:

# 查看锁信息
SELECT * FROM performance_schema.data_lock_waits;

隐式锁的两种场景(聚簇索引 & 二级索引)

对于聚簇索引来说:

  1. 每条记录中,都一个隐含的 DB_TRX_ID 字段,记录着最后改动该记录的 事务id
  2. 其他事务想对该记录添加 S锁X锁 时,会先看一下该记录的 DB_TRX_ID 检查该事务是否是活跃的事务(未提交或回滚)
    • 如果是活跃的事务,就为该事务添加一个 X锁is_waiting = false,表示获取锁成功)
  3. 检查是否有锁冲突(当前读 可能获取 S锁X锁更新操作 要获取 X锁
    • 如果有冲突,为自己也创建一个锁结构( is_waiting = true,表示尚未获取到锁),进入等待状态
    • 如果没有冲突,不加锁,直接进行第5步。
  4. 等待加锁成功,被唤醒,或者超时。
  5. 写数据,并将自己的 DB_TRX_ID 写入 DB_TRX_ID 字段。

对于二级索引来说:

  1. 本身并没有 DB_TRX_ID 隐藏列,但是在二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 属性
    • 该属性代表对该页面做改动的最大的事务id
  2. 如果 PAGE_MAX_TRX_ID < 当前最小的活跃事务id ,说明对该页面做修改的事务都已经提交了
  3. 否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复 聚簇索引 的做法。

五、乐观锁 & 悲观锁

对待数据并发的态度 可以分为 乐观锁悲观锁(乐观锁和悲观锁并不是实际意义上的锁,而是锁的设计思想

1、悲观锁(Pessimistic Locking)

悲观锁就是认为数据一定会被其他事务修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞, 用完后再把资源转让给其它线程),从而保证数据操作的排它性。

比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。

Java 中 synchronizedReentrantLock 等独占锁就是 悲观锁思想 的实现。

2、乐观锁(Optimistic Locking)

乐观锁认为对同一数据的并发操作不会总发生,不用每次都对数据上锁,但是在更新的时候会判断一下,在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过 程序 来判断当前拿到的数据是否最新。。

在程序上,我们可以采用 版本号机制时间戳机制CAS机制 实现。

Java 中 java.util.concurrent.atomic 包下的 原子变量类 就是使用了乐观锁 的一种实现方式:CAS实现的。

乐观锁的版本号机制

在表中设计一个 版本字段 version

  • 第一次读的时候,会获取 version 字段的取值。
  • 对数据进行 更新 或 删除 操作时,会执行 UPDATE x SET version=version+1 WHERE version=version
  • 如果已经有事务对这条数据进行了更改,修改就不会成功。

乐观锁的时间戳机制

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

可以看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或 者时间戳),从而证明当前拿到的数据是否最新。

3、两种锁的适用场景

从这两种锁的设计思想中,我们总结一下乐观锁和悲观锁的适用场景:

  • 乐观锁:
    • 适合 读操作多 的场景,可以提高吞吐量。
    • 优点在于通过程序实现, 不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
  • 悲观锁:
    • 适合 写操作多 的场景,因为写的操作具有 排它性
    • 采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读-写写-写 的冲突。

六、全局锁

全局锁 就是 对整个数据库实例加锁。

  • 加锁影响:阻塞 数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
  • 使用场景:需要让整个库处于’只读状态’的时候,可以使用这个命令。(例如:做全库逻辑备份)
# 加锁命令(FTWRL)
Flush tables with read lock;

既然要全库只读,为什么不使用 set global readonly = true 的方式,而是使用全局锁的方式呢?

  1. 在有些系统中,readonly的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。
  2. 在异常处理机制上有差异。
    • 执行FTWRL命令之后,如果客户端发生异常断开,那么MySQL会自动释放这个全局锁。
    • 将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态。

七、死锁

1、什么是死锁?

死锁:并发系统中,不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源,导致这几个线程都进入无限等待的状态。

在这里插入图片描述

上图的例子中,事务A在等待事务B释放id=2的行锁,而事务B在等待事务A释放id=1的行锁,互相等待,进入了死锁状态。

2、超时时间 vs 死锁检测

通过参数 innodb_lock_wait_timeout ,可以设置锁等待超时时间。(默认是50s)

设置参数 innodb_deadlock_detect = on开启死锁检测:(默认就是on)

  • 发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。(回滚持有最少行级排他锁的事务)

选择 设置超时时间 还是 开启死锁检测 ?

设置超时时间

  • 默认超时时间是50s,对于在线服务来说,这个等待时间往往无法接受。
  • 如果时间设置的很短,那么就会误伤到一些简单的锁等待。

因此,一般采用死锁检测的策略。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的。

死锁检测的弊端?

死锁检测的过程:

每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。

热点行更新导致的性能问题:

每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度 O(n) 的操作。

假设有1000个并发线程要同时更新同一行,那么死锁检测操作就是100万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的CPU资源。

因此,你就会看到CPU利用率很高,但是每秒却执行不了几个事务。

怎么解决 热点行更新 导致的性能问题呢?(高并发情况下,死锁检测要耗费大量的CPU资源)

1、关闭死锁检测

  • 确保业务一定不会出现死锁,可以临时把死锁检测关掉。(风险比较大)

2、控制并发度

  • 客户端 做并发控制(不可行)

    假设客户端很多,即使每个客户端并发线程都很少,汇总到数据库服务端以后,峰值并发数也可能达到很高。

  • 数据库服务端 做并发控制

    如果你有中间件,可以考虑在中间件实现;如果有能修改MySQL源码的人,也可以做在MySQL里面。

    基本思路:对于相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。

3、从设计上做优化

  • 将一行改成逻辑上的多行来减少锁冲突。

    以影院账户为例,可以考虑放在多条记录上,影院的账户总额等于这多条记录的值的总和。每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的1/n,可以减少锁等待个数,也就减少了死锁检测的CPU消耗。

    这个方案看上去是无损的,但其实这类方案需要根据业务逻辑做详细设计。如果账户余额可能会减少,比如退票逻辑,那么这时候就需要考虑当一部分行记录变成0的时候,代码要有特殊处理。

3、怎么看待死锁?

出现死锁后,执行 show engine innodb status 命令会输出很多信息,有一节LATESTDETECTED DEADLOCK,就是记录的最后一次死锁信息。

在这里插入图片描述

我们来看看这图中的几个关键信息。

  • (1) TRANSACTION,是第一个事务的信息;
  • (2) TRANSACTION,是第二个事务的信息;
  • WE ROLL BACK TRANSACTION (1),是最终的处理结果,表示回滚了第一个事务。

第一个事务的信息中:

  • (1) WAITING FOR THIS LOCK TO BE GRANTED,表示的是这个事务在等待的锁信息;

  • index c of table test.t,说明在等的是表t的索引c上面的锁;

  • lock mode S waiting,表示这个语句要自己加一个读锁,当前的状态是等待中;

  • Record lock,说明这是一个记录锁;

  • n_fields 2,表示这个记录是两列,也就是字段c和主键字段id;

  • 0: len 4; hex 0000000a; asc ;;是第一个字段,也就是c。值是十六进制a,也就是10;

    1: len 4; hex 0000000a; asc ;;是第二个字段,也就是主键id,值也是10;

    这两行的asc表示的是,接下来要打印出值里面的“可打印字符”,但10不是可打印字符,因此就显示空格。

  • 第一个事务信息就只显示出了等锁的状态,在等待(c=10,id=10)这一行的锁。

  • 既然出现死锁了,就表示这个事务也占有别的锁,但是没有显示出来。可以从第二个事务的信息中推导出来。

第二个事务显示的信息要多一些:

  • (2) HOLDS THE LOCK(S) ,用来显示这个事务持有哪些锁;
  • index c of table test.t,表示锁是在表t的索引c上;
  • hex 0000000ahex 00000014表示这个事务持有c=10和c=20这两个记录锁;
  • (2) WAITING FOR THIS LOCK TO BE GRANTED,表示在等(c=5,id=5)这个记录锁。

从上面这些信息中,我们就知道:

  1. “lock in share mode”的这条语句,持有c=5的记录锁,在等c=10的锁;
  2. “for update”这个语句,持有c=20和c=10的记录锁,在等c=5的记录锁。

因此导致了死锁。这里,我们可以得到两个结论:

  1. 由于锁是一个个加的,要避免死锁,对同一组资源,要按照尽量相同的顺序访问;

  2. 在发生死锁的时刻,for update 这条语句占有的资源更多,回滚成本更大,

    所以InnoDB选择了回滚成本更小的 lock in share mode 语句,来回滚。

八、锁监控与排查

1、InnoDB_row_lock 等状态变量

通过检查 InnoDB_row_lock 等状态变量来分析系统上的行锁的争夺情况

mysql> show status like 'innodb_row_lock%'; 
+-------------------------------+-------+
| Variable_name 				| Value | 
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 	| 
| Innodb_row_lock_time 			| 0 	| 
| Innodb_row_lock_time_avg 		| 0 	| 
| Innodb_row_lock_time_max 		| 0 	| 
| Innodb_row_lock_waits 		| 0 	| 
+-------------------------------+-------+
5 rows in set (0.01 sec)

对各个状态量的说明如下:

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

2、其他监控方法

MySQL5.7及之前,MySQL把事务和锁的信息记录在了 information_schema 库中:

  • 涉及INNODB_TRXINNODB_LOCKSINNODB_LOCK_WAITS 这三张表
# 查询正在被锁阻塞的sql语句
SELECT * FROM information_schema.INNODB_TRX;

# 查看事务的锁情况(只能看到阻塞事务的锁;如果事务并未被阻塞,则在该表中看不到该事务的锁情况)
SELECT * FROM information_schema.INNODB_LOCKS;

# 查看锁等待情况
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

MySQL8.0

  • information_schema.INNODB_TRX 仍然在使用
  • information_schema.INNODB_LOCKSperformance_schema.DATA_LOCKS 代替
  • information_schema.INNODB_LOCK_WAITSperformance_schema.DATA_LOCKS_WAITS 代替。
# 查询正在被锁阻塞的sql语句
SELECT * FROM information_schema.INNODB__TRX;

# 查看事务的锁情况(不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁)
SELECT * from performance_schema.DATA_LOCKS;

# 查看锁等待情况
SELECT * from performance_schema.DATA_LOCKS_WAITS;

3、其他的命令

# 查询锁等待情况
select * from data_lock_waits;

# 查询是否锁表 
show open tables where In_use > 0;

# 直接找出造成阻塞的process id
select blocking_pid from sys.schema_table_lock_waits;

# 查询前100条进程(可以获取进程id)
show processlist;

# 查询所有进程
show full processlist;

# 杀死进程
kill id;
  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scj1022

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值