一、需要场景
在最大程度利用数据库的并发访问时候,需要确保每个用户能以一致的方式读取和修改数据。本质上:锁机制是用于管理对共享资源的并发访问。
二、锁的种类对比
lock | latch | |
对象 | 事务 | 线程 |
保护 | 数据库内容 | 内存数据结构 |
持续时间 | 整个事务过程 | 临界资源 |
模式 | 行锁、表锁、意向锁 | 读写锁、互斥量 |
死锁 | 通过waits-for graph、time out 等机制进行死锁检测和处理 | 无死锁检测与处理机制,仅通过应用程序加锁的顺序保证无死锁的情况发生 |
存在于 | lock manager的哈希表中 | 每个数据结构的对象中 |
三、锁的相关定义
- 行级锁 (共享锁和排他锁)
只有对同一行记录,共享锁是兼容的,其他彼此都是不兼容的(需要依次获得该行上的锁)。
- 表级锁 (意向共享锁和意向排他锁)
意向锁:将锁定的对象分多个层次,希望在更细粒度加锁(即行级)。如果对细粒度上锁,则首先需要对粗粒度的对象上锁,
目的:主要是为了在一个事务中揭示下一行将被请求的锁类型。
- 一致性非锁定读
innodb存储引擎通过多版本的控制的方式,来读取当前执行时间数据库中行的数据,如果读取的行正在执行delete或update操作,这时读操作不会因此去等待行上的排他锁(x锁)的释放,相反,innodb存储引擎会去读取行的一个快照数据。(具体实现是通过uodo来实现的,后面事务单独讲)。
对于快照版本,不同的事务隔离级别定义不同,
read committed (读已提交)表示被锁定行最新一份快照数据,
举例:如果查询后,有修改并提交,再次查询,是最新的修改后的数据。
repeatable read(可重复读)表示读取事务开始时的行数据版本。mysqlinnodeb的默认配置
举例:查询后,会记录最开始行的版本数据,已经锁定,无论后期数据修改,都不会变。
- 一致性锁定读
显式的对数据库读取操作进行加锁保证数据逻辑的一致性。是对数据行本身进行加锁,满足排他锁和共享锁的兼容特性。
a、select ... for update b、 select ... lock in share mode
四、锁的算法
1、record lock(单个行记录上的锁):锁定索引记录
如果表在简历的时候没有设置任何一个索引,那么会使用隐世的主键来进行锁定。
2、gap lock:间隙锁:锁定一个范围,但不包含记录本身
作用:为了阻止多个事务降记录插入到同一范围内,从而产生幻读。
3、next-key lock:临键锁:锁定一个范围,并且锁定记录本身 (对于行的查询都是采用该算法)
采用该锁,主要目的是为了解决幻读问题,
例如一个索引有10,13,20这三个值,那给索引可能被next-key locking的区间为 (-无穷,10],(10,13],(13,20],(20,+无穷]。
a、当查询的索引含有唯一属性时,next-key lock进行优化,将其降级为 record lock,即锁住索引本身,而不是范围。
栗子:
create table t ( a int primary key);
inset into t select 1;
inset into t select 2;
inset into t select 5;
按照如下步骤进行:
表t共有1,2,5三个值,在上面,回话a中首先对a = 5 进行x锁定,,由于采用next-key lock 锁,所以锁定的范围为(2,5],由于a是主键且唯一,锁优化降级为record lock,仅锁定5的这个值,,这样会在b会话插入值4的时候不会阻塞,可以立即插入并返回。这样提高了应用的并发性。
b、当存在辅助索引,则仍然是next-key lock,而且会对辅助索引的下一个键值加上gap lock(空隙锁),
栗子:
create table z ( a int ,b int, primary key(a),key(b));
inset into z select 1,1;
inset into z select 3,1;
inset into z select 5,3;
inset into z select 7,6;
inset into z select 10,8;
表z中的b是辅助索引,若在会话a中执行:select * from z where b =3 for update,这时候sql通过b进行查询,因此使用next-key locking技术加锁,因为有两个索引,需要分别锁定,。对于列a=5的索引加record lock,对于辅助索引,加next-key lock,锁定的范围为(1,3),特别注意的是,存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁,因此,在新的会话b中运行下面的语句,都会被阻塞:
select * from z where a = 5 lock in share mode;
insert into z select 4,2;
insert into z select 6,5;
第一个sql是因为在a=5上加x锁,第二个是辅助索引值2在锁定范围(1,3)。第三个sql是因为5在间隙锁(3,6)内。
下面的语句,则会立即执行:
insert into z select 8,6;
insert into z select 2,0;
五、锁问题
1、脏读:不同事务下,当前事务可以读取到另一个事务未提交的数据。
示例:
time | 会话a | 会话b |
1 | set session transaction isolation level read uncommitted; | |
2 | set session transaction isolation level read uncommitted; | |
3 | begin; | |
4 | select * from z where b = 1; (两条数据) | |
5 | insert into z select 9,1; | |
6 | select * from z where b = 1; (三条数据) |
在replication(复制)环境中的slave节点,不需要做精准查询,可以使用这个。
2、不可重复读(幻读):在一个事务内多次读取统一数据集合,在这个事务还没有结束时,另一个事务也访问该统一数据集合,做dml操作,导致第一个事务两次读取的数据不一样。
示例
time | 会话a | 会话b |
1 | set session transaction isolation level read committed; | |
2 | set session transaction isolation level read committed; | |
3 | begin; | begin; |
4 | select * from t; | |
5 | insert into t select 3; | |
6 | commit; | |
7 | select * from t; (多出了 “3” 这条数据) |
在mysql的innodb中,使用next-key lock算法避免了不可重复读。这时候mysql默认设置的事务隔离级别为“read repeatable”;
不可重复读下,还存在着修改数据提交后被另一个事务读到的情况。
3、丢失更新:一个事务的更新操作会被另一个事务的更新操作所覆盖,导致数据的不一致。
示例:
time | 会话a | 会话b |
1 | begin; | begin; |
2 | select * from z where a = 10 ; | |
3 | select * from z where a = 10 ; | |
4 | .... | ......... |
5 | update z set b = 1 where a = 10; commit; | |
6 | update z set b = 100 where a = 10; commit; | |
7 | select b from z where a = 10 |
分别查询出结果后,分别处理业务逻辑更新操作,会导致最后的结果时最后一个事务提交后的结果,
避免方法:在查询时候显示的使用for update,锁住该行。
六、阻塞问题
定义:因为锁之间的兼容性关系,在某些时刻,一个事务中的锁需要等待另一个事务的锁释放它所占的资源,这就是阻塞。
注:默认情况下,innodb不会回滚超时引起的错误异常,其实对大部分情况都不会回滚,会话如果执行成功一部分,则最后不会回滚,必须注意。
参考资料:《mysql技术内幕++innodb存储引擎》