-
解决方法一 悲观锁
public function seckill()
{
DB::beginTransaction();
$detail = DB::select("select num,id from zlsn_concurrency_goods where id = 1 for update ");
$ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
$userId = "测试用户" . $ids[rand(0, 9)];
if (!empty($detail) && $detail[0]->num > 0) {
$number = $detail[0]->num;
$id = $detail[0]->id;
$testUserOrderModel = new TestUserOrder();
$check = $testUserOrderModel->where('user_code', $userId)->get()->toArray();
if (empty($check)) {
$number -= 1;
$testGoodModel = new TestGoods();
$param['num'] = $number;
$res = $testGoodModel->where('id', $id)->update($param);
if ($res) {
$testUserOrderModel->user_code = $userId;
$testUserOrderModel->order_code = rand(111111, 999999);
$testUserOrderModel->save();
echo '抢购成功';
DB::commit();
} else {
echo '商品数量修改失败';
DB::rollBack();
}
} else {
echo '您已经购买过此商品';
DB::rollBack();
}
} else {
echo '商品数量不足';
DB::rollBack();
}
}
在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,其余请求必须等待。
缺点:虽然可以解决线程安全的问题,但是对于高并发并不适用。对于很多没有抢到锁的请求,会陷入一直等待的状态,同时,这种请求很多的话,瞬间增大系统的平均响应时间,结果可能是数据库连接数耗尽,系统陷入异常。
-
解决方法二 FIFO队列思路
FIFO(first input first output):即先进先出。这样的话就会避免有些请求永远获取不到锁的情况。但是有种将多线程强行变成单线程的感觉。就像是一条八车道的路,强行并称了单行道。
缺点:如果使用这种情况,可能一瞬间把队列内存撑爆,然后系统陷入异常。假如设计一个超级大的内存队列,但是系统处理队列内请求的速度,根本无法和疯狂涌入队列内的数目相比。也就是说队列内的请求会越积累越多,最终web系统平均相应时长还是会增大,造成系统异常。
-
解决方法三 文件锁思路
使用非阻塞的文件排他锁
缺点:在我们对文件进行读写操作时,很有可能多个进程对进一文件进行操作,如果这时不对文件的访问进行相应的独占,就容易造成数据丢失
public function seckill()
{
$fp = fopen("lock.txt", "w+");
if (!flock($fp, LOCK_EX | LOCK_NB)) {
echo "系统繁忙,请稍后再试";
return;
}
$detail = TestGoods::where('id', 1)->select('id', 'num')->get()->toArray();
$ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
$userId = "测试用户" . $ids[rand(0, 9)];
if (!empty($detail) && $detail[0]['num'] > 0) {
$number = $detail[0]['num'];
$id = $detail[0]['id'];
$testUserOrderModel = new TestUserOrder();
$check = $testUserOrderModel->where('user_code', $userId)->get()->toArray();
if (empty($check)) {
$number -= 1;
$testGoodModel = new TestGoods();
$param['num'] = $number;
$res = $testGoodModel->where('id', $id)->update($param);
if ($res) {
$testUserOrderModel->user_code = $userId;
$testUserOrderModel->order_code = rand(111111, 999999);
$testUserOrderModel->save();
echo '抢购成功';
flock($fp, LOCK_UN);
} else {
echo '商品数量修改失败';
}
} else {
echo '您已经购买过此商品';
}
} else {
echo '商品数量不足';
}
fclose($fp);
}
-
解决方法四 乐观锁
在数据表中添加version字段。
所有进来的请求都可以去修改数据,但是在修改数据之前会先获得该数据的一个版本号,修改之后如果和修改前的版本号不一致,则进行事务回滚。
缺点:增大CPU的计算开销。
public function seckill()
{
DB::beginTransaction();
$detail = TestGoods::where('id', 1)->select('id', 'num','version')->get()->toArray();
$ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
$userId = "测试用户" . $ids[rand(0, 9)];
if (!empty($detail) && $detail[0]['num'] > 0) {
$number = $detail[0]['num'];
$id = $detail[0]['id'];
$version = $detail[0]['version'];
$testUserOrderModel = new TestUserOrder();
$check = $testUserOrderModel->where('user_code', $userId)->get()->toArray();
if (empty($check)) {
$number -= 1;
$testGoodModel = new TestGoods();
$param['num'] = $number;
$res = $testGoodModel->where('id', $id)->update($param);
$data = TestGoods::where('id', 1)->select('version')->get()->toArray();
$version_after = $data[0]['version'];
if($version_after != $version){
DB::rollBack();
}
if ($res) {
$testUserOrderModel->user_code = $userId;
$testUserOrderModel->order_code = rand(111111, 999999);
$testUserOrderModel->save();
echo '抢购成功';
DB::commit();
} else {
echo '商品数量修改失败';
DB::rollBack();
}
} else {
echo '您已经购买过此商品';
DB::rollBack();
}
} else {
echo '商品数量不足';
DB::rollBack();
}
}
-
Redis中的watch
这种方法也属于乐观锁之一。(模拟了多次,其中有两次还是出现了超卖的情况,不知道是不是自己操作的问题。)
public function seckill()
{
$myWatchKey = Redis::get('myWatchKey');
$number = 20;
if ($myWatchKey < $number) {
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 "剩余数量:" . ($number - $myWatchKey - 1) . "<br/>";
echo "用户列表:<pre>";
var_dump($myWatchList);
} else {
Redis::hSet("watchKeyList", "user_" . mt_rand(1, 9999), '没抢到');
echo "手气不好,再抢购!";
exit;
}
}else{
echo '商品数量不足,抢购失败';
}
}