Home  >  Article  >  Backend Development  >  How to solve the problem of high concurrency (product flash sale) in PHP? Two solutions shared

How to solve the problem of high concurrency (product flash sale) in PHP? Two solutions shared

青灯夜游
青灯夜游forward
2022-02-23 10:52:217196browse

How to solve the problem of high concurrency (product flash sales) in PHP? The following article will share with you two solutions (based on mysql or based on Redis), I hope it will be helpful to you.

How to solve the problem of high concurrency (product flash sale) in PHP? Two solutions shared

Instakill will produce an instant high concurrency. Using the database will increase the access pressure of the database and also reduce the access speed, so we should use caching to reduce database access. Pressure;

It can be seen that the operation here is different from the original order: the flash sale pre-order generated will not be written to the database immediately, but will be written to the cache first. When the user pays successfully, the status will be modified. , written to the database.

Assume that num is a field stored in the database, which stores the remaining quantity of the flash-killed product.

if($num > 0){
  //用户抢购成功,记录用户信息
  $num--;
}

Assume that in a scenario with high concurrency, when the value of num in the database is 1, multiple processes may read that num is 1 at the same time. The program determines that the conditions are met and the purchase is successful. , num minus one.

This will lead to over-delivery of products. There are only 10 products that can be snapped up, but more than 10 people may grab them. At this time, num will be negative after the rush buying is completed.

There are many solutions to this problem, which can be simply divided into solutions based on mysql and redis. The performance of redis is due to mysql, so it can carry higher concurrency, but the solutions introduced below are all based on For a single mysql and redis, higher concurrency requires a distributed solution, which is not covered in this article.

1. Solution based on mysql

Product table goods

