为什么需要锁
锁机制用于管理对共享资源的并发访问。
锁不仅是数据库事务实现不同级别隔离性的手段,由其带来的所冲突也是影响数据库并发访问性能的一个重要因素。
MyISAM是表级锁,并发读没有问题,但并发插入性能较差。
Microsoft SQL Server数据库,在2005版本之前都是页锁,相对MyISAM并发性能有所提升,但对于热点数据页的并发问题依然无能为力。
InnoDB存储引擎锁的实现与Oracle非常相似,提供一致性的非锁定读、行级锁支持。
锁的基本原理
当我们说的MySQL锁住了这一行,其实并不会真正的锁住对应记录,而是锁住相应的索引,具体的锁哪些索引是根据查询条件来决定的。
因为MySQL的表的数据,本身通过1个或多个B+树索引来组织的。其中主键索引包含了完整的每一行的数据,非主键索引记录的是到行记录对应的主键索引的位置。所以,通过在索引记录上进行加锁操作,可以有效的读取、插入、修改、删除进行冲突判断。
如果这个表没有索引,是不是就加不了锁?这是不可能的,因为MySQL的每一个表至少有一个隐藏的聚簇索引,即主键索引。
锁的类型
行级锁
InnoDB存储引擎实现了如下两种标准的行级锁:
共享锁(S Lock),允许事务读一行的数据。
排它锁(X Lock),允许事务删除或更新一行的数据。
行锁即只会锁住一行,它的原理即是在对应的索引记录上加锁;
具有并发度高、锁冲突的概率低的优势,相对而言,由于粒度小,行锁成本高;
举例来说,当一行记录加了排它行锁的时候,其它事务是不能对这行记录进行修改的,但其他的行则不受此锁的影响。
但需要注意的是,在RR模型下进行更新一行记录时,如查询条件所在的列并无索引时,会退化成在主键索引对应的记录上全部加锁,即锁表。
表级锁
InnoDB同时支持表锁。表锁又分为表级读锁和表级写锁,具体语法是
LOCK TABLES XXX READ|WRITE
如表加了读锁时,这个表进入了只读模式,其它会话不能对此表进行修改;
如表加了写锁,则此表进入独占模式,其它表的读和写都会被阻塞,一般用于特殊的场景,如drop table或者truncate table的场景
表锁实现原理,不是由InnoDB存储引擎层管理的,而是由其上一层MySQL Server负责的。需要注意以下两点:
(1)需要设置autocommit=0,innodb_table_lock=1(也是默认设置),InnoDB层才能知道MySQL加的表锁
(2)在事务结束前,不要用UNLOCAK TABLES释放表锁,因为UNLOCK TABLES会隐含地提交事务;同时,COMMIT或ROLLBACK不能释放由LOCAK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁。
意向锁
InnoDB存储引擎支持意向锁的设计比较简练,其意向锁即为表级锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。
其支持两种意向锁:
意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。
意向排它锁(IX Lock),事务想要获得一张表中某几行的排它锁。
由于InnoDB存储引擎支持的是行级别锁,因此意向锁不会阻塞除全表扫描以外的任何请求。
InnoDB存储引擎中锁的兼容性
锁
IS
IX
S
X
IS
兼容
兼容
兼容
不兼容
IX
兼容
兼容
不兼容
不兼容
S
兼容
兼容
兼容
不兼容
X
不兼容
不兼容
不兼容
不兼容
简而言之,排它锁不兼容任何锁;意向排它锁不兼容共享锁;其他场景均可兼容。
自增锁
自增长是数据库一种常见属性,也是很多DBA或开发人员首选的主键方式。
在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter)。
当对含有自增长的计数器的表进行插入操作时,这个计数器会被初始化,执行如下的语句来得到计数器的值。
SELECT MAX(auto_inc_col) FROM t FOR UPDATE;
这个实现方式称作AUTO-INC Locking,这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。
锁的算法
Record Lock:单个行记录上的锁。
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身。
记录锁
Record Lock总是会锁住索引记录,如果没有设置索引,会使用隐式的主键来进行锁定。
间隙锁
间隙锁(gap lock)是RR模式下,为了防止幻读而设计出来的。它锁住的是一个区间(开区间),当一个区间被加了间隙锁时,是无法执行插入的。
它的实现原理是,在对应的索引记录范围进行加锁,是一个左右均是开区间。
TABLE `test` (
`id` bigint(20) unsigned NOT NULL COMMENT '主键',
`key` bigint(20) DEFAULT NULL COMMENT 'k',
PRIMARY KEY (`id`),
KEY `k` (`k`)
)
如果(2,2)和(6,6)之间被加了间隙锁,则事物2和事物3的插入,将会被阻塞,而事物1和事物4,则不会被阻塞。
需要注意的是,间隙锁本身之间是不会相互冲突的,它的唯一作用就是阻止在间隙内插入新的行。
临键锁
临键锁即next-key lock,是行锁和它之前的间隙共同构成的锁,即一个前开后闭的加锁区间。
然而,当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,降级为Record Lock,即仅锁住索引本身,而不是范围。
需要注意的是,对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型,而不是point类型,依然会使用Next-Key Lock进行锁定。
由于间隙锁和临键锁的加锁规则比较复杂,这里引用林晓斌的总结为2个原则,2个优化,1个bug:
原则1:RR模式加锁的基本单位是next-key lock,即前开后闭的区间
原则2:查找过程中访问到的对象才会加锁
优化1:唯一索引上的等值查询,next-key lock会退化为行锁
优化2:索引上的等值查询,向右遍历的时候,最后一个值不满足等值条件的时候会退化为间隙锁。
一个bug:唯一索引上的范围查询,会访问到不满足条件的第一个值为止。
这里举例来说明
TABLE `test` (
`id` int(11) unsigned NOT NULL COMMENT '主键',
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `k` (`c`)
)
insert into t values (0,0,0),(5,5,5), (10,10,10),(15,15,15), (20,20,20),(25,25,25)
例子一:唯一索引查询间隙锁。sql如下:
update t set d=d+1 where id=7;
根据加锁规则1,加锁范围是(5,10]
同时由于优化2,最终的加锁范围是(5,10).
例子二:非唯一索引等值锁
select id from t where c=5 lock in share mode;
1)根据加锁原则1,加锁区间是(0,5]。
2)但由于c是普通索引,需要继续向右遍历,直到查询到c=10才会放弃
3)根据优化2,由于10不满足等值判断,因此会退化成间隙锁(5,10)
4)根据原则2,只有访问到的对象才会加锁,这个查询本身是有覆盖索引,并不需要访问主键,因此id主键上并没有锁。因此不会阻塞如下sql
update t set d=d+1 where id=5
例子三:主键索引范围锁
考虑如下两个SQL,语义上等同的,但是对加锁的效果确是不一样的。
select * from t where id=10 for update;
select * from t where id>=10 and id < 11 for update;
我们都知道第一条语句,会退化成id=10的行锁。
第二条语句,其实是id=10和 10
锁问题
脏读
脏页:在缓冲池中已经被修改的页,但是还没刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。
而所谓脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。在一个事务中可以读到另一个事务中未提交的数据(脏数据),显然违反了数据库的隔离性。
主从拷贝过程的slave节点,并且slave节点上的查询不需要特别精确的返回值的情况下,可以采用脏读隔离级别。
脏读本身违反了事务的隔离性。
不可重复读
不可重复读是指在一个事务内多次读取到同一数据集合(范围查询),且两次读到的数据可能不一样(其他事务对部分数据进行修改),这种情况称为不可重复读。
不可重复读本身违反了事务的一致性。
丢失更新
丢失更新是另一个锁导致的问题,简单来说就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。
事务T1将行记录R更新为V1,但不提交。
事务T2将行记录R更新为V2,但不提交。
事务T1提交。
事务T2提交。
此时事务T1的更新就被事务T2覆盖了,但事实上,在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。
但可能存在逻辑上的丢失更新,将上述过程想象成,用户查询数据到界面,更新数据,但未提交至数据库。