赵玉伟的博客

innodb-事务&锁

数据库事务是一个比较复杂话题。 而对于程序员来说, 在需要事务支持的时候,可能加段配置(比如spring transaction,或者aspectj的@Transaction注解)就可以完成工作。 这得益于以下两点:
1、框架对事务管理封装的好;
2、框架提供的接口或者使用方法比较友好。

但是深入事物内部的实现, 发现事务并不是特别容易理解的。 前段时间我在思考下面这个问题:
mysql中update一条语句是否是一个原子操作?
基于这一点,我查了下《高性能mysql》,很遗憾,这本书中并没有对事务有比较详细的讲解;《mysql技术内幕》仅仅介绍了事务的使用方式。之后,通过在网上搜集的资料, 让我对事务的理解加深了一层,做下笔记,主要涉及以下知识点:
事务的特性、隔离级别、锁、MVCC(包括快照读、当前读、redo log、 undo log)

事务有哪些性质

1、原子性 :
最小的工作单元,不可再分割。事务具有原子性之后,事务就是最小的执行单元,在事务里面的行为不能再被分割。
2、一致性
事务影响的多个数据要保持一致, 某个数据多了, 那么就意味着某个数据变少。
3、隔离性
针对于同一个数据,同时被多个事务操作时,innodb有四种级别。 同时的概念: 多个事务在并行的处理。
4、持久性
事务提交之后,事务中操作的数据要落库, 数据由“动态数据”变成“静态数据”。

事务的隔离级别

级别名称与翻译

隔离级别一般有以下四种:
1、read uncommitted —–翻译—–> 脏读、读未提交、未提交读
2、read committed —–翻译—–> 提交读、读提交、不可重复读、
3、repeatable read —–翻译—–> 可重复读、幻读
4、serializable —–翻译—–> 串行

翻译成汉语之后,确实加重了理解的困难。我说下我的理解, 用A、B两个并行事务举例:

1、read uncommitted, 直译: 读未提交

事务A可以读取事务B正在操作,但是还没有正式提交的数据
说明:A对数据D进行处理,比如对D + 1 操作, 但是并没有完成; B事务读到的结果是D + 1, 而不是D, 如果A回滚,B正常提交, 会产生数据不一致问题。 此种级别几乎不被使用。

2、read committed,直译:读提交

事务A能够读到事务B已经提交的数据,重复读取可能会导致前后数据不一致
说明: A数据对D进行处理,之后对D+1操作、提交; 在A事务执行的过程中,对数据D做的改变,B事务是不可见的。 B事务只能读到A事务开始之前,以及A事务结束之后的数据。但是B事务查询多次D时, 可能会造成多次的结果不一致, 比如: 第一次读时A未开始,第二次读时A已经结束(在这期间,事务A可能对D做了修改或者删除)。 所以,该级别也可以称为 不可重复读(取其他事务的数据)。
该级别为Oracle默认的隔离级别。

3、repeatable read 直译: 重复读

事务A能够读到事务B已经提交的数据,而且可以重复读取,前后读取的单条数据能够保持一致
说明: A事务操作数据D, B事务在任何时候, 都可以对D进行读取,而且保证每次读取的D的值保持一致。但是,如果B读取的是某个范围的数据,而且是多条数据时,其他事务对这个范围内的数据进行新增后,B事务再次读取时,能够读取到被新增的记录,会产生幻读。
该级别为innodb默认的隔离级别(但是innodb解决了幻读问题)。

4、serializable 串行话

说明:让所有的事务串行的处理, 效率低下, 几乎不被使用。


锁是为了保证数据的一致性。 多个事务(在应用中是多个线程)对同一数据进行读写时, 通过加锁、释放锁实现。保证事务处理的数据不会相互影响。

从锁定数据范围的角度看, 可以分为表锁、行锁

表锁:锁的对象是一张表,性能最差,系统开销最低。
行锁:锁的对象是某一行记录,并发性会比较好,同时会导致系统开销增大。

从加锁对于其他事务的影响看, 可以分为X锁、S锁

排它锁(eXclusive lock),又叫X锁:
如果事务A在数据D上加了X锁,其他事务不能加任何锁,既其他事务不能读取D,也不能修改D。
共享锁(Share Locks),又叫S锁:
如果事务A在数据D上加了S锁,A只能读取D, 不能修改D; 其他事务也只能在D上加S锁。 D被加了S锁之后, 只能被读取,不能被修改。 当作用在数据D上的事务结束后,S锁被释放, 此后, D才可能被加X锁。

注:X锁、S锁是一层高度抽象的统称,他们的实现,尤其是S锁,往往借助于多种方式配合完成,比如版本号、日志等, 而且每种应用的实现方式也不完全一样。

锁与性能是不可调和的矛盾, 加锁就意味着性能的下降; 提高性能就需要避免加锁。 在系统设计时,要把握两者的平衡点。
mysql有自己的一套架构,事务是由存储引擎实现的,而不是引擎上层的服务。


