标签归档:锁机制

锁机制之MySQL表锁

如何保证在被并发访问时数据的一致性、完整性和有效性,是数据库关注的核心问题。数据库的锁机制就是为了解决这个问题而出现的。锁机制在一定程度上将对共享资源的并发访问有序化,从而保证数据的一致完整性。锁机制的好坏直接影响到数据的并发处理能力和性能。一个好的锁机制的实现是一个数据的核心竞争力之一。

我们知道在MySQL中存在表级锁、页级锁和行级锁,其中MySQL默认实现了表级锁定。其它锁机制在不同的存储引擎中实现,这也是MySQL特点之一:针对特定的应用场景可以使用当前合适的存储引擎。先不论各种存储引擎和锁机制的优劣,这里只是说说他们各自的特点和实现。

MyISAM存储引擎作为曾经的默认存储引擎,其使用的锁机制是MySQL提供的默认表级锁定。虽然它没有实现自己的锁机制,但是在默认表级锁的基础上,增加了并发插入的特性。并发插入与系统参数concurrent_insert相关,concurrent_insert有三个值:

  • concurrent_insert=0 关闭并发写入
  • concurrent_insert=1 (默认)在没有空数据块的MyISAM表中启用并行插入
  • concurrent_insert=2 为所有MyISAM表启用并行插入。如果表有空记录或正被另一线程使用,新行将插入到表的最后。如果表未使用,MySQL将进行普通读锁定并将新行插入空记录。

此参数与MyISAM存储引擎的数据存储方式相关:常规情况下,MyISAM的新数据都会被附加到数据文件的结尾,当做了一些DELETE操作之后,数据文件就不再是连续的,形象一点来说,就是数据文件里出现了很多hole,此时再插入新数据时,按缺省设置会先看这些hole的大小是否可以容纳下新数据,如果可以,则直接把新数据保存到hole里,反之,则把新数据保存到数据文件的结尾。之所以这样做是为了减少数据文件的大小,降低文件碎片的产生。

如果我们使用concurrent_insert=2(通常也推荐这样做),这样会产生较多的文件碎片,为此,我们需要在设置这个参数值的同时,定期对数据表进行OPTIMIZE TABLE操作。此操作可以去除删除操作后留下的数据文件碎片,减小文件尺寸,加快未来的读写操作。但是,在OPTIMIZE TABLE运行过程中,MySQL会锁表。

MySQL的表锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。共享锁和独占锁在锁机制中是一种非常普通的实现方式。 MyISAM在执行查询语句前,会自动给涉及的所有表加读锁,在执行更新操作(DDL)前,会自动给相关的表加写锁。 MySQL的读写锁(mysys/thr_lock.c)是通过4个队列来维护的,他们分别是:

  • 当前读锁队列(lock->read): 存储当前持有读锁所有线程相关信息,按获取锁的时间排序
  • 读锁等待队列(lock->read_wait):存储正在等待读锁锁定资源的线程相关信息
  • 当前写锁队列(lock->write):存储当前持有写锁所有线程相关信息,按获取锁的时间排序
  • 写锁等待队列(lock->write_wait):存储正在等待写锁锁定资源的线程相关信息

对于读锁,当请求的资源没有加写锁或在写锁等待队列中没有更高优先级的写锁定在等待。读锁是共享锁,不会阻塞其他进程对同一资源的读请求,但会阻塞对同一资源的写请求。只有当读锁释放后,才会执行其它进程的写操作。

对于写锁,当请求的资源在当前写锁队列、写锁等待队列或当前读锁队列,进入等待写锁队列;写锁会阻塞其他进程对同一资源的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。

表锁是MySQL数据库中加锁粒度最大的一种锁,除此之外,MySQL还有页级锁和行锁。表锁的执行开销小,加锁速度快,不会出现死锁,但是其加锁的粒度大,发生锁冲突的概率非常高,从而导致并发度低。可以考虑使用主从结构解决并发度低的问题。

参考资料

http://www.zhaokunyao.com/archives/206

http://dev.mysql.com/doc/refman/5.1/zh/database-administration.html

《MySQL性能调优与架构设计》 – 简朝阳

锁机制之PHP文件锁

锁机制之PHP文件锁

锁机制之所以存在是因为并发导致的资源竞争,为了确保操作的有效性和完整性,可以通过锁机制将并发状态转换成串行状态。作为锁机制中的一种,PHP的文件锁也是为了应对资源竞争。假设一个应用场景,在存在较大并发的情况下,通过fwrite向文件尾部多次有序的写入数据,不加锁的情况下会发生什么?多次有序的写入操作相当于一个事务,我们此时需要保证这个事务的完整性。

如下代码简单模拟了这种事务并发状态: process1.php

 
    <?php
    $num = 100;
    $filename = "processdata.txt";
 
    $fp = fopen($filename, "a");
    for ($i = 0; $i < $num; $i++) {
        fwrite($fp, "process1: " . $i . "\r\n");
        usleep(100000);
    }
    fclose($fp);
    ?>

我们需要先执行第一个事务,在processdata.txt文件中写入这100行。

process2.php

   <?php
    $num = 100;
    $filename = "processdata.txt";
 
    $fp = fopen($filename, "a");
    for ($i = 0; $i < $num; $i++) {
        fwrite($fp, "process2: " . $i . "\r\n");
        usleep(100000);
    }
    fclose($fp);
    ?>

