锁机制
锁机制
当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制。
在数据库中,除了传统的计算资源(CPU、RAM、I/O等)的争用外,数据也是一种供需要用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题。
锁冲突也是影响数据库并发访问性能的一个重要因素。
锁分类
从性能上分,分为乐观锁(用版本对比或者CAS机制)和悲观锁;从数据操作的粒度上分,分为表锁、页锁、行锁;从数据操作的类型上分,分为读锁、写锁和意向锁,读锁和写锁两种锁都属于悲观锁
乐观锁
适合读多写少的场景,如果在写操作多的场景下使用,会导致比对次数过多从而影响性能
悲观锁
适合写操作多的场景
表锁
锁定粒度最大的一种锁,对当前操作的整张表加锁。实现简单,加锁力度大,资源消耗较小,加锁快,不会出现死锁,并发度最低。大部分引擎都支持。一般用于表迁移。
叶锁
粒度介于表锁和行锁之间,锁定的是相邻的一组记录(一页)。基于索引加锁,锁粒度介于表锁和行锁之间,会出现死锁,并发度一般。只有BDB支持叶锁。
行锁
粒度最小的一种锁,只针对当前操作的行加锁。基于索引加锁,锁粒度最小,资源开销大,加锁慢,会出现死锁,并发度最高。Innodb支持行锁。
InnoDB的行锁实际上是针对索引加的锁(在索引对应的索引项上做标记),不是针对整个行记录加的锁。该索引不能失效,否则会从行锁升级为表锁(RR级别会升级为表锁,RC级别不会)。
例如:
select * from user where name='zhangsan' for update;
如果where条件中name字段没有索引,其他Session对该表任意一行做修改操作都会被阻塞住。
RR级别行锁升级为表锁的原因分析:
RR级别下,需要解决不可重复读和幻读问题,所以在便利扫描聚集索引时,为了防止扫描过的索引被其他事务修改(不可重复读)或间隙被其它事务插入新数据(幻读),从而导致数据不一致。
所以MySQL的解决方案就是把所有扫描过的索引记录和间隙都上锁(并不是直接将整张表加表锁,因为不一定能加上表锁,可能会有其他事务已经锁住了表里的某些行数据)。
读锁
又称共享锁,简称S锁(Shared)。它允许多个事务对统一数据进行读取,但不能进行修改。对同一个数据,多个读操作可以同时进行,互不干扰。可以通过添加lock in share mode来加读锁。
写锁
又称排他锁,简称X锁(Exclusive)。它允许获取排它锁的事务读取和修改数据,阻止其他事务取得相同数据集的共享锁或排它锁。如果当前写操作没有完毕,则无法进行其他的读、写操作。数据新增、修改都会加写锁,查询也可以通过加for update来加写锁。
读锁会阻塞写,但是不会阻塞读;写锁则会把读、写都阻塞。
意向锁
属于InnoDB引擎中的一种机制。当有事务给表的数据加了锁(读锁或写锁),同时会给表设置一个 标识(意向锁) 表示表已经有了行锁。当其他事务要对表进行加表锁时,就不用便利表数据判断数据有没有行锁会跟表锁冲突了,直接读取这个标识就可以进行判断是否可以加表锁。在表中记录很多时,逐行判断加表锁的方式效率很低。
意向锁由分为:
- 意向共享锁(IS): 事务有意向对表中的某些行加共享锁(S锁)。
- 意向排它锁(IX): 事务有意向对表中的某些行加排它锁(X锁)。
间隙锁(Gap Lock)
间隙锁锁的是两个值之间的空隙,间隙锁是在可重复读隔离级别下才会生效。 对于RR级别下幻读的问题,可以通过间隙锁可以解决。
假设user表有如下数据:
id | name | age |
---|---|---|
1 | lilei | 5 |
2 | zhangsan | 6 |
5 | lisi | 7 |
18 | wangwu | 8 |
则此表的间隙有id为(2,5)、(5,18)、(18,+∞)这三个区间(开区间),当某个连接执行
select * from user where id = 3 for update;
则其他的连接将没法在(2,5)这个间隙范围里插入任何数据。
如果执行的是
select * from user where id = 20 for update;
则其他的连接将没法在(18,+∞)这个间隙范围里插入任何数据。
也就是说,只要在间隙范围内锁了一条不存在的记录就会锁住整个间隙范围,不锁边界记录,这样就能防止其他的连接在这个间隙范围内插入数据,就解决了RR级别的幻读问题。
临建锁(Next-key Locks)
临建锁是行锁于间隙锁的组合,行锁锁间隙两头的行。
在MySQL中,当你执行范围查询时,例如`SELECT…从表value1和value2; '之间的列中,InnoDB不仅锁住了满足查询条件的行(键),还锁住了键之间的间隔或范围。这被称为下一键锁定或间隙锁定。
临建锁目的是防止其他事务插入原本应该包含在原始范围查询中的新行,从而导致幻影读。当一个事务检索一个范围的行,而另一个事务在第一个事务提交或回滚之前将新行插入到范围中时,就会发生幻影读取,导致第一个事务看到一个在最初读取范围时并不存在的“幻影”行。
通过锁定键之间的空隙,InnoDB确保了没有新行被插入到被查询的范围中,保持了事务的一致性和可序列化性。
当您需要在特定的值范围内处理频繁的插入、更新和删除时,下一键锁特别有用。它们保证范围查询的结果集在整个事务执行过程中保持稳定,即使其他事务修改了该范围内的数据。
值得注意的是,下一个键锁定可能会导致锁争用增加和并发性降低,特别是在涉及许多范围查询和相同范围内并发修改的工作负载中。在这种情况下,您可能需要考虑其他技术,如应用程序级别的锁或分区,以平衡并发和一致性需求。
MyISAM与InnoDB锁实现
MyIsAM 在执行查询语句之前,会自动给设计的所有表加读锁,在执行insert、update、delete操作会自动给设计的表加写锁。
InnoDB在执行查询语句(非串行化隔离级别),不会加锁,但是insert、update、delete操作会加行锁。
InnoDB由于实现了行级锁,虽然在锁定机制的实现方面锁带来的性能损耗可能会比表锁更高,但整体并发处理能力要远远优于MyISAM的表级锁定。当系统并发量高时,InnoDB的整体性能额MyISAM相比就会有比较明显的优势了。
但是,InnoDB的行级锁定同样有不好的地方,当我们使用不当,可能就会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至有可能会更差。
死锁
大多数情况下MySQL可以自动检测死锁并回滚产生死锁的事务,但是在某些情况MySQL是没法自动检测的,这就需要我们可以通过日志找到对应的事务线程ID,通过ID来kill杀掉事务。
锁优化实践
- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁。
- 合理设计索引,尽量缩小锁的范围
- 尽可能减少索引条件范围,避免间隙锁
- 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的SQL尽量放在事务最后执行
- 尽可能用低的事务隔离级别