防止超卖之二 - redis原子锁

63 阅读1分钟

思路: 商品库存用redis的list结构管理,下单时判断库存操作,扣除库存成功才成功下单,否则返回下单失败。

库存,苹果6万,葡萄6万,西瓜8万,总共20万。线上开8个客户端,每个客户端发起3万次请求,看是否出现超卖。

3次实验结果:20万商品库存消耗完,不多不少,生成20万笔订单。耗时分别为318秒、327秒、307秒。

性能:200000笔/320秒,性能上看,redis原子锁方式约为mysql排他锁的2.5倍。

image.png

image.png

image.png

image.png

image.png

image.png

关键代码

// 库存入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

image.png

image.png

另外,此处有对商品库存、版本号操作,发现redis库存操作+订单入库数量没问题,消耗苹果库存235256,生成订单数为235256,剩余库存为64744,但商品表的库存和版本号错误。重试一次,此问题依旧存在。 因此,即使用到redis原子锁,如果要维护mysql表的库存,还是得在mysql层使用锁(排他锁、乐观锁)。