第二个事务,继续向processdata.txt文件中写入100行。

多次同时执行,虽然都写了100行,但是事务1和事务2的数据交错写入,这并不是我们想要的结果。我们要的是事务完整的执行,此时我们需要有个机制去保证在第一个事务执行完后再执行第二个。在PHP中,flock函数完成了这一使命。在事物1和事务2的循环前面都加上: flock($fp, LOCK_EX); 就能满足我们的需求,将两个事务串行。

当某一个事务执行完flock时,因为我们在这里添加的是LOCK_EX(独占锁定),所以所有对资源的操作都会被阻塞,只有当事务执行完成后,后面的事务才会执行。我们可以通过输出当前的时间的方法来确认这一点。

关于在尾部追加写入,在unix系统的早期版本中存在一个并发写入的问题,如果要在尾部追加,需要先lseek位置,再write。当多个进程同时操作时,会因为并发导致的覆盖写入的问题,即两个进程同时获取尾部的偏移后,先后执行write操作,后面的操作会将前面的操作覆盖。这个问题在后面以添加打开时的O_APPEND操作而得到解决,它将查找和写入操作变成了一个原子操作。

在PHP的fopen函数的实现中,如果我们使用a参数在文件的尾部追加内容,其调用open函数中oflag参数为 O_CREAT|O_APPEND,即我们使用追加操作不用担心并发追加写入的问题。

在PHP的session默认存储实现中也用到了flock文件锁,当session开始时就调用PS_READ_FUNC,且以O_CREAT | O_RDWR | O_BINARY 打开session数据文件,此时会调用flock加上写锁,如果此时有其它进程访问此文件(即同一用户再次发起对当前文件的请求),就会显示页面加载中,进程被阻塞了。加写锁其出发点是为了保证此次会话中对session的操作事务能完整的执行,防止其它进程的干扰,保证数据的一致性。如果一个页面没有session修改操作,可以尽早的调用session_write_close()释放锁。

文件锁是针对文件的锁,除了这种释义,还可以理解为用文件作为锁。在实际工作中,有时为确保单个进程的执行,我们会在程序执行前判断文件是否存在,如果不存在则创建一个空文件,在进程结束后删除这个空文件,如果存在,则不执行。

锁机制概述

本文主要回答如下问题:

  • 什么是锁机制?
  • 锁机制的作用是什么?
  • 锁机制的类型有哪几种?

关键字:并发、并发控制、乐观锁、悲观锁

在我们常见的程序设计、操作系统和数据库等领域,并发是非常棘手的问题之一。而并发在操作系统、数据库等领域,最终还是会体现在软件开发(代码实现)上。并发的主要纠结点在于对共享资源的处理,虽然现在有事务来处理并发,但是这只是在一定程序上缓解了并发的问题,并没有彻底解决,比如跨事务的并发。

在软件开发过程中,并发控制是确保及时纠正由并发操作导致的错误的一种机制。并发控制主要采用时间戳、乐观并发控制和悲观并发控制等技术手段来实现。而今天我们要说的锁机制主要是指后面的两种技术手段:乐观并发控制和悲观并发控制,他们分别对应乐观锁和悲观锁。锁机制是管理对共享资源的并发访问机制。

乐观锁并不是纯粹意义上的锁,它可以理解为冲突检测,属于事后的操作,其中一种实现是依赖数据版本记录机制。在数据源增加一个版本标记,当请求方读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据源中对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据源当前版本号,则予以更新,否则认为是过期数据。比如某个共享数据被并发访问修改,在各个请求方获取了数据后,请求方都会被分配一个相同版本,如果有一个请求方提交了修改,则将原始数据版本增加1,则其它请求方再次提交修改时,由于提交的版本小于当前版本则显示冲突。除了基于数据版本的控制外,还有包含对更新时间的控制,对不同字段的对比控制等。

悲观锁可以理解为冲突避免,属于事前的操作,即不让并发修改的操作发生,减少并发出现。当一个用户访问一个共享对象时锁住它,即先获得锁,此时其它用户无法访问此对象,当对共享对象的操作完成以后要为被它封锁的对象解锁,此时其它对象才能访问此共享对象。如果此时一个用户一直占着一个资源不放,其它所有用户都只能永远等待,此时可能需要引入其它机制来防止这种情况的发生。

悲观锁减少了并发的程序,而乐观锁在一定程度上会更加自由一些,其在获取资源时是不受限制的,仅在提交的时候才会有限制。当需要在悲观锁与乐观锁之间抉择时,可以考虑如下两个点:

  • 冲突的频率 如果冲突少,通常可以选择乐观锁,这样可以获得更多的并发性,但是此时也需要考虑冲突的严重性,如果系统不能容忍冲突的出现,则需要考虑牺牲并发性,使用悲观锁
  • 冲突的严重性 如果冲突所产生的后果比较严重或者为用户不能容忍,需要使用悲观锁

无论是乐观锁还是悲观锁都存在其优点和缺点,只使用某一种机制都会产生其它问题,可以考虑将这两种锁放一起使用,或者提供两种机制,供用户选择,默认使用乐观锁。