计算机系统应用教程网站

网站首页 > 技术文章 正文

深入理解mysql锁机制--全局锁,表级锁,行锁

btikc 2024-10-08 01:16:32 技术文章 8 ℃ 0 评论

概述

根据加锁的范围,MySQL 里面的锁大致可以分成全局锁,表级锁,行锁。


1、全局锁

全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。

当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。

1.1 全局锁使用场景

全局锁的典型使用场景是,做全库逻辑备份(mysqldump)。重新做主从时候

也就是把整库每个表都 select 出来存成文本。

以前有一种做法,是通过 FTWRL 确保不会有其他线程对数据库做更新,然后对整个库做备份。注意,在备份过程中整个库完全处于只读状态。

数据库只读状态的危险性:

如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就能停止。如果你在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟。

注:上面逻辑备份,是不加--single-transaction参数

看来加全局锁不太好。但是细想一下,备份为什么要加锁呢?来看一下不加锁会有什么问题?

1.2 不加锁产生的问题

比如手机卡,购买套餐信息

这里分为两张表 u_acount (用于余额表),u_pricing (资费套餐表)

步骤:

1 . u_account 表中数据 用户A 余额:300

u_pricing 表中数据 用户A 套餐:空

2. 发起备份,备份过程中先备份u_account表,备份完了这个表,这个时候u_account 用户余额是300

3. 这个时候套用户购买了一个资费套餐100,餐购买完成,写入到u_print套餐表购买成功,备份期间的数据。

4. 备份完成

可以看到备份的结果是,u_account 表中的数据没有变, u_pricing 表中的数据已经购买了资费套餐100。也就是说,不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个数据是逻辑不一致的。

1.3 为什么需要全局读锁(FTWRL)

可能有的人在疑惑,官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数--single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性快照视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。

为什么还需要 FTWRL 呢?

一致性读是好,但前提是引擎要支持这个隔离级别。比如,对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用FTWRL 命令了。

所以,single-transaction 方法只适用于所有的表使用事务引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。

1.4 全局锁两种方法

一.FLUSH TABLES WRITE READ LOCK

二.set global readonly=true

既然要全库只读,为什么不使用 set global readonly=true 的方式呢?确实 readonly 方式也可以让全库进入只读状态,但我还是会建议你用 FTWRL 方式,主要有几个原因:

一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。

二是,在异常处理机制上有差异。如果执行FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。

三是,readonly 对super用户权限无效

注 :业务的更新不只是增删改数据(DML),还有可能是加字段等修改表结构的操作(DDL)。不论是哪种方法,一个库被全局锁上以后,你要对里面任何一个表做加字段操作,都是会被锁住的。

即使没有被全局锁住,加字段也不是就能一帆风顺的,还有表级锁了


2、表级锁

MySQL 中表级别锁有两种:一种是普通表锁,一种是元数据锁(metadata lock. MDL)

表锁的语法是 lock tables xxx read/write 同样使用 unlock tables 来释放锁。通过加读锁我们可以限制其他语句进行写入,但是重复加读锁不受影响。但是当我们加写锁的时候,既不可以读也不可以写。同样在使用 unlock tables 之后可以解除锁定。

另外一种表级锁是 MDL 锁(metadata lock) MDL 锁不需要显示的使用,在访问一个表的时候自动就被加上了。 MDL 锁是用来保证读写正确性的,当我们对一个表在做 增删改查操作的时候都会被加上 MDL 读锁。当要进行 ddl 的时候需要加 MDL 写锁。

MDL 读锁与读锁之间不互斥,因此我们可以多个线程进程对一个表进行增删改查。

MDL 读写锁之间互斥,用来保证表结构变更的安全性。因此如果有两个线程同时要给同一个表加字段,其中一个要等另外一个执行完成之后再开始执行。

下面我们来看一个比较有代表性的场景 MDL 读锁写锁互斥导致表无法读写被死锁。

  • session A: 开始一个事务,然后查询 t 表,这会给 t 表加上 MDL 读锁。(注意该事务被打开后就一直没有结束)
  • session B: 查询一个 t 表。这里应该是 autoocommit 会自动成功。
  • session C: 修改表 ddl 会加 MDL 写锁,和 session A 的读锁互斥。这个时候就锁住了表。
  • session D: 由于 session C 造成了写锁阻塞,所以后面所有的请求都会被锁住。

如果该表查询频繁,而且客户端有重试的机制,那么这个数据库的查询线程会很快被打满。

可能在进行 web 开发的同学会经常遇到类似的情况。比如我在 ipython 里面打开了一个数据库某个表的连接,然后我一直没有 commit 。就可能造成该表在加写锁的时候阻塞后面所有的操作。

这种事情非常常见。

那么我们如何安全的给小表加字段,首先我们应该解决长事务或者脚本事务的问题,因为他们会一直挂读锁不结束。在 MySQL 的 information_schema 中的 innodb_trx 中可以查询到执行中的长事务,但是比较麻烦的是这个看不到很短的事务。但是往往进行 sleep 的短事务也可能因为一直没有 commit 而导致上面的情况出现。

这个时候就需要把对应表的 sleep 进程 kill 掉使其恢复正常。


3、行级锁

先来看个描述两阶段锁的例子:

事务 A 会持有两条记录的行锁,并且只会在 commit 之后才会释放。

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

知道了这个设定我们应该在长事务中把影响并发度的锁尽量往后放。下面的这一段的介绍比较复杂,我觉得丁奇讲得还是比较清楚的所以直接引用原文了。

假设你负责实现一个电影票在线交易业务,顾客 A 要在影院 B 购买电影票。我们简化一点,这个业务需要涉及到以下操作:

1. 从顾客 A 账户余额中扣除电影票价;

2. 给影院 B 的账户余额增加这张电影票价;

3. 记录一条交易日志。

也就是说,要完成这个交易,我们需要 update 两条记录,并 insert 一条记录。当然,为了保证交易的原子性,我们要把这三个操作放在一个事务中。那么,你会怎样安排这三个语句在事务中的顺序呢?

试想如果同时有另外一个顾客 C 要在影院 B 买票,那么这两个事务冲突的部分就是语句 2 了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。

根据两阶段锁协议,不论你怎样安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果你把语句 2 安排在最后,比如按照 3、1、2 这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度。


后面会分享更多devops和DBA方面的内容,感兴趣的朋友可以关注一下~


Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表