文章目录
- 全局锁
- 表级锁
- 表锁
- 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 再请求的话,这个库的线程很快就会爆满。
解决上述问题,有两个方案:
- 暂停操作:如果长事务在执行,就暂时停止DDL操作。
- 超时时间:alter table 语句里面设定等待时间
行锁
行锁就是针对数据表中行记录的锁。
比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新。
两阶段锁
在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
如下例子
事务B会阻塞,等待id=1行锁的释放。从上面这个例子发现,我们应该把更新频繁、冲突可能性大的放后面,因为越往后,这个被锁的时间就会越短。
死锁和死锁检测
并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
- 超时时间,这个容易造成误伤,时间长度不好控制
- 死锁检测,主动回滚其中一个事务。
正常情况下我们还是要采用第二种策略,即:主动死锁检测,但是也存在问题:
假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。
这样我们就需要解决这个热点行更新的问题,有两个思路:
- 关闭死锁检测,出现超时再重试
- 控制并发度,客户端和中间件。