转载自:http://tech.meituan.com/innodb-lock.htmlhtml
咱们都知道事务的几种性质,数据库为了维护这些性质,尤为是一致性和隔离性,通常使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,若是加锁过分,会极大的下降并发处理能力。因此对于加锁的处理,能够说就是数据库对于事务处理的精髓所在。这里经过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底作了什么。sql
由于有大量的并发访问,为了预防死锁,通常应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,而后所有锁住,在方法运行以后,再所有解锁。这种方式能够有效的避免循环死锁,但在数据库中却不适用,由于在事务开始阶段,数据库并不知道会用到哪些数据。
数据库遵循的是两段锁协议,将事务分红两个阶段,加锁阶段和解锁阶段(因此叫两段锁)数据库
事务 | 加锁/解锁处理 |
---|---|
begin; | |
insert into test ..... | 加insert对应的锁 |
update test set... | 加update对应的锁 |
delete from test .... | 加delete对应的锁 |
commit; | 事务提交时,同时释放insert、update、delete对应的锁 |
这种方式虽然没法避免死锁,可是两段锁协议能够保证事务的并发调度是串行化(串行化很重要,尤为是在数据恢复和备份的时候)的。安全
在数据库操做中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。咱们的数据库锁,也是为了构建这些隔离级别存在的。session
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
Read Uncommitted这种级别,数据库通常都不会用,并且任何操做都不会加锁,这里就不讨论了。并发
MySQL中锁的种类不少,有常见的表锁和行锁,也有新加入的Metadata Lock等等,表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会致使并发能力降低,通常是作ddl处理时使用。高并发
行锁则是锁住数据行,这种加锁方法比较复杂,可是因为只锁住有限的数据,对于其它数据不加限制,因此并发能力强,MySQL通常都是用行锁来处理并发事务。这里主要讨论的也就是行锁。性能
在RC级别中,数据的读取都是不加锁的,可是数据的写入、修改和删除是须要加锁的。效果以下spa
MySQL> show create table class_teacher \G\ Table: class_teacher Create Table: CREATE TABLE `class_teacher` ( `id` int(11) NOT NULL AUTO_INCREMENT, `class_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `teacher_id` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_teacher_id` (`teacher_id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 1 row in set (0.02 sec) MySQL> select * from class_teacher; +----+--------------+------------+ | id | class_name | teacher_id | +----+--------------+------------+ | 1 | 初三一班 | 1 | | 3 | 初二一班 | 2 | | 4 | 初二二班 | 2 | +----+--------------+------------+
因为MySQL的InnoDB默认是使用的RR级别,因此咱们先要将该session开启成RC级别,而且设置binlog的模式rest
SET session transaction isolation level read committed; SET SESSION binlog_format = 'ROW';(或者是MIXED)
事务A | 事务B |
---|---|
begin; | begin; |
update class_teacher set class_name='初三二班' where teacher_id=1; | update class_teacher set class_name='初三三班' where teacher_id=1; |
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
commit; |
为了防止并发过程当中的修改冲突,事务A中MySQL给teacher_id=1的数据行加锁,并一直不commit(释放锁),那么事务B也就一直拿不到该行锁,wait直到超时。
这时咱们要注意到,teacher_id是有索引的,若是是没有索引的class_name呢?update class_teacher set teacher_id=3 where class_name = '初三一班';
那么MySQL会给整张表的全部数据行的加行锁。这里听起来有点难以想象,可是当sql运行的过程当中,MySQL并不知道哪些数据行是 class_name = '初三一班'的(没有索引嘛),若是一个条件没法经过索引快速过滤,存储引擎层面就会将全部记录加锁后返回,再由MySQL Server层进行过滤。
但在实际使用过程中,MySQL作了一些改进,在MySQL Server过滤条件,发现不知足后,会调用unlock_row方法,把不知足条件的记录释放锁 (违背了二段锁协议的约束)。这样作,保证了最后只会持有知足条件记录上的锁,可是每条记录的加锁操做仍是不能省略的。可见即便是MySQL,为了效率也是会违反规范的。(参见《高性能MySQL》中文第三版p181)
这种状况一样适用于MySQL的默认隔离级别RR。因此对一个数据量很大的表作批量修改的时候,若是没法使用相应的索引,MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,可是它们仍是被锁住了的现象。
这是MySQL中InnoDB默认的隔离级别。咱们姑且分“读”和“写”两个模块来说解。
读就是可重读,可重读这个概念是一事务的多个实例在并发读取数据时,会看到一样的数据行,有点抽象,咱们来看一下效果。
RC(不可重读)模式下的展示
事务A | 事务B | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
begin; | begin; |
|||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=1;
|
||||||||||
update class_teacher set class_name='初三三班' where id=1; |
||||||||||
commit; | ||||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=1;
读到了事务B修改的数据,和第一次查询的结果不同,是不可重读的。 |
||||||||||
commit; |
事务B修改id=1的数据提交以后,事务A一样的查询,后一次和前一次的结果不同,这就是不可重读(从新读取产生的结果不同)。这就极可能带来一些问题,那么咱们来看看在RR级别中MySQL的表现:
事务A | 事务B | 事务C | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
begin; | begin; |
begin; |
|||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=1;
|
|||||||||||
update class_teacher set class_name='初三三班' where id=1; commit;
|
|||||||||||
insert into class_teacher values (null,'初三三班',1); commit; |
|||||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=1;
没有读到事务B修改的数据,和第一次sql读取的同样,是可重复读的。 没有读到事务C新添加的数据。 |
|||||||||||
commit; |
咱们注意到,当teacher_id=1时,事务A先作了一次读取,事务B中间修改了id=1的数据,并commit以后,事务A第二次读到的数据和第一次彻底相同。因此说它是可重读的。那么MySQL是怎么作到的呢?这里姑且卖个关子,咱们往下看。
不少人容易搞混不可重复读和幻读,确实这二者有些类似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
若是使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务没法修改这些数据,就能够实现可重复读了。但这种方法却没法锁住insert的数据,因此当事务A先前读取了数据,或者修改了所有数据,事务B仍是能够insert数据提交,这时事务A就会发现莫名其妙多了一条以前没有的数据,这就是幻读,不能经过行锁来避免。须要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么作能够有效的避免幻读、不可重复读、脏读等问题,但会极大的下降数据库的并发能力。
因此说不可重复读和幻读最大的区别,就在于如何经过锁机制来解决他们产生的问题。
上文说的,是使用悲观锁机制来处理这两种问题,可是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。
正如其名,它指的是对数据被外界(包括本系统当前的其余事务,以及来自外部系统的事务处理)修改持保守态度,所以,在整个数据处理过程当中,将数据处于锁定状态。悲观锁的实现,每每依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,不然,即便在本系统中实现了加锁机制,也没法保证外部系统不会修改数据)。
在悲观锁的状况下,为了保证事务的隔离性,就须要一致性锁定读。读取数据时给加锁,其它事务没法修改这些数据。修改删除数据时也要加锁,其它事务没法读取这些数据。
相对悲观锁而言,乐观锁机制采起了更加宽松的加锁机制。悲观锁大多数状况下依靠数据库的锁机制实现,以保证操做最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销每每没法承受。
而乐观锁机制在必定程度上解决了这个问题。乐观锁,大可能是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增长一个版本标识,在基于数据库表的版本解决方案中,通常是经过为数据库表增长一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,以后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,若是提交的数据版本号大于数据库表当前版本号,则予以更新,不然认为是过时数据。要说明的是,MVCC的实现没有固定的规范,每一个数据库都会有不一样的实现方式,这里讨论的是InnoDB的MVCC。
这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论,实现简单,数据更加安全,可是并发能力很是差。若是你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可使用这种模式。
这里要吐槽一句,不要看到select就说不会加锁了,在Serializable这个级别,仍是会加锁的!