5

mysql最初是希望设计出一种独立于各种存储引擎的锁定机制,mysql存储引擎的设计者是建立在“任何表在同一时刻只允许单个线程对其进行访问(包括读)”这样的假设的基础之上的!很明显,现在的mysql并不是这个样子的,因为mysql如今已经发展成为了一款多用户、多线程的mysql关系型数据库! 其显著特点就是不同的存储引擎支持不同的锁机制!在我们浅析锁机制之前,需要先明白一些基础的概念!

一次性封锁与两段锁

一次性封锁:在sql语句的开始执行的时候,已经预先知道要涉及到那些数据,然后全部锁住,在执行完毕之后,再全部解锁!(myisam就是采用这样的锁协议)

两段锁: 是指每个事务的执行可以分为两个阶段:生长阶段(加锁阶段)和衰退阶段(解锁阶段)。

加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁,在进行写操作之前要申请并获得X锁。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行;

解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

两段封锁法可以这样来实现:事务开始后就处于加锁阶段,一直到执行ROLLBACK和COMMIT之前都是加锁阶段。ROLLBACK和COMMIT使事务进入解锁阶段,即在ROLLBACK和COMMIT模块中DBMS释放所有封锁。(innodb就是采取这样的锁协议)

mysql使用了三种级别的锁定机制(依照锁定范围而言)

行级别的锁定、页级别的锁定、表级别的锁定!

行级锁定:
行级锁定最大的特点就是锁定对象的颗粒度很小,由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。
虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。

表级锁定:
和行级锁定相反,表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。此外,表级别的锁定是不会产生死锁问题的。

页级锁定:
页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。当然,也会产生死锁问题。

常见的存储引擎与三种级别的锁机制

行级锁定:innodb(innodb的锁是建立在索引基础上的,必要的时候会由行锁升级为表锁,所以,innodb既支持表锁也支持行锁)

表级锁定:myisam、memory,innodb,BDB

页级锁定: BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁

隐式锁与显式锁

mysql锁分为隐式锁和显式锁。当多个客户端并发访问同一个数据的时候,为了保证数据的一致性,数据库管理系统会自动的为该数据加锁、解锁,这种被称为隐式锁。隐式锁无需开发人员维护(包括锁粒度、加锁时机、解锁时机等)

当时在某些特殊的情况下需要开发人员手动的进行加锁、解锁,这种锁方式被称为显式锁。对于显式锁而言,开发人员不仅要确定锁的粒度,还需要确定加锁的时机(何时加锁)、解锁的时机(何时解锁)以及所的类型。

锁的生命周期

锁的生命周期是指在一个msql回话内,对数据进行加锁到解锁之间的时间间隔。锁的声明周期越长,并发性能就越低;锁的声明周期越短,并发性能就越高。另外锁是数据库管理系统的重要资源,需要占据一定的服务器内存,锁的周期越长,占用的服务器内存时间就越长;相反如果锁周期越短,占用的内存也就越短。因此,总的来说,我们应该尽可能的缩短锁的生命周期。

锁类型

clipboard.png

读锁(read lock,也叫共享锁): 不会阻塞其他用户对锁定数据的读请求,但会阻塞对锁定数据的写请求。

clipboard.png

写锁(x lock,也叫排它锁): 会阻塞其他用户对锁定数据的读和写操作。

clipboard.png

Myisam存储引擎的锁机制

MyISAM存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型。随着应用对事务完整性和 并发性要求的不断提高,MySQL才开始开发基于事务的存储引擎,后来慢慢出现了支持页锁的BDB存储引擎和支持行锁的InnoDB存储引擎(实际 InnoDB是单独的一个公司,现在已经被Oracle公司收购)。但是MyISAM的表锁依然是使用最为广泛的锁类型。

1、如何维护读锁和写锁

在MySQL 中,主要通过四个队列来维护这两种锁定:两个存放当前正在锁定中的读和写锁定信息,另外两个存放等待中的读写锁定信息,如下:
• Current read-lock queue (lock->read)
• Pending read-lock queue (lock->read_wait)
• Current write-lock queue (lock->write)
• Pending write-lock queue (lock->write_wait)
当前持有读锁的所有线程的相关信息都能够在Current read-lock queue 中找到,队列中的信息按照获取到锁的时间依序存放。而正在等待锁定资源的信息则存放在Pending read-lock queue 里面,另外两个存放写锁信息的队列也按照上面相同规则来存放信息。

2、如何加锁

如何加读锁?

  • 请求的资源当前没有被写锁定,就是没有在Current write-lock queue 队列中出现;

  • 写锁定的等待队列中(Pending write-lock queue )队列中并没有优先级更高的写锁定等待;

满足上面的两个条件之后,请求会被立即通过,并将相关信息存入到Current read-lock queue队列中,而如果有一个条件没有满足,就会被迫进入到Pending-read-lock-queue队列中进行等待。

如何加写锁?

  • 在Current write-lock queue中是否有锁定相同的资源;

  • 在Pending write-lock queue中是否有锁定相同的资源;

  • 在Current read-lock queue中是否有锁定相同的资源;

满足上面的三个条件后,请求会被立即通过,将相关信息存入到Current write-lock queue队列中,而如果有一个条件没有满足,就会被迫进入到Pending-write-lock-queue队列中进行等待!

