锁
1、为什么要加锁?
- 当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况,若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。
- 因此加锁是为了在多用户环境下保证数据库完整性和一致性。
2、MySQL都有哪些锁呢?
锁的分类:
- 按操作分类:
- 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据
- 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入
- 按粒度分类:
- 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM
- 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB
- 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般
- 按使用方式分类:
- 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁
- 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据
MyISAM和InnoDB存储引擎使用的锁:
- MyISAM采用表级锁(table-level locking)。
- InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
LOCK TABLE product_comment READ;
SELECT * FROM product_comment WHERE user_id = 912178 LOCK IN SHARE MODE
UNLOCK TABLE;
LOCK TABLE product_comment WRITE;
SELECT * FROM product_comment WHERE user_id = 912178 FOR UPDATE;
UNLOCK TABLE;
3、什么是乐观锁和悲观锁?如何实现 ?使用场景?
悲观锁:
- 概念:每次查询数据时都认为别人会修改,很悲观,所以一个事务拿到悲观锁后,其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。
- 数据库中的行锁,表锁,读锁,写锁均为悲观锁。
- 实现方式:使用数据库中的锁机制。
乐观锁:
- 概念:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新个数据;
- 实现方式:一般会使用版本号机制或 CAS 算法实现。具体来讲,在更新数据时,MySQL 会先检查当前数据的版本号,如果当前版本号与数据库中的版本号相同,则说明该数据没有被其他事务修改,可以更新该数据,并将版本号加 1,以此来表明数据已被变动;否则,说明该数据已被其他事务修改,不能更新该数据,此时 MySQL 会抛出异常。
悲观锁和乐观锁使用前提:
- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁;
- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁。
4、什么是死锁?如何解决死锁?
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法
- 1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
- 2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
- 3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
如果业务处理不好可以用分布式事务锁或者使用乐观锁。
排查死锁的一般步骤:
- 查看死锁日志 show engine innodb status;
- 找出死锁 sql;
- 分析 sql 加锁情况;
- 模拟死锁案发 ;
- 分析死锁日志;
- 分析死锁结果。
5、InnoDB引擎的行锁是怎么实现的?
行锁 ,也称为记录锁 ,顾名思义就是在记录上加的锁。对于 InnoDB 存储引擎来说,行级锁又分三种,或者说有三种行级锁算法。因此,即使对同一条记录加行锁 ,如果类型不同,起到的功效也是不同的。
InnoDB的行锁的主要实现如下:
- Record Lock 记录锁
记录锁就是直接锁定某行记录。当我们使用唯一性的索引(包括唯一索引和聚簇索引)进行等值查询且精准匹配到一条记录时,此时就会直接将这条记录锁定。例如 select * from t where id =6 for update; 就会将 id=6 的记录锁定。
- Gap Lock 间隙锁
间隙锁(Gap Locks) 的间隙指的是两个记录之间逻辑上尚未填入数据的部分,是一个左开右开空间。
间隙锁就是锁定某些间隙区间的。当我们使用等值查询或者范围查询,并且没有命中任何一个 record ,此时就会将对应的间隙区间锁定。例如 select * from t where id =3 for update; 或者 select * from t where id > 1 and id < 6 for update; 就会将(1,6)区间锁定。
- Next-key Lock 临键锁
临键指的是间隙加上它右边的记录组成的左开右闭区间。⽐如上述的(1,6]、(6,8]等。
临键锁就是记录锁(Record Locks)和间隙锁(Gap Locks)的结合,即除了锁住记录本⾝,还要再锁住索引之间的间隙。当我们使用范围查询,并且命中了部分 record 记录,此时锁住的就是临键区间。注意,临键锁锁住的区间会包含最后一个record的右边的临键区间。例如 select * from t where id > 5 and id <= 7 for update; 会锁住(4,7]、(7,+∞)。
MySQL默认行锁类型就是 临键锁(Next-Key Locks) 。当使用唯一性索引,等值查询匹配到一条记录的时候,临键锁(Next-Key Locks)会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。
6、隔离级别与锁的关系?
在读未提交级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
在读已提交级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
在可重复读级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
可串行化是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。
7、有哪些优化锁方面的意见?
优化建议:
- 对于特定的事务,可以使用表锁来提高处理速度或者减少死锁的可能;
- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁;
- 合理设计索引,尽量缩小锁的范围;
- 尽可能减少索引条件及索引范围,避免间隙锁;
- 尽量控制事务大小,减少锁定资源量和时间长度;
- 尽可能使用低级别事务隔离(需要业务层面满足需求)。
8、锁机制与 InnoDB 锁算法
MyISAM 和 InnoDB 存储引擎使用的锁:
- MyISAM 采用表级锁(table-level locking)。
- InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
表级锁和行级锁对比:
- 表级锁: MySQL 中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。
- 行级锁: MySQL 中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
InnoDB 存储引擎的锁的算法有三种:
- Record lock:记录锁,单个行记录上的锁;
- Gap lock:间隙锁,锁定一个范围,不包括记录本身;
- Next-key lock:record+gap 临键锁,锁定一个范围,包含记录本身。
9、什么是意向锁?
意向锁是表级锁的一种,它是由数据库引擎自行维护的,用户自己无需也无法操作意向锁。如果用户想要在表上面添加一个共享锁或者排他锁的时候,需要做如下两个检查:
- 检查这张表的排他锁有没有被其他事务占用,如果有,那么加锁失败;
- 检查这张表中的行锁有没有被其他事务占用,如果有,那么加锁失败。
如果一张表的数据量特别大,然后我们又想在这张表上添加一个表锁,如果一行一行地去遍历这张表的数据有没有被锁住,效率比较低下。意向锁的存在正是为了解决这个问题。
- 意向锁能够将检查行锁的时间复杂度由 O(n) 变成 O(1),其加锁的具体做法就是,当一个事务想要获取表中某一行的(共享/排他)锁的时候,它会自动尝试给当前表的加上意向(共享/排他)锁。然后,表锁和行锁之间的兼容互斥性就变成了表锁和意向锁之间的竞争关系
- 有了意向锁这个表级锁之后,则我们直接判断一次就知道表中是否有数据行被锁定了。
意向锁的兼容性和互斥性
- 意向锁之间互相兼容,因此针对表中的记录加锁不会因为意向锁而产生互斥,行锁之间的竞争关系是行锁与行锁的竞争,意向锁并不会参与其中;
- 意向锁只会与表级的共享 / 互斥锁具有互斥性,这里互斥的作用就是用于检查这张表中的行锁有没有被其他事务占用;
- 意向锁不会与行级的共享 / 排他锁互斥,如记录锁,间隙锁,临键锁。