MySQL锁相关知识点
MySQL 锁
MySQL 的锁机制,主要用于高并发场景下,比较重要的知识点是保证数据的一致性的和事务隔离性,在高并发下控制并发访问。
锁的种类
MySQL 中锁的分类按照不同类型的划分可以分成不同的锁,
- 按照锁的粒度:
- 表锁:粒度最大的锁,开销小,加锁快,不会出现死锁,但是由于粒度太大,因此造成锁的冲突几率大,并发性能低。常见的有 MyISAM 储存引擎就支持表锁,MyISAM的表锁模式有两种:表共享读锁和表独占写锁
当一个线程获取到 MyISAM 表的读锁的时候,会阻塞其他用户对该表的写操作,但是不会阻塞其它用户对该用户的读操作。
相反的,当一个线程获取到 MyISAM 表的写锁的时候,就会阻塞其它用户的读写操作对其它的线程具有排它性。
- 页锁:粒度是介于行锁和表锁之间的一种锁,页锁是在 BDB 中支持的一种锁机制,很少提及和使用
- 行锁:粒度最小的锁机制,行锁的加锁开销性能大,加锁慢,并且会出现死锁,但是行锁的锁冲突的几率低,并发性能高。
行锁是 InnoDB 默认的支持的锁机制,MyISAM 不支持行锁,这个也是 InnoDB 和 MyISAM 的区别之一。
行锁在使用的方式上可以划分为:共享读锁( S 锁)和排它写锁( X 锁)。
当一个事务对 MySQL 中的一条数据行加上了 S 锁,当前事务不能修改该行数据只能执行读操作,其他事务只能对该行数据加 S 锁不能加 X 锁。若是一个事务对一行数据加了 X 锁,该事务能够对该行数据执行读和写操作,其它事务不能对该行数据加任何的锁,既不能读也不能写。
- 表锁:粒度最大的锁,开销小,加锁快,不会出现死锁,但是由于粒度太大,因此造成锁的冲突几率大,并发性能低。常见的有 MyISAM 储存引擎就支持表锁,MyISAM的表锁模式有两种:表共享读锁和表独占写锁
- 按照使用的方式:共享锁和排它锁;
- 按照思想:数据库管理系统中为了控制并发,保证在多个事务执行时的数据一致性以及事务的隔离性,使用悲观锁和乐观锁来解决并发场景下的问题。
- 乐观锁:需要程序员自己去实现的锁机制
- 悲观锁:悲观锁的实现是基于Mysql自身的锁机制实现
悲观锁和乐观锁是在很多框架都存在的一种思想,不要狭义地认为它们是某一种框架的锁机制。
MyISAM
MyISAM中 默认支持的表级锁有两种:共享读锁和独占写锁。表级锁在 MyISAM 和 InnoDB 的存储引擎中都支持,但是 InnoDB 默认支持的是行锁。
MySQL 中平时读写操作都是隐式的进行加锁和解锁操作,MySQL 已经自动实现加锁和解锁的操作,若是想要测试锁机制,就要显示的自己控制锁机制。
MySQL 中可以通过以下 sql 来显示的在事务中显式的进行加锁和解锁操作:
-- 显式的添加表级读锁 LOCK TABLE 表名 READ -- 显示的添加表级写锁 LOCK TABLE 表名 WRITE -- 显式的解锁(当一个事务commit的时候也会自动解锁) unlock tables;
MyISAM 表级写锁
CREATE TABLE IF NOT EXISTS employee ( id INT PRIMARY KEY auto_increment, name VARCHAR(40), money INT )ENGINE MyISAM INSERT INTO employee(name, money) VALUES('黎杜', 1000); INSERT INTO employee(name, money) VALUES('非科班的科班', 2000);
LOCK TABLE employee WRITE
select * from employee # 阻塞
第一个程序可以任意读写该表,但其他程序不行。只有在锁被释放时,别的程序才能对该表进行操作
MyISAM表级共享读锁
测试数据同上
LOCK TABLEemployee read;
第一个程序进行插入、更新数据,发现都会报错,只能查询数据。
其他程序也无法进行插入、更新数据,但是可以查询数据。
MyISAM表级锁竞争情况
MyISAM存储引擎中,可以通过查询变量来查看并发场景锁的争夺情况
show status like 'table_locks%';
通过 table_locks_waited
和 table_locks_immediate
的值的大小分析锁的竞争情况。
Table_locks_immediate
:表示能够立即获得表级锁的锁请求次数;Table_locks_waited
表示不能立即获取表级锁而需要等待的锁请求次数分析,值越大竞争就越严重。
并发插入
上述例子中的加锁和释放锁都是在 MySQL 中已经实现了的隐式的操作,实际并不会这么做的。
MyISAM 存储引擎中,虽然读写操作是串行化的,但是它也支持并发插入,这个需要设置内部变量 concurrent_insert
的值。它的值有三个值0、1、2
。可以通过以下的 sql 查看 concurrent_insert
的默认值为AUTO(或者1)。
- 值为
NEVER (or 0)
表示不支持比并发插入; - 值为
AUTO (或者1)
表示在MyISAM表中没有被删除的行,运行另一个线程从表尾插入数据; - 值为
ALWAYS (or 2)
表示不管是否有删除的行,都允许在表尾插入数据。
show variable like '%concurrent_insert';
锁调度
MyISAM 存储引擎中,假如同时一个读请求,一个写请求过来的话,会优先处理写请求,因为 MyISAM 存储引擎中认为写请求比读请求重要。这样就会导致,假如大量的读写请求过来,就会导致读请求长时间的等待,或者"线程饿死",因此 MyISAM 不适合运用于大量读写操作的场景,这样会导致长时间读取不到用户数据,用户体验感极差。
可以通过设置low-priority-updates
参数,设置请求链接的优先级,使得 MySQL 优先处理读请求。
InnoDB
InnoDB 和 MyISAM 不同的是,InnoDB 支持行锁和事务
InnoDB 中除了有表锁和行级锁的概念,还有 Gap Lock(间隙锁)、Next-key Lock锁,间隙锁主要用于范围查询的时候,锁住查询的范围,并且间隙锁也是解决幻读的方案。
InnoDB 中的行级锁是对索引加的锁,在不通过索引查询数据的时候,InnoDB 就会使用表锁。但是通过索引查询的时候是否使用索引,还要看 MySQL 的执行计划,MySQL 的优化器会判断是一条 sql 执行的最佳策略。若是 MySQL 觉得执行索引查询还不如全表扫描速度快,那么 MySQL 就会使用全表扫描来查询,这是即使 sql 语句中使用了索引,最后还是执行为全表扫描,加的是表锁。
InnoDB 的行锁与表锁
InnoDB的行锁也是分为行级共享读锁(S锁)和排它写锁(X锁),原理特点和 MyISAM 的表级锁两种模式是一样的。
- 执行非索引条件查询执行的是表锁。
- 执行索引查询是否是加行锁,还得看 MySQL 的执行计划,可以通过 explain 关键字来查看。
- 用普通键索引的查询,遇到索引值相同的,也会对其他的操作数据行的产生影响。
InnoDB 的间隙锁
当使用范围条件查询而不是等值条件查询的时候,InnoDB 就会给符合条件的范围索引加锁,在条件范围内并不存的记录就叫做"间隙(GAP)"
在事务的四大隔离级别中,不可重复读会产生幻读的现象,只能通过提高隔离级别到串行化来解决幻读现象。但是 MySQL 中的不可重复是已经解决了幻读问题,它通过引入间隙锁的实现来解决幻读,通过给符合条件的间隙加锁,防止再次查询的时候出现新数据产生幻读的问题。
- 主键索引不需要间隙锁——主键索引具有唯一性,不允许出现重复,那么当进行等值查询的时候id=3,只能有且只有一条数据,是不可能再出现id=3的第二条数据。因此它只要锁定这条数据(锁定索引),在下次查询当前读的时候不会被删除、或者更新id=3的数据行,也就保证了数据的一致性,所以主键索引由于他的唯一性的原因,是不需要加间隙锁的。
- 范围查询会加上间隙锁
- 是用不存在的检索条件,会使用间隙锁
死锁
死锁在 InnoDB 中才会出现死锁,MyISAM 是不会出现死锁,因为 MyISAM支持的是表锁,一次性获取了所有的锁,其它的线程只能排队等候。而 InnoDB 默认支持行锁,获取锁是分步的,并不是一次性获取所有的锁,因此在锁竞争的时候就会出现死锁的情况。虽然 InnoDB 会出现死锁,但是并不影响 InnoDB 成为最受欢迎的存储引擎,MyISAM 可以理解为串行化操作,读写有序,因此支持的并发性能低下。
当一个事务开始并且update一条id=1的数据行时,成功获取到写锁,此时另一个事务执行也update另一条id=2的数据行时,也成功获取到写锁(id为主键)。 此时cpu将时间分配给了事务一,事务一接着也是update id=2的数据行,因为事务二已经获取到id=2数据行的锁,所以事务已处于等待状态。 事务二有获取到了时间,像执行update id=1的数据行,但是此时id=1的锁被事务一获取到了,事务二也处于等待的状态,因此形成了死锁。
死锁的解决方案
要解决死锁问题,在程序的设计上,当发现程序有高并发的访问某一个表时,尽量对该表的执行操作串行化,或者锁升级,一次性获取所有的锁资源。也可以设置参数innodb_lock_wait_timeout
,超时时间,并且将参数innodb_deadlock_detect
打开,当发现死锁的时候,自动回滚其中的某一个事务。
总结
MyISAM的表锁分为两种模式:「共享读锁」和「排它写锁」。获取的读锁的线程对该数据行只能读,不能修改,其它线程也只能对该数据行加读锁。获取到写锁的线程对该数据行既能读也能写,对其他线程对该数据行的读写具有排它性。
MyISAM中默认写优先于去操作,因此MyISAM一般不适合运用于大量读写操作的程序中。
InnoDB的行锁虽然会出现死锁的可能,但是InnoDB的支持的并发性能比MyISAM好,行锁的粒度最小,一定的方法和措施可以解决死锁的发生,极大的发挥InnoDB的性能。
InnoDB中引入了间隙锁的概念来决解出现幻读的问题,也引入事务的特性,通过事务的四种隔离级别,来降低锁冲突,提高并发性能。