标签归档:并发

锁机制之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,则其它请求方再次提交修改时,由于提交的版本小于当前版本则显示冲突。除了基于数据版本的控制外,还有包含对更新时间的控制,对不同字段的对比控制等。

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

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

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

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