前言:何为锁?

锁是计算机协调多个进程或线程并发访问某一资源的机制

像 CPU, 内存, I/O 这些都是资源,在现在这种多用户的操作系统中,都会被共享到,那势必涉及到用锁来协调。

区别于上述的物理硬件,数据亦是一种共享资源,同样需要通过锁来解决多用户,并发访问的问题,而数据库这样的应用恰恰帮我们用锁来实现上面的问题。

不难发现,想避免数据竞争,加锁实现串行化,一个个去操作数据是最好的方法,但是这样相当于就没有了并发,是要数据安全,还是要并发性能,这又是一个计算机领域的难题。

MySQL 锁机制

这里以 MySQL 为例(因为我只接触过它:P),先大致了解下锁的种类,后面再看如何解决数据安全跟性能之间的权衡。

按锁定的数据范围,MySQL 的锁可分为:

范围 粒度 开销 加锁速度 锁冲突概率 死锁 并发
表级 没有 最低
页级 存在 一般
行级 存在 最高

我个人还有三个小疑问:

  1. MySQL 不是号称实现了锁机制,为何还要开发人员去管理锁?
  2. 如果开发人员对操作加锁,该加什么类型的锁?
  3. 加锁的数据范围及时间范围又该怎么定夺?

MyISAM 和表锁

表锁分为两种:

  1. Table Read Lock
  2. Table Write Lock

锁定关系

锁定关系 Read Lock Write Lock
Read Lock 并行 互斥
Write Lock 互斥 互斥

简单说,除了读读是并发的,其他锁组合都是串行,需要等待上一个锁释放才能进行上锁。

如何上锁

MyISAM 会自动操作上锁(隐式)

  1. SELECT 自动加 Read Lock
  2. UPDATE/INSERT/DELETE 自动加 Write Lock

那为何还要用语句 LOCK TABLE (显式)加锁呢?无非是为了模拟事务,MyISAM 是不支持事务的。

事务是由一组 SQL 语句组成的逻辑处理单元,事务具有4属性,通常称为事务的ACID属性。

因为事务一组 SQL 语句可能涉及多表,所以语句级别的隐式锁定不能满足,必须显式地 LOCK TABLE,例如有 orders 及 products 两张表,里面的 SUM(price) 正常情况下是一致的。

1
2
SELECT SUM(price) FROM order;
SELECT SUM(price) FROM product;

但如果不 LOCK TABLE,可能你读出 orders 的 SUM(price) 时,products 的 SUM(price) 已经改变了,可见 LOCK TABLE 在这种场景下是必要的。

1
2
3
4
LOCK tables orders read local, products read local;
SELECT SUM(price) FROM order;
SELECT SUM(price) FROM product;
Unlock tables;

特别强调:

  1. local 是一个 LOCK TABLE 的一个选项,作用就是在满足 MyISAM 并发插入条件的情况下,允许其他用户在表尾插入记录
  2. LOCK TABLE 要一次性 LOCK 住所有要操作的表,不能访问其他一切表,如果加的是读锁,就只能执行读,而不能执行写,写锁同理
  3. 其他 Session 对表的锁定关系不变

锁并发

在一定的条件下,MyISAM 也支持 r/w 并发,涉及到一个引擎的系统参数 (concurrent_insert),这里不展开讲了,有兴趣自己去查文档。

锁调度

上面已经讲了锁定关系,读写锁是互斥的,串行的,那是不是按照先来后到的顺序?并不是!写锁的优先级要远远高过读锁!申请写锁的进程会优先上锁,这也正是 MyISAM 不太适合于有大量更新操作和查询操作应用的原因,大量的更新操作会造成查询操作很难获得读锁,读操作可能永远阻塞。

好在有两个系统参数可以调整读锁的优先级,这里不展开讲了,有兴趣自己去查文档。

  1. low-priority-updates
  2. max_write_lock_count

特别强调:

  1. 避免时间长的查询语句,这样可以避免锁冲突,减少其他进程超时的情况
  2. 如果无法避免,就在低峰期进行操作,例如统计报表数据可放在深夜去跑

InnoDB 和行锁

InnoDB 相比 MyISAM 最大的不同是:

  1. 支持行锁
  2. 支持事务

