事务的隔离级别在之前我们已经学习过,那么事务隔离级别的实现原理是什么呢?锁+MVCC
下面我们就来分开讲解:
注意:表锁和行锁说的是锁的粒度
,不要以为它与下面讲到的其他锁是单独的概念。因为有表级共享锁等概念的存在。
排它锁(Exclusive)
,又称为X 锁,写锁。共享锁(Shared)
,又称为S 锁,读锁。X和S锁之间有以下的关系:
SS可以兼容的,XS、SX、XX之间是互斥的
显示加锁:
select ... lock in share mode
强制获取共享锁,select ... for update
强制获取排它锁
从上面的示例中,我们可以看出InnoDB中,是支持行锁的。
以主键为过滤条件时,事务1和事务2可以获取不同行的排它锁
接着看以下的例子,以非索引列作为过滤条件,会出现什么情况呢?
于是我们得出结论:
InnoDB的行锁是加在索引项上面的,是给索引加锁,并不是单纯的给行记录加锁,所以如果过滤条件没有加索引的话,使用的就是表锁,而不是行锁。
因此,我们得出InnoDB并不是只有行锁,还存在表锁。
行锁使用的条件:加在索引项上。
InnoDB存储引擎支持事务处理,表支持行级锁定,并发能力更好。
通过给索引上的索引项加锁来实现
的,而不是给表的行记录加锁实现的,这就意味着只有通过索引条件检索数据,InnoDB才使用行级锁,否则InnoDB将使用表锁。 (原理是在二级索引树上搜索了主键索引)所以:InnoDB默认情况下是使用表锁,一旦使用索引项,就会使用行锁。
串行化的隔离级别之下,不需要手动获取锁,会自动加上表的共享锁和排它锁 , 示例如下:
我们知道串行化的隔离级别之下,解决了可重复读未解决的幻读问题,那么它是怎么实现的呢?这就是我们接下来要学习的内容。
串行化解决幻读:依靠的就是间隙锁。
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)” ,InnoDB 也会对这个“间隙”加锁
,这种锁机制就是所谓的间隙锁。
不仅给表中满足范围查询要求的记录间隙加锁,给在其之后的 “ 空洞 ” 区域也加上了锁。
间隙(gap)(左开右闭)的范围是 : (11,12],(12,22],(22,23],(23,+正无穷)
给这些间隙加锁,就叫做间隙锁。
在串行化的隔离级别下:使用范围查询不仅给满足范围的记录添加了行锁,还添加了间隙锁,从而消除幻读现象。
即next-key lock : record-lock + gap-lock
读者在理解时要注意思考间隙锁的作用范围。
当查询的表是小表时,很可能用不到索引,此时在串行化的模式下就会添加表锁,即使你插入的范围不再间隙锁的范围之内,也无法插入。
综上,这种情况解决了串行化在范围查询下的幻读问题
那么在等值查询的条件下,间隙锁是如何工作的呢?
添加行锁后
,那么在另一个事务中也是可以完成插入操作的。就会在已过滤的辅助索引列上加上行锁之后,再在其左右两边范围内加上间隙锁
,在间隙锁范围内是不能够完成插入获取排它锁。在绝大部分情况下都应该使用行锁,因为事务和行锁往往是选择InnoDb的理由,但个别情况下也会使用表锁:
LOCK TABLE user READ;读锁锁表
LOCK TABLE user WRITE; 写锁锁表
事务执行...
COMMIT/ROLLBACK; 事务提交或者回滚
UNLOCK TABLES; 本身自带提交事务,释放线程占用的所有表锁
在了解了串行化是如何解决幻读之后,那么已提交读和可重复读的隔离级别下的并发控制是如何实现的呢?
——底层实现原理:MVCC(多版本并发控制):并发读取方式:快照读
InnoDb提供了两种读取操作,锁定读(s和x)和非锁定读(MVCC提供的快照读),依赖底层的undolog(回滚日志)
追根溯源,我们一步一步来看:
事务日志:uodo log和redo log
undo log的主要作用是:
MVCC的多版本并发控制中,读操作分为以下的两类:
快照内容读取原则:(仔细理解,跟下面的内容息息相关)
1、版本未提交无法读取生成快照
2、版本已提交,但是在快照创建后提交的,无法读取
3、版本已提交,但是在快照创建前提交的,可以读取
4、当前事务内自己的更新,可以读到
1、已提交读解决脏读的原理以及为什么没有解决不可重复读和幻读的原因:
每次select的时候会重新生成一次快照
(前提是数据已经被另一个事务正确commit过了,此时脏读时数据是处于prepare状态,此时事务生成的快照还是旧数据生成的快照,并没有用未提交的数据照快照),另一个事务一旦提交,此时再次select快照,照到的就是已提交的新数据。2、可重复读解决脏读和不可重复读的原理以及为什么部分解决幻读的原因
①因为第一次select产生数据快照,而且只产生一次!其他事务虽然新增了数据,但是当前事务select时依然查看的是最初的快照数据。
②当前事务是可以看见自己
事务修改、更新的数据的(此时事务ID号会更新,旧数据会到lundo log中),此时不是快照读,而是当前读,再去select,此时就没有解决幻读问题:
注意
:这两个概念都是和表锁相关的,存在的目的就是更快速地获取表锁
。InnoDb行锁的存在可能会导致获取表锁时效率较低,为了避免大面积的行锁扫描,我们直接通过观察表级的意向锁即可,就可以判断能否获取当前表的X或S锁。
1、意向锁是由InnoDB存储引擎获取行锁之前自己获取的
2、意向锁之间都是兼容的,不会产生冲突
3、意向锁存在的意义是为了更高效的获取表锁(表格中的X和S指的是表锁,不是行锁!!!)
4、意向锁是表级锁,协调表锁和行锁的共存关系。主要目的是显示事务正在锁定某行或者试图锁定某行。
MyISAM 表锁是 deadlock free 的, 这是因为 MyISAM 总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步获得的,即锁的粒度比较小,这就决定了在 InnoDB 中发生死锁是可能的。
死锁问题一般都是我们自己的应用造成的,和多线程编程的死锁情况相似,大部分都是由于我们多个线程在获取多个锁资源的时候,获取的顺序不同而导致的死锁问题。因此我们应用在对数据库的多个表做更新的时候,不同的代码段,应对这些表按相同的顺序进行更新操作,以防止锁冲突导致死锁问题。
1.尽量使用较低的隔离级别
2.设计合理的索引并尽量使用索引访问数据,使加锁更加准确,减少锁冲突的机会提高并发能力
3.选择合理的事务大小,小事务发生锁冲突的概率小
4.不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会
5.尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响(等值查询与范围查询)
6.不要申请超过实际需要的锁级别
7.除非必须,查询时不要显示加锁