文章目录
先回顾一下概念性问题——
什么是事务?
官方点说,事务就是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作作序列构成。
通俗点说,事务就是一组操作要么同时成功要么同时失败。
事务的四大特性(ACID):
1.原子性(atomicity):一个事务必须视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。
2.一致性(consistency):数据库总数从一个一致性的状态转换到另一个一致性的状态。
3.隔离性(isolation):一个事务所做的修改在最终提交以前,对其他事务是不可见的。
4.持久性(durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。
事务并发带来的问题:
1.脏读:脏读是指一个事务在处理过程中读取了另一个事务未提交的数据。
2.不可重复读:指的是在一个事务处理中读取到其他事务修改或删除并提交
的数据,导致多次读取结果不一致。
3.幻读:指的是在一个事务处理中读取到其他事务插入并提交
的数据,导致多次读取结果不一致。
事务并发带来的三大问题其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
mysql事务的隔离级别:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 可能 | 可能 | 可能 |
不可重复读(read-committed) | 不可能 | 可能 | 可能 |
可重复读(repeatable-read) | 不可能 | 不可能 | 在InnoDB中不可能 |
串行化(serializable) | 不可能 | 不可能 | 不可能 |
为什么InnoDB中在rr级别的时候就解决了幻读这个问题?
MVCC与LBCC
如果要解决读一致性问题,保证一个事务前后读取数据结果的一致性,实现事务隔离应该怎么做?
简单思考下解决方案:
LBCC
(全称Lock Based Concyrrency Control):在读取数据前对其加锁,阻止其他事务对数据进行修改。
MVCC
(全称Multi Version Concurrency Control):生成一个数据请求时间点的一致性数据快照(snapshot),并用这个快照来提供一定级别(语句级或者事务级)的一致性读取。
InnoDB中怎么实现的MVCC
其实InnoDB中,自动为每一行数据添加了三个隐藏字段:
DB_ROW_ID
(6字节):行标识,前边的文章中提到过这个字段。
DB_TRX_ID
(6字节):创建版本号,插入或更新行的最后一个事务的id,自动递增。
DB_ROLL_PIR
(7字节):删除版本号,用于回滚。
主要记住,在InnoDB中,一个事务开始时,会生成一个当前时间点的数据快照(可以理解为一个副本),只要事务不结束,他就只能在这个数据快照里查找数据,也就是只能查找到创建版本号小于当前事务id的数据和删除版本号大于当前事务id的数据(或者是在创建版本号小于当前事务id的前提下,没有删除版本号的数据)
在InnoDB中MVCC和锁是相互配合使用的,下边就看看锁的东西。
锁
表锁与行锁的区别:
锁定粒度:表锁 > 行锁
加锁效率:表锁 > 行锁
冲突概率:表锁 > 行锁
并发性能:表锁 < 行锁
注意:MYISAM引擎只支持表锁,InnoDB既支持表锁也支持行锁
行锁
共享锁
(Shared Locks):
又称为读锁,简称s锁,顾名思义共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能写,不然容易造成死锁。
加锁方式:在select语句后加LOCK IN SHARE MODE;
释锁方式:commit / rollback
排他锁
(Exclusive Locks):
又称为写锁,简称x锁,排他锁不能和其他锁共存,如一个事务获取了一个数据行的排他锁,其他事务就不能获取该行的锁(包括共享锁和排他锁),只有获取到该行的排他锁的事务是可以对数据进行读取和修改。
加锁方式:
1.手动加锁:在语句后加FOR UPDATE;
(select时需手动)
2.自动加锁:在insert / update / delete 语句时或自动加排他锁
释锁方式:commit / rollback
注意:innoDB中获取锁超时间默认为50秒,查看语句为 show variables like 'innodb_lock_wait_timeout';
。
表锁
意向共享锁
(Intention Shared Lock):
简称IS锁,表示事务准备给数据行进行加入共享锁,也就是说一个数据行加共享锁之前必须先获得该表的IS锁。
意向排他锁
(Intention Exclusive Lock):
简称IX锁,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须要获得该表的IX锁。
注意:意向锁是由数据引擎自己维护的,用户是无法手动操作意向锁的。
问:为什么需要表级别的意向锁?
答:假如要给一张表加表锁,那么这个加表锁的前提是没有其他任何事务已经锁住了这张表的任意一行数据。那么怎么来确定这个前提呢?
必然要有一个全表扫描才能确定每一行数据都没有被加锁。如果这张表数据比较大呢?有千万数据,那这个全表扫描的过程就要消耗很多时间才能确定能不能加表锁,并且如果在扫描的过程中万一有其他事务来加锁怎么办,所以说,全表扫描加表锁这种方式既慢并且消耗性能,带来严重的并发问题。为了解决这一问题,才有了意向锁。加表锁的时候只要看一下这个表上有没有意向锁就ok了,因为你加行锁的时候都会先获取到表的意向锁才行。意向锁就是为了提高加表锁的效率
。
问:锁的作用?
答:和Java中的锁一样,都是为了解决资源竞争的问题,Java中的资源是对象。而数据库中的资源就是表和行数据,锁就是为了解决对于事务并发访问的问题。
问:锁到底锁住了什么?是一行数据?还是一个字段?
InnoDB行锁原理
针对上述问题,其实有三种情况:
1.没有索引的表上加行锁,会锁住整张表,出现锁表的情况。
2.有主键索引的表上加行锁,会锁住索引。
3.用唯一索引(辅助索引)的字段加行锁,会锁住辅助索引和主键索引。
在InnoDB中,行锁就是锁住索引记录来实现的,加锁的时候在mysql自带的information_schema库中的INN0DB_LOCKS表中可以看到:
lock_type:锁类型;lock_table:加锁的表;lock_index:锁住的索引
问:没有索引的表上加行锁为什么会锁表?
注意:在之前的文章中说过,InnoDB中一张表是不可能没有索引的,如果用户没定义,默认就会用隐藏的字段ROW_ID作为聚集索引。那么加行锁的时候,条件没有命中索引(因为用的是隐藏字段作为索引,那么肯定不能命中啊),就会走全表扫描,不得不把所有的聚集索引全都锁住,所以会出现锁表的情况。
问:为什么用唯一索引加行锁时,会锁住主键索引?
这就又涉及到前边的知识——回表,因为InnoDB中,辅助索引的叶子节点上存放的是主键索引,索引用辅助索引加行锁时会先锁住辅助索引,然后锁住主键索引。本质上其实还是锁住了主键索引。那么锁到底锁住了什么?相信大家心里已经有答案了。
InnoDB行锁算法
主要有三种算法:
1.记录锁(Record Lock)
锁定记录
,在唯一性索引(唯一/主键)等值查询时,精准匹配到一个索引记录的时候会使用记录锁。
(如下图)比如用where id = 4这个条件时,就会精准匹配到一个索引记录。
2.间隙锁(Gap Lock)
锁定区间
,锁定是的是数据库不存在的区间范围,Gap Lock相互之间不会冲突。
比如,用where id = 6这个条件时,发现没有这个数据记录,就会锁住(4,7)这个范围(注意左开右开不包括记录)。
3.临建锁(Next-key Lock)
锁定记录和区间
,条件范围查找时,会锁住记录和区间。Next-key Lock = Record Lock + Gap Lock;
比如,用where id > 5 and id <9时,就会锁住(4,7]和(7,10]这个范围的区间和记录,也就是(4,10]这个范围的记录和区间(注意是左开右闭,不包括左边的记录,包括右边的记录)
事务隔离级别的选择
首先Read Uncommited不加锁和Serializable串行化这两种级别基本是不会被使用的。
Read Commited 对于普通的select语句使用mvcc,对于加锁的select和更新语句使用Record Lock记录锁。
Repeatable Read 对于普通的select语句使用mvcc,对于加锁的select和更新语句会使用Next-key Lock 、 Record Lock 、 Gap Lock。
1.RR的间隙锁会导致锁的范围扩大
2.条件列未使用到索引时,RR锁表,RC锁行
3.RC的“半一致性”读(semi-consistent)可以增加update操作的并发性。
从上述三个问题中,看似RC的级别更有优势,其实在实际中,合理的加锁,在有索引的列加锁并不会出现上边的情况,通常使用默认的事务隔离级别RR就ok
了。