秒杀抢购思路以及高并发下数据安全【2】

2017-01-01 08:13 阅读 () 评论 () 喜欢 () 秒杀
优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false2. 悲观锁思路解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。


优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false


2. 悲观锁思路

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。


 


虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。

也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。

同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。


优化方案2:使用MySQL的事务,锁住操作的行

<?php//优化方案2:使用MySQL的事务,锁住操作的行include('./mysql.php');
//生成唯一订单号function build_order_no(){
  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}//记录日志function insertLog($event,$type=0){
    global $conn;
    $sql="insert into ih_log(event,type)
    values('$event','$type')";
    mysqli_query($conn,$sql);
}//模拟下单操作//库存是否大于0mysqli_query($conn,"BEGIN");  //开始事务$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行$rs=mysqli_query($conn,$sql);
$row=$rs->fetch_assoc();if($row['number']>0){
    //生成订单    $order_sn=build_order_no();
    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
    $order_rs=mysqli_query($conn,$sql);
    //库存减少    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
    $store_rs=mysqli_query($conn,$sql);
    if($store_rs){
      echo '库存减少成功';
        insertLog('库存减少成功');
        mysqli_query($conn,"COMMIT");//事务提交即解锁    }else{
      echo '库存减少失败';
        insertLog('库存减少失败');
    }
}else{
  echo '库存不够';
    insertLog('库存不够');
    mysqli_query($conn,"ROLLBACK");
}?>


3. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的

采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。

看到这里,是不是有点强行将多线程变成单线程的感觉哈。


 

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。

那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。


4. 文件锁的思路

对于日IP不高或者说并发数不是很大的应用,一般不用考虑这些!用一般的文件操作方法完全没有问题。

但如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件进行操作,如果这时不对文件的访问进行相应的独占,就容易造成数据丢失


5. 乐观锁思路

这个时候,我们就可以讨论一下“乐观锁”的思路了。

乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。

实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。

但是,综合来说,这是一个比较好的解决方案。

 

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一

通过这个实现,我们保证了数据的安全。

优化方案5:Redis中的watch

<?php$redis = new redis(); $result = $redis->connect('127.0.0.1', 6379); echo $mywatchkey = $redis->get("mywatchkey");/*
  //插入抢购数据 if($mywatchkey>0)
 {     $redis->watch("mywatchkey");
  //启动一个新的事务。    $redis->multi();   $redis->set("mywatchkey",$mywatchkey-1);   $result = $redis->exec();   if($result) {      $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());      $watchkeylist = $redis->hGetAll("watchkeylist");        echo "抢购成功!<br/>"; 
        $re = $mywatchkey - 1;   
        echo "剩余数量:".$re."<br/>";        echo "用户列表:<pre>";        print_r($watchkeylist);
   }else{      echo "手气不好,再抢购!";exit;
   }  
 }else{
     // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");
     //  $watchkeylist = $redis->hGetAll("watchkeylist");        echo "fail!<br/>";    
        echo ".no result<br/>";        echo "用户列表:<pre>";
      //  var_dump($watchkeylist);  
 }*/$rob_total = 100;   //抢购数量if($mywatchkey<=$rob_total){    $redis->watch("mywatchkey");    $redis->multi(); //在当前连接上启动一个新的事务。
    //插入抢购数据
    $redis->set("mywatchkey",$mywatchkey+1);    $rob_result = $redis->exec();    if($rob_result){         $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);        $mywatchlist = $redis->hGetAll("watchkeylist");        echo "抢购成功!<br/>";     
        echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";        echo "用户列表:<pre>";
        var_dump($mywatchlist);
    }else{          $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');        echo "手气不好,再抢购!";exit;
    }
}?>