redis+lua优惠券秒杀demo
目标是实现高并发的优惠券秒杀系统,项目地址:https://github.com/owenliang/redis-lua
redis+lua
主要基于redis内嵌lua来实现库存的幂等性扣减,这块代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
local quan_id = tostring(KEYS[1]) local uid = tostring(ARGV[1]) -- 应答函数 local function response(errno, msg, data) errno = errno or 0 msg = msg or "" data = data or {} return cjson.encode({errno = errno, msg = msg, data = data}) end -- 判断用户没有抢过该优惠券 local log_key = "LOG_{" .. quan_id .. "}" -- return log_key local has_fetched = redis.call("sIsMember", log_key, uid) if (has_fetched ~= 0) then return response(-1, "已经领取过") end -- 遍历优惠券所有批次 local quan_key = "QUAN_{" .. quan_id .. "}" local batch_list = redis.call("hGetAll", quan_key) local result = false for batch_idx = 1, #batch_list, 2 do repeat -- 校验批次状态(是否online) local batch_info = cjson.decode(batch_list[batch_idx + 1]) if (batch_info["online"] ~= true) then break end -- 尝试从券池取出1个券码 local batch_key = batch_list[batch_idx] local coupon = redis.call("zRange", batch_key, 0, 0) if (#coupon == 0) then break end coupon = coupon[1] redis.call("zRem", batch_key, coupon) -- 弹出一个券码, 标记用户已抢 redis.call("sAdd", log_key, uid) -- 将券码放入异步队列 result = {uid = uid, quanId = quan_id, batchKey = batch_key, coupon = coupon} redis.call("rPush", "DB_QUEUE", cjson.encode(result)) until true if result ~= false then break end end if (result == false) then return response(-1, "优惠券已抢完") else return response(0, "秒杀成功", result) end |
脚本懒惰加载
当执行evalsha发现报错的情况下,则执行一次script load命令上传lua脚本,可以简化脚本上传的工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public static function fetchFromRedis($uid, $quanId) { // 出于性能考虑, sha1请提配置到程序中 $scriptSha1 = '8c2bdb856cefb05f39dc675c0fde28ef4c7bb0bd'; if (0) { // 加载lua脚本 $script = file_get_contents(__DIR__ . '/QuanFetch.lua'); // 计算lua的sha1哈希 $scriptSha1 = sha1($script); } $redisMaster = Redis::master('default'); // evalsha执行脚本, 完成秒杀 $result = $redisMaster->evalSha($scriptSha1, [$quanId, $uid], 1); // 按quanid做路由 if ($redisMaster->getLastError()) { // 如果evalsha报错, 则进行一次script load // 懒惰加载lua脚本 $script = file_get_contents(__DIR__ . '/QuanFetch.lua'); if (!$redisMaster->script('load', $script)) { return false; } // 然后重试脚本 $result = $redisMaster->evalSha($scriptSha1, [$quanId, $uid], 1); } $result = json_decode($result, true); return $result; } |
开发与调试脚本
直接基于PHP调用lua很难调试脚本的问题,官方的redis-cli支持单步调试lua脚本,你可以自己学习一下:《Redis Lua scripts debugger》。
我的开发流程是这样的:
编写lua脚本,然后命令行执行脚本:
redis-cli EVAL “$(cat /path/to/your/script.lua)” 1 “mykey” “myargv”
如果发现报错,则进行单步调试:
redis-cli –ldb –eval /path/to/your/script.lua mykey1 mykey2 , myargv1 myargv2
注意逗号之前的是key列表,之后的是argv列表,逗号两边需要留白。
使用起来就像gdb一样,n是下一步,p 变量名查看变量值,restart重新执行脚本,大家自己去挖掘更多吧。
压测脚本
先把脚本上传到redis,得到对应的SHA1值。
redis-cli SCRIPT LOAD “$(cat /path/to/your/script.lua)”
现在,执行压测:
redis-benchmark -r 100000000 -n 1000000 EVALSHA “8c2bdb856cefb05f39dc675c0fde28ef4c7bb0bd” 1 “mykey1” __rand_int__
-r指定了随机数的区间,__rand_int__用于随机数占位。
在这里,我让argv[1]随机生成,请求的次数是-n指定的,并发连接数使用-c(默认50)。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

有个问题我想问一下,for循环里面为什么要用repeat until 这种写法呢?
是lua语法限制,我希望可以跳出后续的处理,所以用了这个语法结构。
补充一下,eval执行lua会自动cache在server端,随后evalsha可以work,不需要主动script load.
请问下redis中使用lua脚本怎么解决多机部署的问题呢
文章有提到,懒惰上传。
先执行,如果不存在,再上传。
注意上传时,key采用{}的key hash tag路由机制,确保脚本上传到目标服务器:https://shift-alt-ctrl.iteye.com/blog/2285470。
1