05 MySQL实战45讲:🔒 全局锁和表锁

173 阅读5分钟

根据加锁的范围,Mysql里面的锁大致可以分为全局锁、表级锁和行锁

1. 全局锁

  • 全局锁就是对整个数据库实例加锁
  • mysql提供了一个加全局读锁的方法,命令是:Flush tables with read lock,当你需要让整个库处于只读状态时,可以使用该命令。以下类型语句会被阻塞
    • 数据库更新语句(数据库的增删改)
    • 数据定义语句(建表,修改表结构)
    • 更新类事务的提交语句
  • 使用场景:
    • 如果不是是所有表使用事务引擎的库使用FTWRL:全库逻辑备份也就是把整库每个表都select出来存成文本
    • 如果是所有表使用事务引擎的库,全库备份可以使用mysqldump --single-transaction,备份之前会启动一个事务,来确保拿到一致性视图,而由于MVCC机制,这个过程的数据是可以正常更新的
  • 使用全局锁备份数据,使得整库只读:
    • 如果是在主库上备份,备份期间都不能执行更新,业务基本停机
    • 如果是在从库上备份,备份期间不能执行主库同步过来的binlog,导致主从延迟
  • 加全局锁的目的是:在备份期间,read view视图是一致的(有点类似可重复隔离级别)
  • 全库只读,还可以通过set global readonly=true的方式,不推荐的原因
    • readonly可能会有其他逻辑,比如判断是主库还是从库,修改全局变量风险较高
    • 在异常处理机制上:如果是FTWRL之后由于客户端发生异常断开,mysql会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly后,如果客户端发生异常,则数据库会一直保持readonly状态,会导致整个库长时间处于不可写状态,风险较高

2. 表级锁

  • Mysql的表级锁分为两种:表锁和元数据锁(meta data lock,MDL)
  • 表锁主要分为读写锁:
    • 表锁的语法是lock tables ... read/write,释放锁是unlock tables,也可以在客户端断开时自动释放。
    • lock tables语法除了限制别的线程读写外,也限定了本线程接下来的操作对象
      • 比如线程t1申请了表t_a的读和t_b的写锁,那么其他线程对于表t_a只能获取读锁,如果要对t_a写或者t_b的读写都会阻塞;并且t1也只能对t_a读操作,以及读写t_b表,对t_a的写操作是不允许的。具体原因可以参考下图 image.png
  • 在还没有更细粒度的锁时,表锁是处理并发的方式,对于Innodb支持行锁的引擎,一般不使用lock tables命令来控制并发,影响面太大。
  • MDL锁不需要显式使用,在访问一个表的时候会自动加上。
    • 作用是:保持读写的正确性
    • 在mysql5.5版本中引入了MDL:对一个表做增删改查时加上MDL读锁,当对表结构变更时加上MDL写锁
  • MDL系统默认会加,但是给小表加字段也有可能导致整个库打挂
    • 原因就是公平的读写锁的获取原则。
    • 比如,线程A获取读锁,一直不释放读锁。线程B获取写锁,锁池等待。线程C获取读锁,因为线程B在获取写锁,所以线程C也会等待。假设在线程A获取了t1的MDL读锁。线程B修改表结构需要获取MDL写锁,线程C查询表中信息需要获取MDL读锁就会被阻塞。
  • 如何安全地给小表加字段:
    • 查看infomation_schema库的innodb_trx表中,可以查到当前执行的事务,如果有长事务,考虑暂停或者kill掉长事务。优化SQL避免长事务,查询语句适当使用事务。
    • 如果是热点表,结合并发编程中的锁模型:在获取锁的时候增加超时时间
      • 修改表结构时添加等待时间
ALTER TABLE t NOWAIT add column
ALTER TABLE t WAITE n add column

3. 行锁

  • 行锁就是针对数据表中行记录的锁

两阶段锁

  • 在Innodb事务中,行锁是在需要的时候才加上的,但并不是不需要就立刻释放,而是要等到事务结束才释放
    • 如果一个事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放

死锁和死锁检测

A deadlock is a situation where different transactions are unable to proceed because each holds a lock that the other needs. Because both transactions are waiting for a resource to become available, neither ever release the locks it holds.

  • 产生死锁的必要条件,只要一个不成立,死锁就不会发生
    • 互斥条件:进程对所分配到的资源进行排他性使用,在一段时间内,某资源只能被一个进程占用;如果此时还有其他进程请求该资源,请求进程只能等待,直至占有该资源的进程使用完释放
    • 请求和保持条件:进程至少已经保持了一个资源,但又提出了新的资源请求,而且该资源已经被其他进程占有,此时请求进程被阻塞。但对自己已经获得的资源保持不放
    • 不可抢占条件:进程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时由自己释放
    • 循环等待条件:在发生死锁时,必然存在一个进程-资源循环链,即:进程集合{P1,P2,...Pn}中的P1等待P2占用的资源,P2等待P3占用的资源,P3等待已经被P1占用的资源。
  • 解决死锁的方式:
    • 设置获取锁的超市时间:通过 innodb_lock_wait_time来设置(默认50s)
    • 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on表示启用这个逻辑
  • CPU利用率很高,每秒执行不了几个事务,可能在进行死锁检测
    • 解决方案:类似java中ConcurrentHashMap中的分段锁,可以考虑把把一行记录分散到不同的记录里,减少热点数据(redis也可以采用此方法设计)