• 如何解决多进程或多线程并发的问题


    如何解决多进程或多线程并发问题

    有个问题:

    一个进程开启事务对表的某一行做了修改,但还没有提交,另一个进程查询该行数据,获取到的是原始的,这时候上面的事物提交了,我再用这个原始数据的时候就有问题……

    那我们该怎么解决这个问题呢?

    解决多事务并发问题的方法有以下几种:

    1、文件锁

    如果对该表的更新或插入的操作,都会经过一个统一的文件,这种方式是可以解决的多进程并发的问题;

    实现方式如下:

    public static function cbInventoryReserve() {
            $LOCK_FILE_PATH = $_SERVER['DOCUMENT_ROOT']."wmsinventoryapi/inventory/InventoryReserve.php";
            $fp = fopen( $LOCK_FILE_PATH, "r" );
            if (!$fp) {
                die("Failed to open the lock file!");
            }
            flock ( $fp, LOCK_EX );
            
        //需要进行的操作
            $params = Flight::request()->getBody();
            $params = json_decode($params, true);
            if (! is_array($params) || empty($params)) {
                Flight::sendRouteResult(array("error_code" => "40002","error_info" => "params empty"));
            }
            $result = InventoryInventoryEngine::getInstance()->inventoryReserve($params);
            
            flock ( $fp, LOCK_UN );
            fclose ( $fp );
            Flight::sendRouteResult($result);
        }

      函数说明  flock()会依参数operation所指定的方式对参数fd所指的文件做各种锁定或解除锁定的动作。此函数只能锁定整个文件,无法锁定文件的某一区域。

       参数  operation有下列四种情况:

       LOCK_SH 建立共享锁定。多个进程可同时对同一个文件作共享锁定。

      LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。

      LOCK_UN 解除文件锁定状态。

       LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做OR(|)组合。

       单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。

       返回值  返回0表示成功,若有错误则返回-1,错误代码存于errno。

    换言之:

    使用共享锁LOCK_SH,如果是读取,不需要等待,但如果是写入,需要等待读取完成。

    使用独占锁LOCK_EX,无论写入/读取都需要等待。

    LOCK_UN,无论使用共享/读占锁,使用完后需要解锁。

    LOCK_NB,当被锁定时,不阻塞,而是提示锁定。

    为了更好的移植性,对于文件的打开与关闭我选择了fopen和fclose的组合,但flock的第一个参数要求的是int类型的文件描述符。这里对fopen返回的FILE类型的文件指针进行转换,转换为int型的文件描述符 (假设open函数返回的文件描述符为fd,而fopen返回的文件指针为*fp,则fd等价于fp->_fileno).

    2、序列化接口(对象序列化)

    所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

    <?php
    // classa.inc:
      
      class A {
          public $one = 1;
        
          public function show_one() {
              echo $this->one;
          }
      }
      
    // page1.php:
    
      include("classa.inc");
      
      $a = new A;
      $s = serialize($a);
      // 把变量$s保存起来以便文件page2.php能够读到
      file_put_contents('store', $s);
    
    // page2.php:
      
      // 要正确了解序列化,必须包含下面一个文件
      include("classa.inc");
    
      $s = file_get_contents('store');
      $a = unserialize($s);
    
      // 现在可以使用对象$a里面的函数 show_one()
      $a->show_one();
    ?>

    3、select *** for update

    Select …forupdate语句是我们经常使用手工加锁语句。通常情况下,select语句是不会对数据加锁,妨碍影响其他的DML和DDL操作。同时,在多版本一致读机制的支持下,select语句也不会被其他类型语句所阻碍。

    借助for update子句,我们可以在应用程序的层面手工实现数据加锁保护操作。

    for update子句的默认行为就是自动启动一个事务,借助事务的锁机制将数据进行锁定。

    开启一个事务使用for update

    start transaction;
    
    select sum(quantity) from ws_inventory_item where inventory_item_id=86 for update;

    再开启另一个事务时,做update 操作的时,只能等待上面的事务,commit才能执行;

    start transaction;
    
    update ws_inventory_item set quantity = quantity + 1  where inventory_item_id = 86;
    MySQL  使用 SELECT … FOR UPDATE 做事务写入前的确认
    以MySQL 的InnoDB 为例,预设的 Tansaction isolation level 为 REPEATABLE READ,在 SELECT 的读取锁定主要分为两种方式:
    SELECT … LOCK IN SHARE MODE
    SELECTFOR UPDATE
    这两种方式在事务(Transaction) 进行当中SELECT 到同一个数据表时,都必须等待其它事务数据被提交(Commit)后才会执行。而主要的不同在于LOCK IN SHARE MODE 在有一方事务要Update 同一个表单时很容易造成死锁 。
    简单的说,如果SELECT 后面若要UPDATE 同一个表单,最好使用 SELECT … UPDATE。
    举个例子:假设商品表单products 内有一个存放商品数量的quantity ,在订单成立之前必须先确定quantity 商品数量是否足够(quantity>0) ,然后才把数量更新为1。
    不安全的做法:
    SELECT quantity FROM products WHERE id=3;
    UPDATE products SET quantity = 1 WHERE id=3;
    为什么不安全呢?
    少量的状况下或许不会有问题,但是大量的数据存取「铁定」会出问题。
    如果我们需要在 quantity>0 的情况下才能扣库存,假设程序在第一行 SELECT 读到的 quantity 是 2 ,看起来数字没有错,但是当MySQL 正准备要UPDATE 的时候,可能已经有人把库存扣成 0 了,但是程序却浑然不知,将错就错的 UPDATE 下去了。
    因此必须透过的事务机制来确保读取及提交的数据都是正确的。
    于是我们在MySQL 就可以这样测试:(注1)
    1    SET AUTOCOMMIT=0;
    2    BEGIN WORK;
    3    SELECT quantity FROM products WHERE id=3 FOR UPDATE;
    此时 products 数据中 id=3 的数据被锁住(注3),其它事务必须等待此次事务提交后才能执行 SELECT * FROM products WHERE id=3 FOR UPDATE (注2)如此可以确保 quantity 在别的事务读到的数字是正确的。
    1    UPDATE products SET quantity = '1' WHERE id=3 ;
    2    COMMIT WORK;
    提交(Commit)写入数据库,products 解锁。
    注1:BEGIN/COMMIT 为事务的起始及结束点,可使用二个以上的MySQL Command 视窗来交互观察锁定的状况。
    注2:在事务进行当中,只有SELECT … FOR UPDATE 或LOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT … 则不受此影响。
    注3:由于InnoDB 预设为Row-level Lock,数据列的锁定可参考这篇。
    注4:InnoDB 表单尽量不要使用LOCK TABLES 指令,若情非得已要使用,请先看官方对于InnoDB 使用LOCK TABLES 的说明,以免造成系统经常发生死锁。
     
    MySQL SELECT … FOR UPDATE 的 Row Lock 与 Table Lock
    上面介绍过SELECT … FOR UPDATE 的用法,不过锁定(Lock)的数据是判别就得要注意一下了。由于InnoDB 预设是Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行 Row lock (只锁住被选取的数据) ,否则MySQL 将会执行 Table Lock (将整个数据表单给锁住)。
    举个例子:
    假设有个表单products ,里面有id 跟name 二个栏位,id 是主键。
    例1: (明确指定主键,并且有此数据,row lock)
     
         SELECT * FROM products WHERE id='3' FOR UPDATE;
    例2: (明确指定主键,若查无此数据,无lock)
         SELECT * FROM products WHERE id='-1' FOR UPDATE;
    例2: (无主键,table lock)
         SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
    例3: (主键不明确,table lock)
         SELECT * FROM products WHERE id<>'3' FOR UPDATE;
    例4: (主键不明确,table lock)
         SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
    注1: FOR UPDATE 仅适用于InnoDB,且必须在事务区块(BEGIN/COMMIT)中才能生效。
    注2: 要测试锁定的状况,可以利用MySQL 的Command Mode ,开二个视窗来做测试。
     

    4、事务隔离级别

    如何解决多进程或多线程并发问题

    本节转载,原文地址:http://singo107.iteye.com/blog/1175084

    数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

    √: 可能出现    ×: 不会出现

      脏读 不可重复读 幻读
    Read uncommitted
    Read committed ×
    Repeatable read × ×
    Serializable × × ×

    注意:我们讨论隔离级别的场景,主要是在多个事务并发的情况下,因此,接下来的讲解都围绕事务并发。

    Read uncommitted 读未提交

    公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资已经到账,是5000元整,非常高兴。可是不幸的是,领导发现发给singo的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有2000元,singo空欢喜一场。


    出现上述情况,即我们所说的脏读,两个并发的事务,“事务A:领导给singo发工资”、“事务B:singo查询工资账户”,事务B读取了事务A尚未提交的数据。

    当隔离级别设置为Read uncommitted时,就可能出现脏读,如何避免脏读,请看下一个隔离级别。

    Read committed 读提交

    singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在singo之前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为何......

    出现上述情况,即我们所说的不可重复读,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

    当隔离级别设置为Read committed时,避免了脏读,但是可能会造成不可重复读。

    大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。如何解决不可重复读这一问题,请看下一个隔离级别。

    Repeatable read 重复读

    当隔离级别设置为Repeatable read时,可以避免不可重复读。当singo拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),singo的老婆就不可能对该记录进行修改,也就是singo的老婆不能在此时转账。

    虽然Repeatable read避免了不可重复读,但还有可能出现幻读。

    singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额(select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction ... ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出现了幻觉,幻读就这样产生了。

    注:Mysql的默认隔离级别就是Repeatable read。

    Serializable 序列化

    Serializable是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。

     Mysql事务隔离级别设置方式

    用户可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别。它的语法如下:

    SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

    注意:默认的行为(不带session和global)是为下一个(未开始)事务设置隔离级别。如果你使用GLOBAL关键字,语句在全局对从那点开始创建的所有新连接(除了不存在的连接)设置默认事务级别。你需要SUPER权限来做这个。使用SESSION 关键字为将来在当前连接上执行的事务设置默认事务级别。 任何客户端都能自由改变会话隔离级别(甚至在事务的中间),或者为下一个事务设置隔离级别。 

  • 相关阅读:
    mysql存储过程 --游标的使用 取每行记录 (多字段)
    mysql rowid实现
    redis进程守护脚本
    CF1042B 【Vitamins】(去重,状压搜索)
    CF1042A 【Benches】(优先队列)
    魔板 Magic Squares(广搜,状态转化)
    解方程(hash,秦九韶算法)
    noip模拟赛 动态仙人掌(并查集,贪心)
    (暴力碾标算)NOIP模拟赛 宗教仪式
    牛客网NOIP赛前集训营-提高组18/9/9 A-中位数
  • 原文地址:https://www.cnblogs.com/sdgf/p/5740998.html
Copyright © 2020-2023  润新知