php多并发模拟实现,解决多并发措施

209 阅读3分钟

1.多并发请求函数封装

public function multiRequest($urls, $nums)
    {
        // 创建一对cURL资源
        // $ch1 = curl_init();
        // $ch2 = curl_init();

        // 设置URL和相应的选项
        // curl_setopt($ch1, CURLOPT_URL, "http://81.69.45.114/test/test.php");
        // curl_setopt($ch1, CURLOPT_HEADER, 0);
        // curl_setopt($ch2, CURLOPT_URL, "http://81.69.45.114/test/test.php");
        // curl_setopt($ch2, CURLOPT_HEADER, 0);

        // 创建批处理cURL句柄
        $mh = curl_multi_init();

        // 增加2个句柄
        // curl_multi_add_handle($mh, $ch1);
        // curl_multi_add_handle($mh, $ch2);


        $key = 0;
        do {
            $ch[$key] = curl_init();
            curl_setopt($ch[$key], CURLOPT_URL, $urls);
            curl_setopt($ch[$key], CURLOPT_HEADER, 0);
            $key++;
            $nums--;
        }while($nums>0);
        foreach ($ch as $c) {
            curl_multi_add_handle($mh, $c);
        }

        $active = null;
        // 执行批处理句柄
        do {
            $mrc = curl_multi_exec($mh, $active);
            var_dump($mrc);
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);

        while ($active && $mrc == CURLM_OK) {
            if (curl_multi_select($mh) != -1) {
                do {
                    $mrc = curl_multi_exec($mh, $active);
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
            }
        }

        // 关闭全部句柄
        foreach ($ch as $c) {
            curl_multi_remove_handle($mh, $c);
        }
        // curl_multi_remove_handle($mh, $ch1);
        // curl_multi_remove_handle($mh, $ch2);
        curl_multi_close($mh);
    }
// 函数调用
multiRequest(请求地址,并发数量)
multiRequest("http://127.0.0.1:88/test_back/public/index.php/test/count", 10);

2.解决措施

1.使用数据库自减或者自增方法,并设置数据字段的符号是否为无符号,通过捕获修改异常来返回错误信息,解决并发下的数据更新问题(但是不适用于新增记录,会存在同时新增的情况,所以将使用下面的方法解决)
假设数据库的要修改的字段的值为10,但是现在同时发送十个请求过来,每个请求都是修改这个字段的值,但是因为是并发,每个请求拿到的值可能是一样的,因为就存在了最后的值不为理想的那个值
// 存在重复的可能,十个请求触发十次这个接口,值减一,但是数据库最后返回的值为4(不为0)
    public function count()
    {
        // 请求超时时间为100s
        set_time_limit(100);
        $num = $this->user->where('id', '2')->value('num');
        var_dump('num' . $num);
        $this->user->where('id', '2')->update(['num' => $num - 1]);
        // 返回查询到的数量
        $count = $this->user->where('id', '2')->value('num');
        return $count;
    }
// 修改之后的方法,虽然依旧是查询的值,但是增减用的是自增或者自减,以原有的数据库为基准,而不是读出来的为基准,十次请求过后,值为0
    public function count()
    {
        // 请求超时时间为100s
        set_time_limit(100);
        $this->user->where('id','2')->dec('num')->update();
        // 返回查询到的数量
        $count = $this->user->where('id', '2')->value('num');
        return $count;
    }
2.文件锁 -- 加入文件读取锁定,限制每个请求都得完成后才能读取,避免了同时读取数据库的可能,从而降低并发量。在文件锁定过程中进行数据库的数据修改读取(实际项目中并不支持使用)
    // 文件锁
    function fileLock()
    {
        $file = fopen(config('path.fileLock') . '/userLock.txt', 'w+');

        if (flock($file, LOCK_EX)) {// 独占资源锁定,不允许其他进程访问
            // 进行数据库操作
            $num = $this->user->where('id', '2')->value('num');
            $this->user->where('id', '2')->update(['num' => $num - 1]);
            $count = $this->user->where('id', '2')->value('num');
            var_dump('count' . $count);
            // 释放释放一个共享锁定或独占锁定
            flock($file,LOCK_UN);
        }else{
            return '系统出错';
        }
        // 请求完都要关闭资源
        fclose($file);
    }
3.悲观锁+数据库事务 默认数据是悲观的,就是每次拿数据的时候这个数据一定会发生改变,因此使用数据库的操作锁定,只有操作完成后解锁记录才能被另外的进程调用
    public function bad()
    {
        // 开启事务
        $this->user->startTrans();
        try {
            // 锁定记录操作
            $num = $this->user->lock(true)->where('id','2')->value('num');
            $this->user->where('id','2')->update(['num' => $num-1]);
            $count = $this->user->where('id','2')->value('num');
            var_dump('count' . $count);
            $this->user->commit();
        } catch (\Exception $e) {
            $this->user->rollback();
            var_dump($e->getMessage());
        }
    }

4.乐观锁+数据库事务 默认数据是乐观的,就是每次拿数据的时候默认数据是不会发生改变,没有被别的进程调用,会在数据库表中加一个版本字段,只有在修改数据的时候查看改数据版本是否发生改变,如果发生版本改变,则取消事务操作。如果修改成功则修改版本字段。(会丢失部分的数据请求修改)
    public function well()
    {
        // 开启事务
        $this->user->startTrans();
        try {
            $num = $this->user->where('id', '2')->value('num');
            $version = $this->user->where('id', '2')->value('version');
            // 要更新的数据
            $data = [
                'num'     => $num - 1,
                'version' => $version + 1
            ];
            // 只有版本号相同才会更新
            if ($this->user->where(['id' => '2', 'version' => $version])->update($data)) {
                $this->user->commit();
            } else {
                // 版本号不同放弃修改,事务回滚
                $this->user->rollback();
            }
        } catch (\Exception $e) {
            var_dump($e->getMessage());
            $this->user->rollback();
        }
    }
5.redis缓存
1.悲观锁适合频繁写入的场景,不适合用于多次读取的场景

悲观锁是将资源进行锁定,利用key的过期时间进行处理,传入key(存储了时间戳),并且使用setnx获取是否能够写入,不能写入则获取现在的时间和key的值进行比对,如果现有时间大于和key的值,则进行删除过期key并且写入新的过期时间,然后返回可以继续操作的标志

 /**
   * 获取锁---用于悲观锁
   * @param  String  $key    锁标识
   * @param  Int     $expire 锁过期时间
   * @return Boolean
   */
  public static function lock($key = '', $expire = 5)
  {
    $is_lock = Cache::store('redis')->setnx($key, time() + $expire);

    //不能获取锁
    if (!$is_lock) {
      //判断锁是否过期
      $lock_time = Cache::store('redis')->get($key);
      //锁已过期,删除锁,重新获取
      if (time() > $lock_time) {
        self::unlock($key);
        $is_lock = Cache::store('redis')->setnx($key, time() + $expire);
      }
    }

    return $is_lock ? true : false;
  }
  
 /**
   * 释放锁
   * @param  String  $key 锁标识
   * @return Boolean
   */
  public static function unlock($key = '')
  {
    return Cache::store('redis')->del($key);
  }
  
//调用:
$key = 'str:lock';
$is_lock = lock($key, 10);
if ($is_lock) {
  // 获得锁之后的操作
  echo 'get lock success<br>';
  echo 'do sth..<br>';
  sleep(5);
  echo 'success<br>';
  Redis::unlock($key);
} else {
  //获取锁失败
  echo 'request too frequently<br>';
}
  
2.乐观锁适合于多次读取的场景,不适合用于多次写入的场景

主要的操作是使用watch监听key的变化,然后使用multi开启redis的事务,进行增删改查等别的操作后,然后使用exec进行redis事务提交,在并发过程中如果watch监听的数据发生改变,这个事务的提交就会失败

  /**
   * 获取锁---用于乐观锁
   * @param  String  $key    锁标识
   */
  public static function wellLock($key = '')
  {
    $num = Cache::get($key); // redis库中值为10

    var_dump('num:' . $num);

    $rob_total = 10; //抢购数量

    if ($num < $rob_total) {

      Cache::store('redis')->watch('num');
      Cache::store('redis')->multi(); //redis事务开始
      // 休眠是为了让并行的修改有足够的时间
      sleep(10);
      // num在事务前后没有变化,这里的操作,下面会提交事务,而与此同时别的事务会失败
      // Cache::store('redis')->incr("testwatch"); //乐观锁的版本号+1

      $rob_result = Cache::store('redis')->exec(); //redis事务提交 ,如果事务过程中num这个值发生变化,这里redis事务失败
      var_dump($rob_result);

      if ($rob_result) {
        $num =  Cache::store('redis')->get('num');
        var_dump($num);
        echo "成功";
      } else {
        echo "再来一次,抢购失败";
      }
    } else {
      echo "卖完!";
    }
  }
  
  // 用于并行触发乐观锁接口
  public static function wellLock2($key = '')
  {
    $num = Cache::get($key); // redis库中值为10

    var_dump('num:' . $num);

    $rob_total = 10; //抢购数量

    if ($num < $rob_total) {

      Cache::store('redis')->watch('num');
      Cache::store('redis')->multi(); //redis事务开始

      // num在事务前后没有变化,这里的操作,下面会提交事务,而与此同时别的事务会失败
      // Cache::store('redis')->incr("testwatch"); //乐观锁的版本号+1
      $num =  Cache::store('redis')->set('num',7);

      $rob_result = Cache::store('redis')->exec(); //redis事务提交 ,如果事务过程中num这个值发生变化,这里redis事务失败
      var_dump($rob_result);

      if ($rob_result) {
        $num =  Cache::store('redis')->get('num');
        var_dump($num);
        echo "成功";
      } else {
        echo "再来一次,抢购失败";
      }
    } else {
      echo "卖完!";
    }
  }
  
  解析:最后redis的数据值为7。在执行wellLock函数中,使用了休眠,在休眠的期间,使用postman触发wellLock2接口,修改了侦听的num的值,接口wellLock的事务提交失败,最后响应wellLock2执行成功