InnoDB 系列文章目录
1.InnoDB的关键特性
2.InnoDB的存储结构
3.InnoDB的索引和算法
4.InnoDB的锁总结
5. InnoDB的事务
InnoDB的一个特点就是实现了行锁,锁虽然大部分是InnoDB自动操作的,似乎不需要用户进行干涉,但是如果对锁不了解,可能会导致很多问题。
1.锁的类型
排他锁(X)和共享锁(S)
所谓排他锁,顾名思义,就是只能独占的锁,比如写数据的时候,一般是排他锁。毕竟,不能一边写数据,一边读数据。
而共享锁一般是读数据用。共享锁的作用是和排他锁互斥,防止读数据的时候,数据被修改。
表锁和行锁
和MyISAM引擎不一样的是,InnoDB不仅实现了表锁,也实现了行锁。而且表锁和行锁是可以共存的,比如某一行数据被加了行共享锁,这张表任然可以加表共享锁。
意向锁
InnoDB的行锁和表锁是可以共存的,但是考虑一个问题。如果要对一个表加上一个表排他锁,这个时候,如果该表的某个数据行已经被加了排他锁,那么,这个表排他锁是不能加的,否则就会冲突了。但是,难道加一个表锁还得一行一行的取判断数据是否已经加过了行锁了吗?显然,这样的效率是非常低的。因此InnoDB实现了意向锁。
意向锁的作用是,当对一个表的某一行数据加了行锁时,同时对该表加一个意向锁,表示该表的至少有一行数据被加了行锁,那么在需要对该表加表锁的时候,不需要一行一行的去判断行锁,只有判断该表的意向锁是否符合条件就可以了。
比如,对行加了一个行排他锁,同时也会对表加一个意向排他锁。当需要对该表添加一个表锁时,就会发现该表已经被添加了意向排他锁。于是就不能添加表锁了。
需要注意的是,意向锁只对添加表锁起作用。如果添加行锁,不需要判断意向锁,直接判断行数据是否被加锁。
2.一致性非锁定读
上面提到,如果数据在被修改的时候,是被添加了排他锁的,我们是不能读取的。但是实际上,InnoDB读取数据的时候并没有阻塞,而是使用了一致性非锁定读的机制来实现非锁定的读。那具体是这么实现的?
首先,我们知道对表数据的修改,并不是直接修改数据,而是先拷贝一份数据,修改拷贝的数据,并把原来的数据标记为删除。因此,随着修改的数据,是会产生不同的数据版本的。每个旧数据版本被称为快照。
而当数据被排他锁锁定时,InnoDB并没有阻塞等待,而是读取前一个版本的快照。这样就实现了非锁定读了。毕竟读取的时候,修改的数据还没修改好。
不过对于不同的事务隔离级别,实际上读取的快照版本并不一样。
对于Read Commited的隔离级别,总是读取最新的快照版本,也就是说,一旦数据被修改,那么在同一个事务中,再次读取得到的是最新的数据。也就是说,会出现重复读的问题,如果在一个事务中不同时间读取一行数据,会发现数据发生变化了。这不符合事务的隔离性要求。
而对于Repeatable Read级别,总是读取事务开始时的那一个版本。也就是说,无论数据发生了什么改变,无论读取多少次数据,数据都是一样的,也就是没有重复读的问题。
ps:事务会在下一篇文章详细介绍。
3.自增长锁
如果对主键使用了自增长,对于每次insert,都要对自增长值+1,这个时候就需要用到这个特殊的锁,自增长锁。和其他锁不一样的是,其他的锁在事务结束的时候才会释放锁。而自增长锁会在insert结束后立即释放。
另外在MySQL 5.1.22版本之后,InnoDB提供了一种轻量级互斥量的自增长实现机制,在这之前,是使用表锁来锁定自增长计算器的。而轻量级互斥量则是在内存中维护一个计数器,对计数器进行累加操作,而不用使用表锁。
可以通过配置innodb_autoinc_lock_mode来进行不同的操作。
- 0:使用表锁。
- 1:对于可以确定插入行数的insert操作,使用轻量级互斥量的自增长实现机制,其他任然使用表锁。
- 2:全部使用轻量级互斥量的自增长实现机制。
需要注意的是,如果innodb_autoinc_lock_mode等于2时,对于不确定插入行数时,互斥量会多申请一定数额,如果未能全部使用完,就会导致主键不连续。
4.锁的算法
- Record Lock:会锁定单个行记录
- Gap Lock:间隙锁,会锁定一个范围,但是不包括边界
- Next-Key Lock:锁定范围,包括边界,还会锁定条件
5.锁问题
在使用不同的事务隔级别的时候,其实是使用了锁实现了隔离级别。在不同的隔离接别下,可能会有不同的问题。
不可重复读
在上面我们也提到了不可重复读的问题,如果使用的是read commited级别,那么每次读取的数据有可能是不一样的。InnoDB是使用一致性非锁定读机制来实现,每次读取都是同一个版本的数据。
丢失更新
不过这时候你可能会考虑到一个问题,每次读取的都是一个历史版本,那读取到的数据不就是不是最新的?比如我想要对某个账户月+100块钱,我先读取出余额是0,加上100,等于100,这时候这个账户余额被其他事务更新为50了,如果我再更新余额为100,那么就会覆盖掉之前的更新了?其实这是另外一个问题,更新覆盖,对于这类问题,应该使用业务的锁来解决,比如添加一个乐观锁,来做判断。或者使用最高级别的串行化隔离级别,但是这会极大的降低效率。
脏读
脏读是指,读取了未提交的数据,导致读取的数据可能是错误的。
如果使用的是Read uncommit,那么在事务中,会读取到其他事务未提交的数据的。解决这个问题的方法就是使用Read uncommit及以上的隔离级别,mysql默认的隔离级别是Repeatable Read。
幻读
幻读指的是,在查询某个条件的数据后,有新的数据插入或者有数据被删除,这就导致查询的数据和数据库中的数据也是不一致的。其他的数据库可能使用串行化级别来解决这个问题,InnoDB则使用了Next-Key Lock锁来避免幻读。除了锁定被查询出来的数据,Next-Key Lock还会锁定该条件的删除和插入操作。
6.强制使用锁
除了数据库自动使用的锁,其实也可以在事务中强制使用锁。
强制排他锁
使用for update,添加一个强制的排他锁
Select …… for update
强制共享锁
select …… Lock in share mode