建一张测试表

首先我们来创建一张库存表

  1. CREATE TABLE `stock` (
  2. `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  3. `name` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '名称',
  4. `stock` INT(10) NOT NULL DEFAULT '0' COMMENT '库存',
  5. PRIMARY KEY (`id`),
  6. INDEX `stock` (`stock`)
  7. )
  8. COMMENT='库存表'
  9. COLLATE='utf8_general_ci'
  10. ENGINE=InnoDB;

再随便添两条数据,最后的样子如下
1542349641287887.png

常规操作

下面我们来写php程序

<?php
//连接数据库
$option = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC];
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'root', 'root', $option);

//查询
$sql = 'select * from stock where id = :id';
$sth = $pdo->prepare($sql);
$sth->bindValue(':id',1);
$sth->execute();
$res = $sth->fetch();
//如果有库存那么库存减1
if ($res['stock'] > 0) {
    //修改
    $sql = 'update stock set stock = stock-1 where id = :id';
    $sth = $pdo->prepare($sql);
    $sth->bindValue(':id',1);
    $sth->execute();
}else{
    echo '无库存';
}

上面程序,一看没啥问题,先查询库存,库存大于0,进行减库存(添加订单)操作
但是我们测试一下,用Apache自带的工具ab

$ ab -n 1200 -c 1200 http://localhost/test.php

测试几次,总会发现库存为负数的情况,如
ddd.png

在并发情况下这就出现了超卖的问题了,那么怎么解决呢?下面我们来看看

数据库锁 - 悲观锁

把上面的程序改为

<?php
//连接数据库
$option = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC];
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'root', 'root', $option);

//开启事务
$pdo->beginTransaction();

//查询 - select ... for update
$sql = 'select * from stock where id = :id for update';
$sth = $pdo->prepare($sql);
$sth->bindValue(':id',1);
$sth->execute();
$res = $sth->fetch();
//如果有库存那么库存减1
if ($res['stock'] > 0) {
    //修改
    $sql = 'update stock set stock = stock-1 where id = :id';
    $sth = $pdo->prepare($sql);
    $sth->bindValue(':id',1);
    $sth->execute();
}else{
    echo '无库存';
}

//提交事务
$pdo->commit();

如上所示,加了一个事务,并且在selest语句上加了 for update
这样我们就给数据表加上了一道锁,我们把这称为悲观锁
select … for update 把此行锁定,在事务提交之前,其他地方是不能修改此行的
锁定后,我们就继续我们的操作减库存的减库存,添加订单的添加订单,最后都完成了后再提交事务
经过上面的改进后,经过多次测试,库存并不会出现负数的情况
aaa.png

文件锁

<?php
//连接数据库
$option = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC];
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'root', 'root', $option);
//随意打开/创建一个文件
$fp = fopen("order.lock", "w");
//锁定
if(flock($fp,LOCK_EX)){
    //查询
    $sql = 'select * from stock where id = :id';
    $sth = $pdo->prepare($sql);
    $sth->bindValue(':id',1);
    $sth->execute();
    $res = $sth->fetch();
    //如果有库存那么库存减1
    if ($res['stock'] > 0) {
        //修改
        $sql = 'update stock set stock = stock-1 where id = :id';
        $sth = $pdo->prepare($sql);
        $sth->bindValue(':id',1);
        $sth->execute();
    }else{
        echo '无库存';
    }
    //解锁
    flock($fp,LOCK_UN);
}else{
        echo  '系统繁忙';
}
//关闭文件
fclose($fp);

总的来说就是,打开一个文件并锁定
这里用的是 阻塞(等待)模式(只要有其他进程已经加锁文件,当前进程会一直等其他进程解锁文件)
也可以用 非阻塞(等待)模式(如果其他进程已经加锁文件,当前进程不会等其他进程解锁文件,直接返回,也就是直接忽略加锁的代码到关闭文件那块)
用非阻塞模式的话,直接把上面的 if(flock($fp,LOCK_EX)) 改为 if(flock($fp,LOCK_EX | LOCK_NB))
怎么样,是不是很简单呢,动手试试看吧