内容简介:首发于Redis 从 2.6 版本起,也已开始支持
首发于 樊浩柏科学院
Redis 从 2.6 版本起,也已开始支持 Lua 脚本 ,我们可以更加得心应手地使用或扩展 Redis,特别是在高并发场景下 Lua 脚本提供了更高效、可靠的解决方案。
<!--more-->
为什么要使用Lua
我们先看一个抢购场景下的问题,用 PHP 可简单实现为:
$key = 'number:string'; $redis = new Redis(); $number = $redis->get($key); if ($number <= 0) { return 0; } $redis->decr($key); return $number--;
这段代码其实存在问题,高并发时会出现库存超卖的情况,因为上述操作在 Redis 中不是原子操作,会导致库存逻辑的判断失效。尽管可以通过优化代码来解决问题,比如使用原子操作命令、或者使用的方式,但这里使用 Lua 脚本来解决。
local key = 'number:string' local number = tonumber(redis.call("GET", key)) if number <= 0 then return 0 end redis.call("DECR", key) return number--
这段脚本代码虽然是 Lua 语言编写( 进入Lua的世界 ),但是其实就是 PHP 版本的翻译版。那为什么这样,Lua 脚本就能解决库存问题了呢?
Redis 中嵌入 Lua 脚本,所具有的几个特性为:
- :Redis 将整个 Lua 脚本作为一个原子执行,无需考虑并发,无需使用事务来保证数据一致性;
- :嵌入 Lua 脚本后,可以减少多个命令执行的网络开销,进而间接提高 Redis 性能;
- :Lua 脚本会保存于 Redis 中,客户端都可以使用这些脚本;
在Redis中嵌入Lua
使用Lua解析器
Redis 提供了 EVAL(直接执行脚本) 和 EVALSHA(执行 SHA1 值的脚本) 这两个命令,可以使用内置的 Lua 解析器执行 Lua 脚本。语法格式为:
- script numkeys key [key ...] arg [arg ...]
- sha1 numkeys key [key ...] arg [arg ...]
参数说明:
- script / sha1:EVAL 命令的第一个参数为需要执行的 Lua 脚本字符,EVALSHA 命令的一个参数为 Lua 脚本的 SHA1 值
- numkeys:表示 key 的个数
- key [key ...]:从第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局数组 KYES[i] 访问
- arg [arg ...]:附加参数,在 Lua 中通过全局数组 ARGV[i] 访问
EVAL 命令的使用示例:
> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
每次使用 EVAL 命令都会传递需执行的 Lua 脚本内容,这样增加了宽带的浪费。Redis 内部会永久保存被运行在脚本缓存中,所以使用 EVALSHA(建议使用) 命令就可以根据脚本 SHA1 值执行对应的 Lua 脚本。
> SCRIPT LOAD "return 'hello'" "1b936e3fe509bcbc9cd0664897bbe8fd0cac101b" > EVALSHA 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 0 "hello"
Redis 中执行 Lua 脚本都是以原子方式执行,所以是原子操作。另外,redis-cli 命令行客户端支持直接使用 --eval lua_file
参数执行 Lua 脚本。
Redis 中有关脚本的命令除了 EVAL 和 EVALSHA 外,如下:
命令 | 描述 |
---|---|
SCRIPT EXISTS script [script ...] | 查看脚本是是否保存在缓存中 |
SCRIPT FLUSH | 从缓存中移除所有脚本 |
SCRIPT KILL | 杀死当前运行的脚本 |
SCRIPT LOAD script | 将脚本添加到缓存中,不立即执行 返回脚本SHA1值 |
数据类型的转换
由于 Redis 和 Lua 都有各自定义的数据类型,所以在使用执行完 Lua 脚本后,会存在一个数据类型转换的过程。
Lua 到 Redis 类型转换与 Redis 到 Lua 类型转换相同部分关系:
Lua 类型 | Redis 返回类型 | 说明 |
---|---|---|
number | integer | 浮点数会转换为整数 3.333-->3 |
string | bulk | |
table(array) | multi bulk | |
boolean false | nil |
> EVAL "return 3.333" 0 (integer) 3 > EVAL "return 'fhb'" 0 "fhb" > EVAL "return {'fhb', 'lw', 'lbf'}" 0 1) "fhb" 2) "lw" 3) "lbf" > EVAL "return false" 0 (nil)
需要注意的是,从 Lua 转化为 Redis 类型比 Redis 转化为 Lua 类型多了一条规则:
Lua 类型 | Redis 返回类型 | 说明 |
---|---|---|
boolean true | integer | 返回整型 1 |
> EVAL "return true" 0 (integer) 1
总而言之,是将一个 Redis 值转换成 Lua 值,之后再将转换所得的 Lua 值转换回 Redis 值,那么这个转换所得的 Redis 值应该和最初时的 Redis 值一样。
全局变量保护
为了防止不必要的数据泄漏进 Lua 环境, Redis 脚本不允许创建全局变量。
-- 定义全局函数 function f(n) return n * 2 end return f(4);
执行 redis-cli --eval function.lua
命令,会抛出尝试定义全局变量的错误:
(error) ERR Error running script (call to f_0a602c93c4a2064f8dc648c402aa27d68b69514f): @enable_strict_lua:8: user_script:1: Script attempted to create global variable 'f'
Lua脚本调用Redis命令
Redis 创建了用于与 Lua 环境协作的组件—— 伪客户端,它负责执行 Lua 脚本中的 Redis 命令。
调用Redis命令
在 Redis 内置的 Lua 解析器中,调用 redis.call() 和 redis.pcall() 函数执行 Redis 的命令。它们除了处理错误的行为不一样外,其他行为都保持一致。调用 格式:
- redis.call(command, [key ...], arg [arg ...] )
- redis.pcall(command, [key ...], arg [arg ...] )
> EVAL "return redis.call('SET', 'name', 'fhb')" 0 > EVAL "return redis.pcall('GET', 'name')" 0 "fhb"
Redis日志
在 Lua 脚本中,可以通过调用 redis.log() 函数来写 Redis 日志。格式为:
redis.log(loglevel, message)
loglevel 参数可以是 redis.LOG_DEBUG、redis.LOG_VERBOSE、redis.LOG_NOTICE、redis.LOG_WARNING 的任意值。
查看 redis.conf
日志配置信息:
# logleval必须一致才会记录 loglevel notice logfile "/home/logs/redis.log"
Lua 写 Redis 日志示例:
> EVAL "redis.log(redis.LOG_NOTICE, 'I am fhb')" 0 113:M 04 Sep 13:12:36.229 * I am fhb
案例
API 访问速率控制
通过 Lua 实现一个针对用户的 API 访问速率控制,Lua 代码如下:
local key = "rate.limit:string:" .. KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = tonumber(ARGV[2]) local times = redis.call("INCR", key) if times == 1 then redis.call("EXPIRE", key, expire_time) end if times > limit then return 0 end return 1
KEYS[1] 可以用 API 的 URI + 用户 uid 组成,ARGV[1] 为单位时间限制访问的次数,ARGV[2] 为限制的单位时间。
批量HGETTALL
这个例子演示通过 Lua 实现批量 HGETALL,当然也可以使用 管道 实现。
-- KEYS为uid数组 local users = {} for i,uid in ipairs(KEYS) do local user = redis.call('hgetall', uid) if user ~= nil then table.insert(users, i, user) end end return users
注意事项
虽然使用 Lua 脚本给我们带来了许多便利,但是需要注意几个使用事项:
{}
- 进入Lua的世界 (2017-09-03)
- Lua在Nginx的应用 (2017-09-09)
以上所述就是小编给大家介绍的《Lua在Redis的应用》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 袜子商店应用:一个云原生参照应用
- Android 应用中跳转到应用市场评分
- 授之以渔-运维平台应用模块一(应用树篇)
- OAM(开放应用模型)——定义云原生应用标准的野望
- ChromeOS 终端应用程序暗示其即将支持 Linux 应用
- Android应用之间数据的交互(一)获取系统应用的数据
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Hacking Growth
Sean Ellis、Morgan Brown / Crown Business / 2017-4-25 / USD 29.00
The definitive playbook by the pioneers of Growth Hacking, one of the hottest business methodologies in Silicon Valley and beyond. It seems hard to believe today, but there was a time when Airbnb w......一起来看看 《Hacking Growth》 这本书的介绍吧!