文章目录

  • 全局锁
  • 表级锁
  • 表锁
  • MDL锁
  • 行锁
  • 两阶段锁
  • 死锁和死锁检测



数据库锁设计的初衷就是处理并发问题,合理的控制资源的访问,根据加锁的范围,可以分为全局锁、表锁、行锁。

全局锁

对整个数据库实例加锁,MySQL可以通过 FTWRL来实现全局读锁,之后更新类的操作都会被阻塞。

Flush tables with read lock

通过加全局读锁然后再进行备份的方式,在主库会引发业务停摆,在从库会引发主从不一致。使用的场景是:不支持可重复读这个隔离级别的引擎全库的逻辑备份。

不使用 set global readonly=true 的方式做全库只读,主要有两个原因:

  • 一般通过这个参数判断是主库还是备库
  • 两者在异常处理机制上有差异,这种方式不会释放全局锁

当使用的存储引擎支持可重复读隔离级别的时候,可以用mysqldump -single-transaction,导数据的时候启动一个事务,确保拿到一致性视图,数据库内所有表必须支持事务。

表级锁

表级锁有两个:表锁和元数据锁(MDL)

表锁
lock tables … read/write
unlock tables

例如:
如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。

MDL锁

MySQL 5.5 版本中引入了 MDL。

  • 当对一个表做增删改查操作的时候,加 MDL 读锁;
  • 当对一个表做结构变更操作的时候,加 MDL 写锁。

不需要显式使用,在访问一个表的时候会被自动加上。

  • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性

事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放,这就造成了下面的问题。

事务A持有MDL读锁。B事务需要读锁,不互斥。C事务需要MDL写锁,读写锁互斥,需要等待,类似D事务后面的查询需要读锁,和事务的写锁互斥,也会等待,这个造成了这个表完全不可读写了。

如果表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新 session 再请求的话,这个库的线程很快就会爆满。

mysql设置全局锁等待时间 mysql 全局读锁_mysql


解决上述问题,有两个方案:

  • 暂停操作:如果长事务在执行,就暂时停止DDL操作。
  • 超时时间:alter table 语句里面设定等待时间

行锁

行锁就是针对数据表中行记录的锁。
比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新。

两阶段锁

在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。

如下例子

mysql设置全局锁等待时间 mysql 全局读锁_数据库_02


事务B会阻塞,等待id=1行锁的释放。从上面这个例子发现,我们应该把更新频繁、冲突可能性大的放后面,因为越往后,这个被锁的时间就会越短。

死锁和死锁检测

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

mysql设置全局锁等待时间 mysql 全局读锁_mysql设置全局锁等待时间_03

  • 超时时间,这个容易造成误伤,时间长度不好控制
  • 死锁检测,主动回滚其中一个事务。

正常情况下我们还是要采用第二种策略,即:主动死锁检测,但是也存在问题:

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

这样我们就需要解决这个热点行更新的问题,有两个思路:

  • 关闭死锁检测,出现超时再重试
  • 控制并发度,客户端和中间件。