下图是mysql的整体架构图:

MVCC

MVCC, 是Multi-Version Concurrency Control 的简写, 翻译为:多版本并发控制。

mvcc与乐观锁的关系

我们知道, 乐观锁是通过在数据上增加版本号,每次commit的时候通过比较版本号,来提高访问性能以及保持数据一致的, 所以,用乐观锁时, 多个事务对同一数据的写操作性能会高。
innodb的mvcc,主要解决的问题是:在写少读多的场景下,读不用阻塞。
所以mvcc与乐观锁处在同一级,他们所解决的问题不一样;同样,mvcc是没有明确的定义的,每种应用面临的业务场景不同,会导致各自的实现也不完全相同。

再次说明下innodb中mvcc实现的意义:是为了 在读多写少的场景下, 读不用加锁, 从而提高读的性能; 比如:事务T1在对数据D1执行update操作的期间, 我们把这个期间命名为time1_time6; 在这段期间内所有的select查询, 都不会被阻塞; 但是其他的update会被阻塞。
所以,mvcc的特点是: 读与读不阻塞; 读与写不阻塞; 写与写阻塞。

当前读、快照读

插播一个知识, 对数据的操作就只包括 select、 insert、update、delete;如果在innodb中对读进行分类, 可以分为快照读和当前读;
快照读: 就是 select * from table where……, 也可以理解为只读数据、 不对数据做任何更新, 这种方式可能会读取到历史版本,但是不会阻塞;
当前读:需要更新数据(更新数据意味着先读后写,insert除外), 就是 insert、 update、 delete; 这种操作处理的是最新的数据,需要对最新的数据加锁。

mvcc实现依赖的数据基础

innodb的RC隔离级别、RR隔离级别中,对mvcc做了实现; serializable是完全阻塞的, 不需要mvcc。
mvcc的实现的数据基础: 在每条数据的最后,存在三个字段;

1、DB_TX_ID:递增的事务号, 每开启一个事务(select也是一个事务), 该字段的值递增1;
2、DB_ROLL_PTR:回滚指针, 该指针指向旧版本的数据,某个事务失败后,需要对数据回滚时,通过该指针取得旧版本的数据;(涉及redo log, undo log);
3、DELETE_BIT:删除标识;

mvcc在实现读不阻塞时, 主要通过每个事务自身的版本号, 与每条数据的版本号做比较完成的。以下我认为是理解的基础,直接从网络上copy下来:

SELECT:InnoDB会根据以下两个条件检查每行记录:
1)InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,只么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
2)行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除.

INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE:InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当系统的版本号为原来的行作为删除标识

mvcc与log

mvcc需要与redo log undo log一起完成事务的隔离性、原子性、与持久性;mysql也是一个数据库应用服务器, 在做数据操作时,也需要把数据从db读到内存。

mvcc与写入

确切的说,mvcc中的数据写入, 我认为mvcc起的更大的作用是用来回滚,因为多个事务在对数据进行写入操作时, 需要加锁,保证写与写阻塞。
实现写写阻塞,同时又能提高性能的方式有多种,最常见的比如: 乐观锁通过写入时比较版本号;
而mvcc是将把操作的数据加上悲观锁来实现的, 对数据加锁之后, 为了支持回滚、提交操作, mysql会将数据记录undo log 与 redo log, 对数据操作完成之后,执行commit, 释放锁。
而mvcc在对数据加锁时, 为了解决幻读的问题,使用的锁为 next-key(next-key包括 gap锁,也就是间隙锁, + 行锁)。 间隙锁用来增加数据的锁定范围, 行锁用于锁定精准的数据。


所以, mysql中的innodb引擎中, update、同时还包括 insert、 delete,都是原子操作, 先读,后写的操作过程中, 需要对数据加锁。 但是加锁的过程中,为了提高性能, 绝对不是仅仅把数据lock下就结束了的, 尤其一个事务中包含多条语句时, 可以扩展一下, 两阶段锁的实现方式。


延伸思考

既然mvcc不能解决写与写的阻塞问题, 引申一个场景:

秒杀减库存的场景, 盖场景在电商领域中,当商家做活动时非常常见,业务很简单, 支持的qps,与数据的一致性是关键, 简单说下性能的预估:

假如库存减1的sql为: update stock set count = count - 1 where productId = 1234;
执行这条语句的耗时加入为2ms, 那么mysql层面的qps最大为 1000/2 = 500qps,也就是一秒能处理500个请求,这显然不能抗住大量的请求; 此时, 可以将库存数据打散成多份,
比如, 原先一条数据中存储了1000个库存; 改成 10条数据中每条存储100个, 此时, qps的量就变成 500 * 10 = 5000了。

参考:
http://tech.meituan.com/innodb-lock.html
http://blog.csdn.net/xifeijian/article/details/45230053
https://www.douban.com/note/516329965/
https://www.zhihu.com/question/27876575

redo log undo log
参考:http://www.cnblogs.com/chenpingzhao/p/5003881.html