为了保证数据的一致完整性,任何一个数据库都存在锁定机制。锁定机制的优劣直接应想到一个数据库系统的并发处理能力和性能,所以锁定机制的实现也就成为了各种数据库的核心技术之一。本章将对MySQL中两种使用最为频繁的存储引擎MyISAM和Innodb各自的锁定机制进行较为详细的分析。
总的来说,MySQL各存储引擎使用了三种类型(级别)的锁定机制:行级锁定,页级锁定和表级锁定。下面我们先分析一下MySQL这三种锁定的特点和各自的优劣所在。
行级锁定是目前各大数据库管理软件所实现的锁定颗粒度最小的,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。 但是由于锁定资源的颗粒度很小,所以每次获取锁和释放锁消耗的资源也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。
表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。 当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度较低。
页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。 在MySQL数据库中,使用表级锁定的主要是MyISAM,Memory,CSV等一些非事务性存储引擎,而使用行级锁定的主要是Innodb存储引擎和NDBCluster存储引擎,页级锁定主要是BerkeleyDB存储引擎的锁定方式。
MySQL的表级锁定主要分为两种类型,一种是读锁定,另一种是写锁定 在MySQL中,主要通过四个队列来维护这两种锁定:两个存放当前正在锁定中的读和写锁定信息,另外两个存放等待中的读写锁定信息,如下:
当前持有读锁的所有线程的相关信息都能够在Currentread-lockqueue中找到,队列中的信息按照获取到锁的时间依序存放。而正在等待锁定资源的信息则存放在Pendingread-lockqueue里面,另外两个存放写锁信息的队列也按照上面相同规则来存放信息。
一个新的客户端请求在申请获取读锁定资源的时候,需要满足两个条件:
如果满足了上面两个条件之后,该请求会被立即通过,并将相关的信息存入Currentread-lockqueue中,否则会被迫进入等待队列Pendingread-lockqueue中等待资源的释放。
一个新的客户端请求在申请获取写锁定资源的时候,被申请资源需要满足两个条件:
行级锁定不是MySQL自己实现的锁定方式,而是由其他存储引擎自己所实现的,如广为大家所知的Innodb存储引擎,以及MySQL的分布式存储引擎NDBCluster等都是实现了行级锁定。
总的来说,Innodb的锁定机制和Oracle数据库有不少相似之处。Innodb的行级锁定同样分为两种类型,共享锁和排他锁,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,Innodb也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。
当对某个资源加锁时,如果
Innodb的这种锁定实现方式被称为“NEXT-KEYlocking”(间隙锁),因为Query执行过程中通过过范围查找的华,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。
间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。而Innodb给出的解释是为了组织幻读的出现,所以他们选择的间隙锁来实现锁定。
除了间隙锁给Innodb带来性能的负面影响之外,通过索引实现锁定的方式还存在其他几个较大的性能隐患:
在Innodb的事务管理和锁定机制中,有专门检测死锁的机制,会在系统中产生死锁之后的很短时间内就检测到该死锁的存在。
当Innodb检测到系统中产生了死锁之后,Innodb会通过相应的判断来选这产生死锁的两个事务中较小的事务来回滚,而让另外一个较大的事务成功完成。
但是有一点需要注意的就是,当产生死锁的场景中涉及到不止Innodb存储引擎的时候,Innodb是没办法检测到该死锁的,这时候就只能通过锁定超时限制来解决该死锁了。另外,死锁的产生过程的示例将在本节最后的Innodb锁定示例中演示。
下面我们以InnodDB引擎来说明产生死锁的情况
Session1 | Session2 |
---|---|
set autocommit=0 | set autocommit=0 |
update test set b = ‘b1’ where a = 1; 更新但不提交 | |
– | update test set b = ‘b1’ where a = 1; 被阻塞,等待 |
commit; 提交 | – |
– | update test set b = ‘b1’ where a = 1; 解除阻塞 |
Session1 | Session2 |
---|---|
update test set b = ‘2’ where b = 2000; | update test set b = ‘3’ where b = 3000; 阻塞,等待 |
commit; | – |
– | update test set b = ‘3’ where b = 3000;解除阻塞,完成更新 |
Session1 | Session2 |
---|---|
select from test; update test set b = a 100 where a < 4 and a > 1; | – |
– | mysql> insert into test values(2,’200’);被阻塞,等待 |
commit; | – |
– | insert into test values(2,’200’);Query OK, 1 row affected (38.68 sec) |
Session1 | Session2 |
---|---|
update t1 set id = 110 where id = 11; | – |
– | update t2 set id = 210 where id = 21; |
update t2 set id=2100 where id=21;阻塞,等待session2释放 | – |
– | update t1 set id=1100 where id=11; 阻塞,等待session1释放 |
两个 session 互相等等待对方的资源释放之后才能释放自己的资源,造成了死锁 | – |
在优化MyISAM存储引擎锁定问题的时候,最关键的就是如何让其提高并发度。由于锁定级别是不可能改变的了,所以我们首先需要尽可能让锁定的时间变短,然后就是让可能并发进行的操作尽可能的并发。
尽两减少大Query,将复杂Query分拆成小的Query分布进行; 尽可能的建立足够高效的索引,让数据检索更迅速; 尽量让MyISAM存储引擎的表只存放必要的信息,控制字段类型; 利用合适的机会优化MyISAM表数据文件;
2、分离能并行的操作
可能有些人会认为在MyISAM存储引擎的表上读写锁就只能是完全的串行化,没办法再并行了。大家不要忘记了,MyISAM的存储引擎还有一个非常有用的特性,那就是ConcurrentInsert(并发插入)的特性。 Concurrent_insert=2,无论MyISAM存储引擎的表数据文件的中间部分是否存在因为删除数据而留下的空闲空间,都允许在数据文件尾部进行 ConcurrentInsert;concurrent_insert=1,当MyISAM存储引擎表数据文件中间不存在空闲空间的时候,可以从文件尾部进行ConcurrentInsert; concurrent_insert=0,无论MyISAM存储引擎的表数据文件的中间部分是否存在因为删除数据而留下的空闲空间,都不允许ConcurrentInsert。
3、合理利用读写优先级
表级锁定对于读和写是有不同优先级设定的,默认情况下是写优先级要大于读优先级。所以,如果我们可以根据各自系统环境的差异决定读与写的优先级。如果我们的系统是一个以读为主,而且要优先保证查询性能的话,我们可以通过设置系统参数选项low_priority_updates=1,将写的优先级设置为比读的优先级低,即可让告诉MySQL尽量先处理读请求。
*尽可能让所有的数据检索都通过索引来完成,从而避免Innodb因为无法通过索引键加锁而升级为表级锁定;
由于Innodb的行级锁定和事务性,所以肯定会产生死锁,下面是一些比较常用的减少死锁产生概率
a. 类似业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁; b. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率; c. 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
系统锁定争用情况查询对于两种锁定级别,MySQL内部有两组专门的状态变量记录系统内部锁资源争用情况 MySQL 实现的表级锁定的争用状态变量: Table_locks_immediate:产生表级锁定的次数; Table_locks_waited:出现表级锁定争用而发生等待的次数;
对于Innodb所使用的行级锁定,系统中是通过另外一组更为详细的状态变量来记录的,如下: Innodb 的行级锁定状态变量不仅记录了锁定等待次数,还记录了锁定总时长,每次平均时长,以及最大时长,此外还有一个非累积状态量显示了当前正在等待锁定的等待数量。对各个状态量的说明如下:
对于这5个状态变量,比较重要的主要是Innodb_row_lock_time_avg(等待平均时长),Innodb_row_lock_waits(等待总次数)以及Innodb_row_lock_time(等待总时长)这三项。尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
此外,Innodb除了提供这五个系统状态变量之外,还提供的其他更为丰富的即时状态信息供我们分析使用。可以通过如下方法查看
1.通过创建InnodbMonitor表来打开Innodb的monitor功能: 2.然后通过使用“show engine innodb status;”查看细节信息(由于输出内容太多就不在此记录了)