基于Redis-Lua脚本的抢购方案

Lua 脚本功能是 Reids的一大亮点, 通过内嵌对 Lua 环境的支持,
Redis 解决了长久以来不能高效地处理 CAS (check-and-set) 命令的缺点。
并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。

该抢购方案基于Lua脚本,实际项目中可根据需要扩展命令。

伪代码

  1. 是否已抢购(是否达到抢购上限)
  2. 是否还有库存
  3. 如果未抢购、有库存则扣减,否则返回0
  4. 加入已抢购集合
  5. 加入购买处理队列
  6. 返回1

脚本

编写脚本

script.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- variable
local stockKey = KEYS[1]
local purchasedKey = KEYS[2]
local handlerKey = KEYS[3]
local userId = ARGV[1]

-- check purchased set
if redis.call("SISMEMBER", purchasedKey, userId) ~= 0 then return 0 end

-- check stock
local stock = redis.call("GET", stockKey)
if not stock or tonumber(stock) <= 0 then return 0 end

-- stock decrease
redis.call("DECR", stockKey)
-- add purchased set
redis.call("SADD", purchasedKey, userId)
-- add purchased handler queue
redis.call("LPUSH", handlerKey, userId)
return 1

调试脚本

20200602171231.png

又是个坑:参数的分隔用逗号 , 而且空格不能少

redis-cli --ldb --eval script.lua apple_stock apple_purchased_set apple_purchased_queue , 711

20200602165535.png
20200602165550.png

测试脚本

redis-cli

文本转换为命令:

1
script load "-- variable\nlocal stockKey = KEYS[1]\nlocal purchasedKey = KEYS[2]\nlocal handlerKey = KEYS[3]\nlocal userId = ARGV[1]\n\n-- check purchased set\nif redis.call(\"SISMEMBER\", purchasedKey, userId) ~= 0 then return 0 end\n\n-- check stock\nlocal stock = redis.call(\"GET\", stockKey)\nif not stock or tonumber(stock) <= 0 then return 0 end\n\n-- stock decrease\nredis.call(\"DECR\", stockKey)\n-- add purchased set\nredis.call(\"SADD\", purchasedKey, userId)\n-- add purchased handler queue\nredis.call(\"LPUSH\", handlerKey, userId)\nreturn 1"

得到sha1,执行命令:

1
evalsha a80403df241c6d1823f5c820a4d2b6284995444e 3 apple_stock apple_purchased_set apple_purchased_queue 777

20200602171006.png

对比

lua

j-lua-10000
j-lua-20000

lua-cluster

j-lua-cluster-10000
j-lua-cluster-20000

watch

j-watch-10000
j-watch-20000

  • lua 方案抢购先到先得,基本不会出现失败。
  • lua 3个Master节点集群,性能对比单实例性能低。
  • watch 方案里,会出现大概30%的抢购失败,且性能比lua方案低。

ps. 由于集群版的 Key 批量操作限制,mset、mget、eval,目前只支持具有 相同slot值的Key 执行批量操作。
所以在集群版的抢购方案中,传入的Key需要带上{}把键值映射入相同的slot,让命令在一个节点执行。
例如:

1
evalsha a80403df241c6d1823f5c820a4d2b6284995444e 3 apple_stock{apple} apple_purchased_set{apple} apple_purchased_queue{apple} 777

应用

  • 可以把有原子性要求的操作放入脚本
  • 监听apple_purchased_queue, 消费消息处理抢购。
  • 如果架构里有MQ,可以去除脚本里的LPUSH
  • 消费者如果处理异常,可以使用延迟队列做”失败重试”、”失效检查”之类操作

队列&延迟队列:
https://www.cnblogs.com/shamo89/p/9873368.html

事务&lua&管道:
https://blog.csdn.net/u013870094/article/details/82461527

命令:
https://redis.io/commands#
https://redis.io/topics/ldb
https://redis.io/commands/script-load
https://redis.io/commands/evalsha
https://www.runoob.com/redis/redis-commands.html

LUA脚本:
https://yq.aliyun.com/articles/645851
https://blog.csdn.net/xixingzhe2/article/details/86167859
https://www.jianshu.com/p/366d1b4f0d13
https://www.jianshu.com/p/8028972cf735
https://www.runoob.com/lua/lua-miscellaneous-operator.html

异常:
Too many cluster redirections redis:
https://segmentfault.com/a/1190000016461888
https://blog.csdn.net/slg1988/article/details/93638501
https://blog.kelu.org/tech/2019/04/05/docker-compose-using-net-host.html

Connection reset:
https://www.cnblogs.com/liu-ke/p/7090698.html