先说事务,上面已经提到了事务具备 ACID 属性,这几个属性势必跟并发性能是想冲突的,如果是表锁实现的事务,其实还是把事务变成串行事务,可想而知并发性能是非常之低,那么 InnoDB 是如何去权衡这两者的关系?

事务隔离级别

为了解决“隔离”与“并发”的矛盾,ISO/ANSI SQL92 定义了4个事务隔离级别:

隔离级别/并发副作用 读取一致性 脏读 不可重复读 幻读
Read Uncommited 只能保证读取物理数据正确 存在 存在 存在
Read Commited 语句级别 没有 存在 存在
Repeatable Read 事务级别 没有 没有 存在
Serializable 事务级别 没有 没有 没有

隔离级别及对应的副作用这篇文章写得非常浅显易懂,这里就不作展开,就看他这篇即可。

其中,Serializable 也就是表级的串行,隔离级别最高,基本没有并发可言,而 Read Uncommited 根本不适用数据库这样的有一致性要求的场景,也基本用不到,本文不对这两种隔离级别进行讨论,主要针对 Read Commited 和 Repeatable Read 两个隔离等级进行讨论。当然还是那句老话,具体选用哪个隔离级别要结合你的应用场景!

为了实现上面的事务隔离级别,InnoDB 提供了四种行锁来实现:

  1. 共享锁(S)
  2. 排它锁(X)
  3. 意向共享锁(IS)
  4. 意向排它锁(IX)

锁定关系

锁定关系 S X IS IX
S 并行 互斥 并行 互斥
X 互斥 互斥 互斥 互斥
IS 并行 互斥 并行 并行
IX 互斥 互斥 并行 并行

同样,InnoDB 也是自动加锁,但也可以显式地加锁

  1. 共享锁(S):WHERE … LOCK IN SHARE MODE
  2. 排它锁(X):WHERE … FOR UPDATE

很多人容易搞混不可重复读和幻读,确实这两者有些相似:

  • 不可重复读在于 UPDATE 和 DELETE
  • 幻读在于 INSERT。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该 SQL 第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住 INSERT 的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以 INSERT 数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要 Serializable 隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。上文说的,是使用悲观锁机制来处理这两种问题,MySQL 出于性能考虑,使用了以乐观锁为理论基础的 MVCC(多版本并发控制)来避免这两种问题。

MVCC

人们一般把基于锁的并发控制机制称成为悲观机制,而把 MVCC 机制称为乐观机制。这是因为锁机制是一种预防性的,读会阻塞写,写也会阻塞读,当锁定粒度较大,时间较长时并发性能就不会太好;而MVCC是一种后验性的,读不阻塞写,写也不阻塞读,等到提交的时候才检验是否有冲突,由于没有锁,所以读写不会相互阻塞,从而大大提升了并发性能。

声明一点:Multi-Version Concurrency Control 是一种无锁事务机制,而不是一种具体的实现。

这里讨论的是 InnoDB 对 MVCC 的实现:

  • SELECT时,读取创建版本号 <= 当前事务版本号,删除版本号为空或 > 当前事务版本号
  • INSERT时,保存当前事务版本号为行的创建版本号
  • DELETE时,保存当前事务版本号为行的删除版本号
  • UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

这样读不阻塞写,写不阻塞读,并发自然就高了,当然也取决与资源读写竞争频率,如果是高频数据,那势必会导致很多重试,万一重拾的成本非常高,那还不如用锁机制去实现。

首先看下 MVCC 引入的两种读:

  • snapshot read (快照读): 顾名思义读取的是数据的某个历史版本,有可能不是最新的

    1. select * from table … ;
  • current read (当前读): 读取的是最新的数据,在 MVCC 中需要显式去加锁

    1. select * from table … lock in share mode;
    2. select * from table … for update;
    3. insert/update/delete;

这里有个疑问,为何 insert/update/delete 这类写操作也归类为当前读?那是因为在 MySQL 内部,一个写操作都是需要先读取记录,再对这些记录进行操作。

对于读操作(快照读):

RC 级别,读取的是 fresh snapshot,也就是最新的数据版本,确实存在重复读的情况。
RR 级别,但读取的是某个版本的 snapshot (事务中第一个读取语句发生那一刻的 snapshot),由于历史数据是按版本为单位的,所以快照读是不存在幻读的。

对于写操作(当前读):

