思路: 商品库存用redis的list结构管理,下单时判断库存操作,扣除库存成功才成功下单,否则返回下单失败。
库存,苹果6万,葡萄6万,西瓜8万,总共20万。线上开8个客户端,每个客户端发起3万次请求,看是否出现超卖。
3次实验结果:20万商品库存消耗完,不多不少,生成20万笔订单。耗时分别为318秒、327秒、307秒。
性能:200000笔/320秒,性能上看,redis原子锁方式约为mysql排他锁的2.5倍。
关键代码
// 库存入redis
private function _add_inventory_to_redis(Collection $goodsList)
{
$redis = Redis::connection();
$result = 0;
foreach($goodsList as $goods) {
// 商品库存信息入redis
$redisKey = "testing_goods_{$goods->id}";
for($i = 0; $i < $goods->num; $i++){
$result = $redis->lpush($redisKey, 1);
print_r($result);
echo PHP_EOL;
}
}
return $result;
}
// 模拟下单
$userList = range(1, 10000);
foreach($goodsList as $goods) {
// 模拟高并发抢购
$orderCreatingRequest->query->set('goods_id', $goods->id);
foreach($userList as $userId) {
$orderCreatingRequest->query->set('user_id', $userId);
$result = app(TestController::class)->createOrderByRedisList($orderCreatingRequest);
print_r($result->original);
echo PHP_EOL;
}
}
// 根据redis的list 下单
public function createOrderByRedisList(Request $request)
{
DB::beginTransaction();
try{
$userId = $request->input('user_id');
$goodsId = $request->input('goods_id');
$keyPrefix = $request->input('redis_key_prefix');
$redis = Redis::connection();
// key逻辑取自Command/TestOversaleOrder
$redisKey = $keyPrefix ."{$goodsId}";
$result = $redis->lpop($redisKey);
if ($result){
// 订单入库
$order = new TestingOrder();
$order->user_id = $userId;
$order->goods_id = $goodsId;
$order->goods_num = 1;
$order->save();
DB::commit();
return $this->response->json(['status' => 'success', 'msg' => '下单成功', 'result' => $result]);
}else{
DB::rollBack();
return $this->response->json(['status' => 'error', 'msg' => '下单失败']);
}
}catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
实验过程中发现个问题,如下图,没有DB::commit语句时,高并发下会报错:SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
另外,此处有对商品库存、版本号操作,发现redis库存操作+订单入库数量没问题,消耗苹果库存235256,生成订单数为235256,剩余库存为64744,但商品表的库存和版本号错误。重试一次,此问题依旧存在。 因此,即使用到redis原子锁,如果要维护mysql表的库存,还是得在mysql层使用锁(排他锁、乐观锁)。