CREATE TABLE `goods` (
 `id` int(11) NOT NULL,
 `num` int(11) DEFAULT NULL,
 `version` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Purchase result table log

CREATE TABLE `log` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `good_id` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

①Pessimistic lock

The pessimistic lock scheme uses exclusive reading, that is, only one process can read the value of num at the same time. After the transaction is committed or rolled back, the lock will be released and other processes can read it.

This solution is the simplest and easiest to understand. When the performance requirements are not high, you can directly adopt this solution. It should be noted that SELECT ... FOR UPDATE should use indexes as much as possible in order to lock as few rows as possible;

exclusive locks are only released after the transaction execution is completed, not It is released after the read is completed, so the transaction used should be committed or rolled back as early as possible to release the exclusive lock earlier.

$this->mysqli->begin_transaction();
$result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1 FOR UPDATE");
$row = $result->fetch_assoc();
$num = intval($row['num']);
if($num > 0){
  usleep(100);
  $this->mysqli->query("UPDATE goods SET num=num-1");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  $this->mysqli->commit();
  echo "fail3:".$num;
}

②Optimistic lock

The optimistic lock scheme does not add an exclusive lock when reading data, but updates it through an The version field will be automatically incremented to solve the problem of multiple processes reading the same num and then updating successfully. When each process reads num, it also reads the value of version. When updating num, it also updates version, and adds an equivalence judgment on version when updating.

Assume that 10 processes have read that the value of num is 1 and the value of version is 9. Then the update statements executed by these 10 processes are UPDATE goods SET num=num-1,version =version 1 WHERE version=9,

However, when one of the processes is successfully executed, the version value in the database will become 10, and the remaining 9 processes will not be executed successfully. This ensures The product will not be over-delivered and the value of num will not be less than 0. However, this also leads to a problem, that is, users who issued a rush purchase request earlier may not be able to grab it, but will be grabbed by later requests.

$result = $this->mysqli->query("SELECT num,version FROM goods WHERE id=1 LIMIT 1");
$row = $result->fetch_assoc();
$num = intval($row['num']);
$version = intval($row['version']);
if($num > 0){
  usleep(100);
  $this->mysqli->begin_transaction();
  $this->mysqli->query("UPDATE goods SET num=num-1,version=version+1 WHERE version={$version}");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  echo "fail3:".$num;
}

③where condition (atomic operation)

The pessimistic lock scheme ensures that the value of num in the database can only be used by one process at the same time Reading and processing, that is, concurrent reading processes must be queued and executed sequentially here.

Optimistic locking scheme Although the value of num can be read by multiple processes at the same time, the equal value judgment of version in the update operation can ensure that only one update of concurrent update operations can be successful at the same time.

There is a simpler solution, which is to only add the conditional restriction of num>0 during the update operation. Although the solution restricted by the where condition seems similar to the optimistic locking solution and can prevent the occurrence of over-issuance problems, the performance is still very different when num is large.

Suppose num is 10 at this time, and 5 processes read num=10 at the same time. For the optimistic locking scheme, due to the equal value judgment of the version field, only one of the 5 processes will be updated successfully, so After the execution of the five processes is completed, num will be 9;

For the where condition judgment scheme, as long as num>0 can be updated successfully, the num will be 5 after the execution of the five processes is completed.

$result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1");
$row = $result->fetch_assoc();
$num = intval($row['num']);
if($num > 0){
  usleep(100);
  $this->mysqli->begin_transaction();
  $this->mysqli->query("UPDATE goods SET num=num-1 WHERE num>0");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  echo "fail3:".$num;
}

2. Solution based on redis

① Watch-based optimistic locking solution

watch is used to monitor one (or more) keys. If this (or these) keys are changed by other commands before the transaction is executed, the transaction will be interrupted.

This scheme is similar to the optimistic locking scheme in mysql, and the specific performance is the same.

$num = $this->redis->get('num');
if($num > 0) {
  $this->redis->watch('num');
  usleep(100);
  $res = $this->redis->multi()->decr('num')->lPush('result',$num)->exec();
  if($res == false){
    echo "fail1";
  }else{
    echo "success:".$num;
  }
}else{
  echo "fail2";
}

②List-based queue scheme

基于队列的方案利用了redis出队操作的原子性,抢购开始之前首先将商品编号放入响应的队列中,在抢购时依次从队列中弹出操作,这样可以保证每个商品只能被一个进程获取并操作,不存在超发的情况。

该方案的优点是理解和实现起来都比较简单,缺点是当商品数量较多是,需要将大量的数据存入到队列中,并且不同的商品需要存入到不同的消息队列中。

public function init(){
  $this->redis->del('goods');
  for($i=1;$i<=10;$i++){
    $this->redis->lPush(&#39;goods&#39;,$i);
  }
  $this->redis->del(&#39;result&#39;);
  echo &#39;init done&#39;;
}
public function run(){
  $goods_id = $this->redis->rPop(&#39;goods&#39;);
  usleep(100);
  if($goods_id == false) {
    echo "fail1";
  }else{
    $res = $this->redis->lPush(&#39;result&#39;,$goods_id);
    if($res == false){
      echo "writelog:".$goods_id;
    }else{
      echo "success".$goods_id;
    }
  }
}

③基于decr返回值的方案

如果我们将剩余量num设置为一个键值类型,每次先get之后判断,然后再decr是不能解决超发问题的。

但是redis中的decr操作会返回执行后的结果,可以解决超发问题。我们首先get到num的值进行第一步判断,避免每次都去更新num的值,然后再对num执行decr操作,并判断decr的返回值,如果返回值不小于0,这说明decr之前是大于0的,用户抢购成功。

public function run(){
  $num = $this->redis->get(&#39;num&#39;);
  if($num > 0) {
    usleep(100);
    $retNum = $this->redis->decr(&#39;num&#39;);
    if($retNum >= 0){
      $res = $this->redis->lPush(&#39;result&#39;,$retNum);
      if($res == false){
        echo "writeLog:".$retNum;
      }else{
        echo "success:".$retNum;
      }
    }else{
      echo "fail1";
    }
  }else{
    echo "fail2";
  }
}

④基于setnx的排它锁方案

redis没有像mysql中的排它锁,但是可以通过一些方式实现排它锁的功能,就类似php使用文件锁实现排它锁一样。

setnx实现了exists和set两个指令的功能,若给定的key已存在,则setnx不做任何动作,返回0;若key不存在,则执行类似set的操作,返回1。

我们设置一个超时时间timeout,每隔一定时间尝试setnx操作,如果设置成功就是获得了相应的锁,执行num的decr操作,操作完成删除相应的key,模拟释放锁的操作。

public function run(){
  do {
    $res = $this->redis->setnx("numKey",1);
    $this->timeout -= 100;
    usleep(100);
  }while($res == 0 && $this->timeout>0);
  if($res == 0){
    echo &#39;fail1&#39;;
  }else{
    $num = $this->redis->get(&#39;num&#39;);
    if($num > 0) {
      $this->redis->decr(&#39;num&#39;);
      usleep(100);
      $res = $this->redis->lPush(&#39;result&#39;,$num);
      if($res == false){
        echo "fail2";
      }else{
        echo "success:".$num;
      }
    }else{
      echo "fail3";
    }
    $this->redis->del("numKey");
  }
}

推荐学习:《PHP视频教程

The above is the detailed content of How to solve the problem of high concurrency (product flash sale) in PHP? Two solutions shared. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:微信公众号- PHP自学中心. If there is any infringement, please contact admin@php.cn delete