有一个场景
某网站准备做一个准点抢购活动,优惠力度非常大,每个用户每件商品限购3件。
方案和问题使用 MySQL 事务
一开始我们使用 mysql 的事务来处理库存和订单,但是发现当并发较高时出现很多的异常:
Error: too many open files
这个异常的原因在于,当一个请求的事务在执行时,其他请求的事务都必须等待,导致很多数据库连接处于使用中的状态,新的请求无法从连接池中获取数据库连接,便不断创建新的数据库连接,最终导致数据库连接数超过预设的最大值(MySQL默认最大连接数为151)。
2. 使用 Redis
由于redis是单线程的服务,因此不必考虑资源竞争的问题,也就是说它的数据操作肯定是安全的。但是我们会涉及到扣除库存和创建订单两个操作,而且这两个操作需要保证原子性,因此使用Lua脚本应该是比较好的选择。
Redis中使用Lua脚本的好处减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延
原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
复用。客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
实现步骤
1. 准备一个Lua脚本
-- 实际代码中,商品id和数量都是通过参数传递过来的,而且支持同时购买多种商品
-- 但是因为参数的处理过于繁琐,因此就删除了这部分代码
-- 最主要的还是扣库存和生成订单的过程
local product_id = "123"
local amount = 3
local user_id = "123456"
local amt = redis.call("get", goods_key)
if amt < amount then
return 0
end
local item = product_id ..":"..amount
local order_key = "ORDER:"..user_id
-- 扣减库存
redis.call("decrby", product_id, amount)
-- 创建订单:往订单队列添加数据即可
redis.call("lpush", order_key, item)
redis.log(redis.LOG_NOTICE, "success to crete order, order_key = "..order_key)
return 1
2. 命令行执行脚本
# 将lua脚本载入 redis 中缓存起来,会得到一个哈希值 hash_value
ubuntu > redis-cli -x script load < script.lua
# 然后使用redis-cli进入命令行,执行以下命令,执行lua脚本:
redis 127.0.0.1:6379 > EVALSHA hash_value 0
# 我们来查看该用户的订单队列
redis 127.0.0.1:6379 > LLEN LORDER:123456
# 从订单队列取出数据
redis 127.0.0.1:6379 > RPOP ORDER:123456
3. Python 操作 Redis 执行脚本
import redis
red = redis.Redis(host="127.0.0.1", port=6379)
lua_script = """将脚本内容作为字符串赋值给lua_script"""
cmd = red.register_script(lua_script)
res = cmd()
# cmd 命令可以接收两个参数,keys 和 args
# 但这两个参数都必须是列表类型
一些说明
传递参数
我们可能会需要从程序中传递一些参数到lua脚本中,比如我需要将用户购买的多个商品以及各个商品的数量传递到脚本中。
cmd实际上是可以接收参数的,它有两个参数:keys 和 args,但这两个参数都必须是列表类型。使用方法如下:
lua_script = """local key = KEYS[1]local field = ARGV[1]local timestamp_new = ARGV[2]"""
cmd = red.register_script(lua_script)
res = cmd(keys=['hello'], args=['time', 1533299183])
Evalsha 命令
Evalsha 命令可以根据给定的sha1校验码,执行缓存过的脚本。
将脚本缓存的操作可以通过 script load 命令完成。
语法
redis 127.0.0.1:6379 > EVALSHA sha1 numkeys key [key ...] arg [arg ...]
参数说明sha1:通过 script load 缓存文件生成的sha1 校验码
numkeys :指明 key 参数的数量
key [key ...]:从第三个参数开始算起,这些参数可以在Lua脚本中通过全局变量 KEYS数组来访问,下标从1开始,如 KEYS[1], KEYS[2] ...
arg [arg ...] :附加参数,在脚本中通过全局变量 ARGV数组 来访问,下标也是从1开始。因为已经通过 numkeys指定了 key的数量,因此程序可以找到 arg 的起始位置。
Eval 命令
对于一些比较简短的lua脚本,我们并不需要提前缓存文件,可以直接通过Eval命令来执行并得到结果。语法跟 Evalsha 基本一样。
语法:
redis 127.0.0.1:6379 > EVAL script numkeys key [key ...] arg [arg ...]
示例:
redis 127.0.0.1:6379 > EVAL "return KEYS[1]..KEYS[2]..ARGV[1]..ARGV[2]" 2 key1 key2 first second
日志
可以通过 redis.log 打印日志,而且可以设定日志级别。这些日志会打印到redis的日志文件中。使用方法:
redis.log(redis.LOG_NOTICE, "notice please")
日志级别:
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
参考