关于数据库锁的那些事

550 阅读6分钟

锁对于传统数据库来说是非常重要的, 里面也掺杂各种权衡, 概念类较多, 本文只针对部分内容做了讲解.

1. 从影响的数据范围看数据库锁

行锁

单独给一行数据记录加锁, mysql 中 我们常用的 InnoDB 引擎支持行锁.

优势: 是常见关系型数据库中锁粒度最小的一种锁, 能够有效的提高并发操作.

劣势: 消耗更多资源, 用法不规范容易产生死锁

表锁

顾名思义, 对当前操作的整张表加锁,是目前 mysql 锁粒度最大的一种

最常见的 myisam 引擎使用表锁.

InnoDB 引擎 sql 使用不当(如非主键、索引条件)会退化为表锁.

优势: 并发读没问题, 消耗资源少, 加减锁速度快, 可以避免死锁.

劣势: 大部分情况下DML操作并发低, 频繁对全表加锁, 发生锁冲突的概率非常高.

页锁

页锁是介于比表锁和行锁之间的一种锁, 只是对数据页进行锁定

页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但并发低,行级并发高,但耗资源。所以取了折衷的页级,一次锁定数据页中相邻的一组记录

mysql 中的BDB引擎支持页级锁

大概锁的层次结构关系

2. 深入 mysql 的锁

锁分类 (latch 和 lock)

在 mysql 数据库中, 从全局看, 共分为这两种锁, 但他们的用处有很大区别.

latch 直译过来为“门闩(shuān)”, 类似于下图

它在数据库中的学名叫做 闩锁(一种轻量级的锁), 在 InnoDB 存储引擎中,latch又可以分为 mutex(互斥量)和 rwlock(读写锁), 其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制.

# 查看关于闩锁的统计信息
SHOW ENGINE INNODB MUTEX ;  
+--------+------------------------+---------+
| Type   | Name                   | Status  |
+--------+------------------------+---------+
| InnoDB | rwlock: log0log.cc:838 | waits=1 |
+--------+------------------------+---------+

lock 锁, 是我们打交道最多的一种, 它主要针对的是事务,用来锁定的是数据库中的对象,如前面提到的表锁、页锁、行锁。并且一般lock的对象仅在事务 commit 或 rollback 后进行释放(不同事务隔离级别释放的时间可能不同)。此外,lock,正如在大多数数据库中一样,是有死锁机制的.

3. InnoDB 实现 lock 锁分类

共享锁 (S Lock)

允许事物读取数据

当你拿到了共享锁, 你可以读取这条数据, 另一个人同样可以拿到共享锁去读取数据(这里我们称为锁兼容)

但是同时另一个人也来了, 他同时想要拿排他锁去更新此条数据,那么他必须等待释放共享锁才可以拿到排他锁更新数据(这里我们称之为锁不兼容)

排他锁 (X Lock)

允许事物删除或更新一行数据

前面我们举例了 如果别人已经持有共享锁了, 其他人是不能拥有排他锁的.

当一个人拿到此条数据的排他锁, 不能同时再拿到排他锁和共享锁的(锁不兼容).

排他锁(X Lock)和任何锁都不兼容.

4. Innodb 面对锁是怎么读取的

一致性的非锁定读

我们知道 当我们频繁 更新数据 加排他锁 (X Lock) 的时候, 因为其锁的不兼容性, 会严重影响正常的数据查询性能.

一致性的非锁定读 是指 InnoDB 存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行 DELETE 或 UPDATE 操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB 存储引擎会去读取行的一个快照数据

一致性非锁定读的快照数据其实是读的 undo 数据(undo是用来回滚事务中的数据), 没有额外的锁操作, 所以读取速度非常的快.

一致性的非锁定读因为不需要等待排他锁 (X Lock) 的释放, 所以极大的提高了并发的性能, 在 innodb 事务隔离级别 不可重复读(read-committed)、可重复读(repeatable-read 默认级别) select 使用的是 一致性的非锁定读.

思考: 一致性的非锁定读 解决了什么问题? 带来了什么问题?

一致性锁定读

在一些强一致的场景, 我们是希望让用户读取到的永远是最新的数据. 这时候, 我们需要使用 一致性锁定读 的场景.

下面展示了两种基于数据库查询语句上X锁和S锁, 一般与显示的事务组合使用.

SELECT…FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁

SELECT…LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被阻塞

FOR UPDATE 也是我们经常所说的悲观锁, 对应的还有乐观锁, 乐观锁更多是业务层面的实现, 这里不再讲述.

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去.

目前主要还是靠业务逻辑来解决, 如果程序是串行, 就不会产生死锁, 只有并发情况下才有可能产生, 程序并行+数据库并行(行级锁)运行, 可能会发生死锁.

这里可以使用此命令查看所有线程, 对应的提示直接 kill 掉即可

show processlist

查看目前锁的情况

1:查看当前的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

2:查看当前锁定的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

3:查看当前等锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

当然你也可以通过配置 innodb_lock_wait_timeout 属性,来指定锁的超时时间, 超时数据库系统自动 kill.

不过如果出现了长时间获取不到锁, 数据库会自动进行死锁检测, 并进行终止.

当然,保证业务中操作数据库的执行顺序, 避免交叉执行, 基本能够避免的死锁情况。

总结

本文一部分参考概念 《mysql 技术内幕》一书.

更多有趣的计算机技术关注 呆呆熊一点通 :