回到事务隔离级别的概念上,定义中带来的副作用指的是当前读下产生的,跟 MVCC 引入的快照读不能一概而谈,当前读的隔离性必须得通过上锁来实现,而此时单单行锁是满足不了需求的,InnoDB 实现了一个叫 Next-Key 的锁来保障 RR 级别不会幻读。

所以 InnoDB 的 RR 隔离级别是消除了幻读吗?怎么跟 ISO 的说法不一样,当然了,标准是标准,具体实现又是另一回事,这里有篇文章有说,很有意思,强烈推荐看一下!

Next-Key Lock

Next-Key Lock = X Lock + Gap Lock

RC 级别,当前读会对数据加 X Lock,存在幻读。
RR 级别,当前读会对记录加 X Lock,同时保证对读取的范围加 Gap Lock,新的满足查询条件的记录不能够插入,不存在幻读现象。

再详细说明一下 Next-Key 是避免 RR 的幻读,我们知道 Record Lock 只能锁定到当前的记录,如果其他事务 Insert 了数据,就会造成幻读

方法一:加表锁,锁住其他事务不给 Insert (相当变为 Serialazable 级别,抛弃)

方法二:

Where 条件走普通索引的情况

next-key

  1. 如图所示先定位到索引的记录,加上 X 锁
  2. 再把索引记录两边的间隙分别加上 Gap 锁
  3. 再由索引定位到主键记录,再加上 X 锁

WHERE 条件走 Primary Key/Unique Key 的情况

  1. 由于这两种索引本身保证了Unique,所以新记录插不进去
  2. 所以只需要加上对应记录的 X 锁即可

WHERE 条件不走任何索引

  1. 扫全表,在每条主键记录上加上 Gap 锁
  2. 阻塞所有需要加锁的操作
  3. 当然 MySQL 本身会优化这种情况,对于不满足的记录会提前释放,有兴趣的可以参考这篇blog

死锁

占坑,也许新开一篇来探讨…

MetaData Lock

本文讨论的都是 CURD Data Manipulate Language (DML) 的锁,而对表结构的操作 Data Definition Language (DDL),MySQL 会加上 MetaData Lock(MDL)。MySQL 5.3 版本后引入了MDL,只有在事务结束后才会释放 MDL,因此在事务提交或回滚前,是无法进行DDL操作的。这里先点一下,下次再写一片慢慢探讨。

总结

先解答文初我的三个疑问:

  1. MySQL 实现的是语句级别自动加锁,事务是涉及特定业务的多条语句,所以有自己加锁的需求
  2. 加锁类型也是根据事务要达成的目的,在一致性要求强的情况下可以加X锁,其他情况考虑并发可以加S锁
  3. 手动加锁范围会直到事务结束(不敢确定,因为还没详细了解锁升级的内容)

简单来说 InnoDB 干了下面几件事:

  1. 为了提高并发性能
  2. 提出了粒度更小的行锁
  3. 提出了基于事务隔离级别来平衡事务的一致性读
  4. 利用 MVCC 来实现隔离级别中产生的读取副作用
  5. 从而提高了并发性能

终于对 MySQL 锁机制/事务隔离级别有了个大概的了解,数据一致性及并发始终是一个难题,一个关于权衡的难题,只有深刻认识业务才能作出合适的选型。其中事务级别 RU 及 Serializable 分别是并发及一致性的两个极端,更好地帮我们去理清两个边界,最终权衡出 RC/RR 两个用处比较广泛的事务场景。

另外关于锁/MVCC,这两者就像是连接数据一致性和并发性能的绳子,特别是 MVCC 让我学习了另一种提升并发性能的方法。

最后,再称赞下 MySQL 设计的精妙,无论是提出当前读跟快照读,还是违反两段提交的提前 unlock 行数据,都是在整个理论模型上的有效补充。

参考

https://coolshell.cn/articles/6790.html
http://blog.csdn.net/zyz511919766/article/details/49450849
https://tech.meituan.com/innodb-lock.html
http://www.cnblogs.com/chenqionghe/p/4845693.html
http://www.cnblogs.com/renolei/p/5910060.html
http://www.jianshu.com/p/4e3edbedb9a8
https://github.com/Yhzhtk/note/issues/42
http://blog.sina.com.cn/s/blog_499740cb0100ugs7.html
http://hedengcheng.com/?p=771#_Toc374698306