php+redis+lua脚本的库存加减原子操作

	/***
	 * 减库存
	 * @return void
	 */
    public function stockLuaDecr($goodsList = [])
    {
    	//class上面自行引用一下 use Redis;
    	$redis = new Redis();
    	#先将用户提交订单的商品列表处理成以下格式
        $goodsList = [
            [
                'nums' => 2,//购买的数量
                'redisKey' => 'goods_sku:1'//Redis的key
            ],
            [
                'nums' => 2,//购买的数量
                'redisKey' => 'goods_sku:2'//Redis的key
            ],
            [
                'nums' => 1,//购买的数量
                'redisKey' => 'goods_sku:3'//Redis的key
            ]
        ];
        //这里的<<<是必须的,后面lua大小写都可以(LUA),包括用script(SCRIPT)也一样能执行,
        //只需保证头尾标签一致就行
        $lua = <<<lua
        --KEYS[1]就是eval方法的第二参数传过来的数组,用json_encode做了转换
        --lua中数组的下标是从1开始,跟php有区别,KEYS[0]是错误的,都是从KEYS[1]开始,
        --相当于eval第二个参数中的数组下标的0
        --可以把local理解为js里面的var,就是声明局部变量用的
        local data = KEYS[1]
        --将json格式转化成数组,同理还可以将数组转化成json用于返回(cjson.encode(数组))
        local data = cjson.decode(data)
        local success = 1
        --k和v就相当于是foreach的key和value,循环的时候可以直接用v["字段名"]
        --in pairs(数组) do 是固定写法,结尾用end,if (条件) then 代码 end 也是固定写法
        --校验key是否存在
        for k, v in pairs(data) do
        	--如果key不存在,直接返回false,如果想返回具体内容,可以对return返回值进行修改
        	--,这里不再举例
            if (redis.call("exists",v["redisKey"]) == 0) 
            then
                return false
            end
        end
        --减全部购买的库存
        for k, v in pairs(data) do
        	--如果decrby之后的库存变成负数了,就把定义的'success'这个变量改成0
        	--暂时不用立刻加回来,反正只要有1个失败了就都要回滚
            if (redis.call("decrby", v["redisKey"], v["nums"]) < 0)
            then
                success = 0
            end
        end
        --如果存在库存小于0的商品,再将全部商品库存加回来,相当于回滚操作
        if (success == 0) 
        then
            for k, v in pairs(data) do
                redis.call("incrby", v["redisKey"], v["nums"])
            end
            return false
        end
        return true
lua;
		//eval方法第一个参数是脚本,第二个参数是数组,里面放一个json_decode的数组就可了
		//第三个参数是第二个参数数组元素的个数,本例中第二个参数数组只有一个元素,所以第三个参数就是1
        if (!$redis->eval($lua, [json_encode($goodsList, JSON_UNESCAPED_UNICODE)], 1)) {
            return false;
        }
        return true;
  	}

	/**
	* 回滚加库存
	* @param $goodsList
	* @return void
	*/
    public function stockLuaIncr($goodsList = [])
    {
        $redis = new Redis();
        $goodsList = [
            [
                'nums' => 2,
                'redisKey' => 'goods_sku:1'
            ],
            [
                'nums' => 2,
                'redisKey' => 'goods_sku:2'
            ],
            [
                'nums' => 1,
                'redisKey' => 'goods_sku:3'
            ]
        ];
        $lua = <<<lua
        local data = KEYS[1]
        local data = cjson.decode(data)
        for k, v in pairs(data) do
            redis.call("incrby", v["redisKey"], v["nums"])
        end
lua;
        $redis->eval($lua, [json_encode($goodsList, JSON_UNESCAPED_UNICODE)], 1);
    }
以上方法适用于创建订单时的库存校验,应放在db业务处理层的外面:
	public function createOrder()
    {
        $goodsList = input('goods_list');
        //执行lua库存校验和减库存
        if (!$this->stockLuaDecr($goodsList)) {
            $this->error('库存不足');
        }
        try {
            //DB层业务操作逻辑
        } catch (\Exception $e) {
            //如果DB层出问题了,需要把上面减掉的redis库存再加回来
            $this->stockLuaIncr($goodsList);
            $this->error('下单失败');
        }
        $this->success('下单成功');
    }
如果catch里的内容执行时出现错误,大不了不返还库存。
    $lua = <<<lua
    redis.call('lpush', KEYS[1],ARGV[1])//队列1(test:a:)
    redis.call('lpush1', KEYS[2],ARGV[2])//队列2(test:b:)
    redis.call('lpush', KEYS[3],ARGV[3])//队列3(test:c:)
    return 1
lua;

     $ret = redis()->eval($lua, ['test:a:', 'test:b:', 'test:c:', date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), date('Y-m-d H:i:s')], 3);
     var_dump($ret);
以上代码执行时会打印bool(false),意味着返回错误,执行最后结果是只有第一个队列1插入了值,队列2和3都没有插入值,即lua脚本里执行redis命令出错时,前面正确的redis命令会执行,出错后的命令都不会执行。redis执行lua脚本,整个lua脚本和普通的单个redis命令一样,中间不会被其他请求插入,具有原子性,减少网络开销。
上述的代码如果换成redis.pcall,执行结果会有差异,pcall执行最后结果,打印为int(1),队列1和队列3都插入了值,队列2因语句问题没有插入值,原因在于redis.call()函数会中断Lua脚本的执行,并抛出异常;redis.pcall()函数不会中断Lua脚本的执行;

PHP技术分享
请先登录后发表评论
  • latest comments
  • 总共0条评论