目录
一、读锁(又叫共享锁、S锁)和写锁(又叫排它锁、X锁)
共享锁:用于不更改数据的操作,如select操作。
排它锁:用于数据修改操作,如 insert、update、delete,确保同一时间内,加锁的数据只被一个事务修改,不会有多个事务同时修改这个数据。
事务与锁
如果一个事务T对A数据上共享锁后,其他事务只能对A上共享锁,不能上排它锁。获得共享锁的事务只能读该数据,而不能修改该数据。
如果一个事务T对A数据上的是排它锁的,那么其他事务不能对A数据上任何类型的锁。而获得排它锁的事务既可以读取该数据,也可以修改该数据。
什么时候上锁
这跟数据库引擎有关(即InnoDB还是Myisam)
InnoDB引擎:
1.对于UPDATE,INSERT,DELETE语句,InnoDB会自动给设计的数据加排它锁。
2.对于普通的 SELECT语句,InnoDB不会加任何锁。
3.事务可以通过一下语句,显示地给数据加共享锁或排它锁:
共享锁:如: SELECT * from table_name where .... LOCK IN SHARE MODE。 其他session 仍可以查询数据,并也可以对该记录加 share mode 的共享锁。
排它锁:如: SELECT * from table_name where .... FOR UPDATE。其他session可以查询该数据,但不能对A数据加写锁或者读锁。
Myisam引擎:
Myisam会在执行 SELECT语句时给相关的数据表自动加共享锁。(而InnoDB执行 SELECT是不会加任何锁的)
在执行 UPDATE、INSERT、DELETE时,会自动给相关数据表加排它锁。
二、行锁和表锁
谈到mysql,就很容易想到它的两个常用的数据表引擎---InnoDB和Myisam。InnoDB是支持行锁和表锁的,但是默认是行锁。而Myisam只支持表锁。
行锁:只锁定操作数据所在的那一行数据。默认的是排它锁。就是本事务锁定的内容,其他事务能查询,但不能加共享锁和排它锁。
表锁:锁定数据所在的整张数据表。加锁的方式:自动加锁。查询操作(SELECT),会自动给涉及的所有表加读锁,更新操作(UPDATE、DELETE、INSERT),会自动给涉及的表加写锁。也可以显示加锁:
行锁和表锁的优缺点:
行锁:
优点:并发度高。
缺点:开销大,加锁慢,容易死锁。
表锁:
优点:开销小,加锁快,不会死锁。
缺点:并发度低。
默认情况下,表锁和行锁都是自动获得的, 不需要额外的命令。
行锁触发死锁的原因
从操作系统的原理来看,触发死锁肯定都满足以下四个条件:
1.互斥
2.不可剥夺
3.请求与保持
4.循环等待
如下图,就死锁了,事务A给id=1的数据上锁,然后事务B给id=2的数据上锁,然后事务A请求id=2的数据,但事务B又请求id=1的数据,事务A,B都不释放锁,当又请求其他事务上锁的数据。就造成了死锁。
表锁为什么不会触发死锁?
从操作系统的原理来看,触发死锁肯定都满足以下四个条件:
1.互斥
2.不可剥夺
3.请求与保持
4.循环等待
既然表锁不会发生死锁,那肯定不满足上面的一个或多个条件。我们一个个来看,互斥和不可剥夺,肯定满足的,那么请求与保持呢,我们假设一下,如果满足了请求与保持,那么循环等待是不是一定也会满足,因为数据库不可能只有一个事务的啊,所以肯定是不满足请求与保持条件了。我们 再看看表锁的上锁原理:执行操作时,会自动给涉及的所有表加锁。那就意味着,在做操作前,会把需要的表都锁上,都拿到那些表的锁,那就不存在请求与保持了,因为你都把你需要的东西都拿到了,你还请求什么呢?所以表锁不会发生死锁。
行锁细分---记录锁、间隙锁、临键锁
1.记录锁
注意:记录锁、间隙锁、临键锁都是排它锁。所以行锁肯定也是排它锁。
记录锁就是最普通的行锁,它只锁住某一行。
如下图,锁住的是id=1的数据行。
需要注意的是:
1. 上面的 id 列必须是 唯一索引 或者 主键。否则会退化成临键锁。(所以记录锁是 基于 唯一索引的锁)
2. 查询条件也必须是精确查询(=),而不能是 >、<、like等范围的,否则也会退化成临键锁。
2. 间歇锁
间歇锁锁住的是一个区间。
间歇锁可以由唯一索引触发,也可以由非唯一索引触发。
唯一索引触发:
结论:
1.对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁和间隙锁,如果记录存在,则只会产生记录锁,如:WHERE `id` = 5 FOR UPDATE;(其中id=5的数据存在)
2.对于查找某一范围内的查询语句并加锁,会产生间歇锁。如:WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;
1-例子:假设我们有如下表,只有两个字段,主键id 和 name:
如果我们试图锁住不存在的 id = 3的数据:
则会锁住 id区间(2,5),即不允许 INSERT 语句插入 id = 2~5 的数据。所以可以看出来间歇锁是锁住 唯一索引的前后区间的。
2-例子:同样假设我们有 1-例子的数据表。
然后对唯一索引使用范围加锁。
这时候会锁住 (5,7] 和 (7,11] 的区间,这两个区间都是不允许插入数据的。
非唯一索引触发:
实验的数据表:
两个字段,第一个是主键id,第二个是number字段,且在number字段中创建了一个普通索引。
结论:
1. 普通索引上,只要加锁了,肯定会触发间隙锁,这跟唯一索引不一样。
2. 如果在INSERT操作中,同时包含了唯一索引和非唯一索引,那么在分析间隙区间时就要注意了,因为这时候INSERT阻不阻塞不仅仅跟非唯一索引有关,还跟唯一索引有关。用2-例子说明把。
1-例子:在实验数据表上开启开启事务1:
在number的范围[1,8)上 INSERT数据会阻塞。
2-例子:
开启事务操作如下:给非唯一索引number=5的数据加锁:
INSERT数据格式如下:INSERT时把唯一索引和非唯一索引都用到:
结果如下图:蓝色的为本来就在数据表中的数据,红色的是尝试INSERT进去的数据。
通过经过我们可以看到,红色部分,同样number都是8,但是id=6的却插入时阻塞了,但是id=8的却插入成功了。那是因为插入数据时,会把所插入的数据在原有 的数据表中做一个排序,首先通过非唯一索引number排,若number相同再通过唯一索引id排。可以看到id=6的数据其实是处于间隙锁范围内的,所以才插入不成功。
3. 临键锁
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
注:临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
每个数据行上的非唯一索引列
上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB
中行级锁
是基于索引实现的,临键锁只与非唯一索引列
有关,在唯一索引列
(包括主键列
)上不存在临键锁。
这章要点:
- 记录锁、间隙锁、临键锁,都属于排它锁;
- 记录锁就是锁住一行记录;
- 间隙锁只有在事务隔离级别 RR 中才会产生;
- 唯一索引只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁,指定给某条存在的记录加锁的时候,只会加记录锁,不会产生间隙锁;
- 普通索引不管是锁住单条,还是多条记录,都会产生间隙锁;
- 间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其它事务在这个区域内插入、修改、删除数据,这是为了防止出现 幻读 现象;
- 普通索引的间隙,优先以普通索引排序,然后再根据主键索引排序;
- 事务级别是RC(读已提交)级别的话,间隙锁将会失效。
三、 意向锁
意向锁分为 共享意向锁(IS锁)和排它意向锁(IX锁)。意向锁是数据引擎自己维护的,用户无法自己操作意向锁。在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在数据表的对应意向锁。
存在意义
InnoDB为了让表锁和行锁共存而使用了意向锁。而且,意向锁是表锁。意向锁不与行锁冲突,只与表锁冲突。
为什么没有意向锁的话,行锁和表锁就不能共存
例如,如果事务A要锁住某个表的某行数据, 但是事务B要锁住整个表。那么问题就是,事务A既然锁住了一行数据,那么那一行数据是不允许别的事务修改的,但是事务B锁住了整个表,那按理说事务B能修改表上的任何数据,所以如果没有意向锁的话,行锁和表锁就会出现这样的问题。
意向锁如何让行锁和表锁共存?
有了意向锁后,当事务A去申请一张数据表的某行数据时(行锁---排它锁)之前,数据库会自动给这张数据表上意向排它锁。这个时候如果事务B再去申请这张数据表的表锁(排它锁)时,事务B就会阻塞。
下图是意向锁和表锁的互斥和兼容关系:
例如,如果一个数据表已经上了意向共享锁(IS),那么这时候有别的事务给他上一个共享的表锁,那他们是可以共存的。但是如果上的是排它的表锁,那就是不能共存的。
注意:这里的排他 / 共享锁指的都是表锁!!!意向锁不会与行级的共享 / 排他锁互斥!!!
四、乐观锁和悲观锁
上面所说的都属于悲观锁。
悲观锁
悲观锁是对于数据的处理持悲观态度,总认为会发生并发冲突,获取和修改数据时,别人会修改数据。所以在整个数据处理过程中,需要先将数据锁定。
悲观锁的实现,通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select .... for update来实现悲观锁。
乐观锁
顾名思义,就是对数据的处理持乐观态度,乐观的认为处理数据时一般情况下不会发生并发冲突,只有提交数据更新时,才会对数据是否冲突进行检测。
乐观锁的实现,是不基于mysql提供的锁机制的,需要我们自已实现,实现方式一般是记录数据版本,一种是通过版本号,一种是通过时间戳。
具体方式:
给表加一个版本号或时间戳的字段,
1. 读取数据时,将版本号一同读出
2. 无论哪个事务更新更新数据时,都将版本号加1。
假设现在有个事务A要更新某行数据,它会先:
1. 查询准备修改的数据 和 得到它的版本号a
2. 这时候如果有其他事务B修改了这行数据的话,事务B肯定会给这行数据的版本号加1.
3. 然后当事务A开始修改这行数据时,再获取一次这行数据的版本号,发现它已经变了(被事务B修改了),那么事务A就不会继续修改这行数据,因为出现并发冲突了。反之如果事务A发现再次获取的版本号跟第一步获得的版本号一致,则说明,没有其他事务修改过这行数据,没有发生并发冲突,这时候事务A就会执行修改命令,修改这行数据了。
总结:
1.悲观锁使用了排他锁,当程序独占锁时,其他程序就连查询都是不允许的,导致吞吐较低。如果在查询较多的情况下,可使用乐观锁。
2.乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入较频繁,对吞吐要求不高,可使用悲观锁。
也就是一句话:读用乐观锁,写用悲观锁。