public function stockLuaDecr($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
--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;
if (!$redis->eval($lua, [json_encode($goodsList, JSON_UNESCAPED_UNICODE)], 1)) {
return false;
}
return true;
}
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');
if (!$this->stockLuaDecr($goodsList)) {
$this->error('库存不足');
}
try {
} catch (\Exception $e) {
$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脚本的执行;