隐式加锁?
myisam存储引擎在执行sql语句之前会自动为涉及到的表加锁。
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作 (UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。在示例中,显式加锁基本上都是为了模拟而已。

显式加锁?
在用LOCK TABLES给表显式加表锁时,必须同时取得所有涉及到表的锁,并且MySQL不支持锁升级。也就是说,在执行LOCK TABLES后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。其实,在自动加锁的 情况下也基本如此,MyISAM总是一次获得SQL语句所需要的全部锁。这也正是MyISAM表不会出现死锁(Deadlock Free)的原因。

lock table 表名 【redad|write】;加锁
unlock tables; 解锁

3、myisam的并发插入

上文提到过MyISAM表的读和写是串行的,但这是就总体而言的。在一定条件下,MyISAM表也支持查询和插入操作的并发进行。
MyISAM存储引擎有一个系统变量concurrent_insert,专门用以控制其并发插入的行为,其值分别可以为0、1或2。

   当concurrent_insert设置为0时,不允许并发插入。
   当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,
   另一个进程从表尾插入记录。这也是MySQL的默认设置。
   当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录

4、myisam的锁调度

前面讲过,MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个 MyISAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先获得锁。
不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这是因为MySQL认为写请求一般比读请求要重要。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。这种情况有时可能会变得非常糟糕!幸好我们可以通过一些设置来调MyISAM 的调度行为。

  • 通过指定启动参数low-priority-updates,使所有的更新操作优先级比select语句的优先级低。

  • 向特定的insert、delete、update语句添加 low_priority选项,降低这些操作的优先级。 这种调度修改暗示着,可能存在LOW_PRIORITY写入操作永远被阻塞的情况。如果前面的读取操作在进行的过程中一直有其它的读取操作到达,那么新的请求都会插入到LOW_PRIORITY写入操作之前。

  • 向特定的select语句添加high_priority选项,提高检索操作的优先级。 它允许SELECT插入正在等待的写入操作之前,即使在正常情况下写入操作的优先级更高。另外一种影响是,高优先级的SELECT在正常的SELECT语句之前执行,因为这些语句会被写入操作阻塞。

虽然上面3种方法都是要么更新优先,要么查询优先的方法,但还是可以用其来解决查询相对重要的应用(如用户登录系统)中,读锁等待严重的问题。(但是个人建议不要使用,除非你真的确定要这么干)。

另外,MySQL也提供了一种折中的办法来调节读写冲突,即给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。上面已经讨论了写优先调度机制带来的问题和解决办法。这 里还要强调一点:一些需要长时间运行的查询操作,也会使写进程“饿死”!因此,应用中应尽量避免出现长时间运行的查询操作,不要总想用一条SELECT语 句来解决问题,因为这种看似巧妙的SQL语句,往往比较复杂,执行时间较长,在可能的情况下可以通过使用中间表等措施对SQL语句做一定的“分解”,使每 一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。

5、Myisam存储引擎中如何查看表级锁的争用状态

clipboard.png

这里有两个状态变量记录MySQL 内部表级锁定的情况,两个变量说明如下:
Table_locks_immediate:使用表级锁后立即释放表级锁的次数。
Table_locks_wait:出现表级锁争用而发生等待的次数;
两个状态值都是从系统开启后开始记录的,每出现一次对应的事件数量就会加1。

6、举例

create table film_text
(

 film_id int not null auto_increment,
 title varchar(64) not null default '',
 primary key (film_id)

)engine=myisam charset=utf8;

insert into film_text (film_id, title) values ('1001', 'Update Test');

=================================================================================================

create table film
(

 film_id int not null auto_increment,
 title varchar(64) not null default '',
 primary key (film_id)

)engine=myisam charset=utf8;

insert into film (film_id, title) values ('1001', 'Update Test');

session1 session2
首先获取film_text表的write锁定: clipboard.png
在当前会话中对锁定表的查询、更新、插入操作都是可以的: clipboard.pngclipboard.png 在其他的会话中对锁定表的任何操作都会被阻塞(串行化): clipboard.png 这里一直处于等待的状态……
在当前会话中,释放对表的锁定: clipboard.png 执行得到结果: clipboard.png
对film_text表加读锁 clipboard.png
在当前会话中,只能对锁定到的表进行查询操作,成功的对锁定的表进行了查询: clipboard.png 对没有锁定的表进行查询,出现报错: clipboard.png 对锁定的表进行更新和删除操作,出现了报错 clipboard.png 在其他会话中,可以对锁定的表进行查询操作 clipboard.png 一直处于等待的状态: clipboard.png
释放对该表的锁定: clipboard.png 更新操作完成: clipboard.pngclipboard.png
对film_text表加上read local锁定: clipboard.png
当前会话不能够对锁定表进行更新、插入、删除的操作,但是查询操作是可以的 : clipboard.png 其他的会话,可以对进行查询、和插入操作: clipboard.pngclipboard.png 删除操作一直处于等待状态…… clipboard.png
释放锁: clipboard.png 执行了删除操作: clipboard.png

ps:READ LOCAL和READ之间的区别是,READ LOCAL允许在锁定被保持时,执行非冲突性INSERT语句(同时插入)。 对于InnoDB表,READ LOCAL与READ相同。

以上是对个人对mysql锁机制的一些理解,如果有理解错误或者不到位的地方,希望高手不吝赐教!


maweibinguo
783 声望36 粉丝

后端开发工程师一枚, keep moving