Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。
使用Lua脚本的好处如下:
- 1.减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。
- 2.原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
- 3.复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。
1、Redis命令行执行Lua脚本
01 、EVAL语法
- EVAL script numkeys key [key ...] arg [arg ...]
(1)script:需要执行的lua脚本
(2)numkeys: key的个数,指定了键名参数的数量
(3)key:redis中各种数据结构的替代符号
(4)script:自定义参数
说明:EVAL 和 EVALSHA 命令是从 Redis 2.6.0 版本开始的,使用内置的 Lua 解释器,可以对 Lua 脚本进行求值。
EVAL的第一个参数是一段 Lua 5.1 脚本程序。 这段Lua脚本不需要(也不应该)定义函数。它运行在 Redis 服务器中。
EVAL的第二个参数是参数的个数,后面的参数(从第三个参数),表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
在命令的最后,那些不是键名参数的附加参数 arg [arg …] ,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。
02 、上面这几段长长的说明可以用一个简单的例子来概括
- 127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 name age Tinywan 24
- 1) "name"
- 2) "age"
- 3) "Tinywan"
- 4) "24"
说明:第一个参数的字符串就是script,也就是lua脚本。2表示keys的个数,KEYS[1] 就是 name 的占位符,KEYS[2] 就是 age 的占位符,ARGV[1] 就是 Tinywan 的占位符,ARGV[2] 就是 24 的占位符,以此类推。所以最后的结果应该就是:{ return name age Tinywan 24}
03 、redis.call() 和 redis.pcall() 两个函数的参数可以是任意的 Redis 命令
- 127.0.0.1:6379> EVAL "return redis.call('SET','Name','Tinywan')" 0
- OK
- 127.0.0.1:6379> get Name
- "Tinywan"
说明:可以看到上面传递的key的个数是为0
03、使用KEYS和ARGV
- 127.0.0.1:6379> EVAL "return redis.call('SET',KEYS[2],ARGV[3])" 3 name01 name02 name03 Tinywan01 Tinywan02 Tinywan03
- OK
- 127.0.0.1:6379> keys *
- 1) "name02"
- 127.0.0.1:6379> get name02
- "Tinywan03"
说明:返回结果是Redis multi bulk replies的Lua数组,这是一个Redis的返回类型,您的客户端库可能会将他们转换成数组类型。
03 、redis.call() 和 redis.pcall() 的区别
redis.call() 执行一个不存在的Redis命令: SETNGX
- 127.0.0.1:6379> EVAL "redis.call('SETNGX',KEYS[1],ARGV[1]);redis.call('SET',KEYS[3],ARGV[1])" 3 name01 name02 name03 Tinywan01 Tinywan02 Tinywan03
- (error) ERR Error running script (call to f_0adfcdb3f740b2aabfe19f0e80de7cda7ce6262f): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script
- 127.0.0.1:6379> keys *
- (empty list or set)
- 127.0.0.1:6379>
说明:当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因。由于第一个执行错误,导致后面的也没有执行,设置不成功。
redis.pcall() 执行一个不存在的Redis命令: SETNGX
- 127.0.0.1:6379> EVAL "redis.pcall('SETNGX',KEYS[1],ARGV[1]);redis.call('SET',KEYS[3],ARGV[1])" 3 name01 name02 name03 Tinywan01 Tinywan02 Tinywan03
- (nil)
- 127.0.0.1:6379> keys *
- 1) "name03"
- 127.0.0.1:6379> get name03
- "Tinywan01"
- 127.0.0.1:6379>
说明: redis.pcall() 出错时并不引发(raise)错误,而是返回一个 nil,后面的命令任然可以执行成功。
redis.call() 与 redis.pcall()很类似, 他们唯一的区别是当redis命令执行结果返回错误时, redis.call()将返回给调用者一个错误,而redis.pcall()会将捕获的错误以Lua表的形式返回。
redis.call() 和 redis.pcall() 两个函数的参数可以是任意的 Redis 命令
2、Redis中Lua脚本命令介绍
01、SCRIPT 命令
命令用于将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本。
- 127.0.0.1:6379> SCRIPT LOAD "return redis.call('set',KEYS[1],ARGV[1])"
- "c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
02、EVALSHA 命令
根据给定的 SHA1 校验码(也就是 SCRIPT LOAD 执行脚本生成的哈希值),对缓存在服务器中的脚本进行求值。 将脚本缓存到服务器的操作可以通过 SCRIPT LOAD 命令进行。
- 127.0.0.1:6379> EVALSHA c686f316aaf1eb01d5a4de1b0b63cd233010e63d 2 Github Blog github.tinywan blog.tinywan
- OK
- 127.0.0.1:6379> keys *
- 1) "Github"
- 2) "name03"
- 127.0.0.1:6379> get Github
- "github.tinywan"
03、SCRIPT FLUSH 命令
清空Lua脚本缓存 Flush the Lua scripts cache.
- 127.0.0.1:6379> SCRIPT FLUSH
- OK
- 127.0.0.1:6379> EVALSHA c686f316aaf1eb01d5a4de1b0b63cd233010e63d 2 Github Blog github.tinywan blog.tinywan
- (error) NOSCRIPT No matching script. Please use EVAL.
- 127.0.0.1:6379> SCRIPT LOAD "return redis.call('set',KEYS[1],ARGV[1])"
- "c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
- 127.0.0.1:6379> EVALSHA c686f316aaf1eb01d5a4de1b0b63cd233010e63d 2 Github Blog github.tinywan blog.tinywan
- OK
- 127.0.0.1:6379>
04、SCRIPT EXISTS
命令用于校验指定的脚本是否已经被保存在缓存当中
- 127.0.0.1:6379> SCRIPT EXISTS c686f316aaf1eb01d5a4de1b0b63cd233010e63d
- 1) (integer) 1
- 127.0.0.1:6379> SCRIPT FLUSH
- OK
- 127.0.0.1:6379> SCRIPT EXISTS c686f316aaf1eb01d5a4de1b0b63cd233010e63d
- 1) (integer) 0
- 127.0.0.1:6379>
05、SCRIPT KILL
杀死当前正在运行的 Lua 脚本
3、调试
script.lua脚本
- local foo = redis.call("ping")
- return foo
执行脚本
- $ redis-cli --eval script.lua
- PONG
loop.lua脚本
- local i = 0
- while true do
- i = i + 1
- redis.debug(i)
- end
- return "OK"
进入调试模式
- $ redis-cli --ldb --eval loop.lua set set , wet set
- Lua debugging session started, please use:
- quit -- End the session.
- restart -- Restart the script in debug mode again.
- help -- Show Lua script debugging commands.
- * Stopped at 1, stop reason = step over
- -> 1 local i = 0
- ^C
打开另外一个shell窗口
- www@iZ2zec3dge6rwz2uw4tveuZ:~$ redis-cli
- 127.0.0.1:6379> keys *
- (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
- 127.0.0.1:6379> keys *
- (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
- 127.0.0.1:6379> keys *
- (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
- 127.0.0.1:6379> SCRIPT KILL
- OK
- 127.0.0.1:6379> keys *
- 1) "REDIS_CACHE:RESTY_VOD_DETAIL:55"
4、案例介绍
1、获取指定的key的List中的所有数据
list.lua脚本
- local key = KEYS[1]
- -- 返回列表 key 中指所有的元素
- local list = redis.call("lrange", key, 0, -1)
- return list
向队列中插入测试数据
- 127.0.0.1:6379> keys *
- (empty list or set)
- 127.0.0.1:6379> LPUSH book Linux Java C Golang PHP Python
- (integer) 6
- 127.0.0.1:6379> keys *
- 1) "book"
- 127.0.0.1:6379> LRANGE book 0 -1
- 1) "Python"
- 2) "PHP"
- 3) "Golang"
- 4) "C"
- 5) "Java"
- 6) "Linux"
- 127.0.0.1:6379>
执行Lua脚本,获取列表数据
- λ redis-cli.exe --eval .\list.lua book
- 1) "Python"
- 2) "PHP"
- 3) "Golang"
- 4) "C"
- 5) "Java"
- 6) "Linux"
2、集合去重
member.lua脚本
- -- 接受key
- local key = KEYS[1]
- -- 接受所有参数
- local args = ARGV
- local i = 0
- -- 初始化表 result
- local result = {}
- for m, n in ipairs(args) do
- local is_repeat = redis.call("sismember", key, n)
- if tonumber(is_repeat) == 1 then
- table.insert(result, 1, n)
- end
- end
- return result
向集合中插入测试数据
- 127.0.0.1:6379> SADD Book HTML CSS SQL JAVA
- (integer) 4
- 127.0.0.1:6379> SMEMBERS Book
- 1) "SQL"
- 2) "JAVA"
- 3) "CSS"
- 4) "HTML"
注意:Windows 环境的Redis是不支持接受多个参数的(Lua脚本中的ARGV接受的参数是一个nil),所以以下调试在Linux环境有效
开始调试脚本(阻塞方式)
HTML 是集合中的元素
- $ redis-cli --ldb-sync-mode --eval member.lua Book , HTML
- Lua debugging session started, please use:
- quit -- End the session.
- restart -- Restart the script in debug mode again.
- help -- Show Lua script debugging commands.
- * Stopped at 2, stop reason = step over
- -> 2 local key = KEYS[1]
- lua debugger> s
- * Stopped at 4, stop reason = step over
- -> 4 local args = ARGV
- lua debugger> s
- * Stopped at 6, stop reason = step over
- -> 6 local i = 0
- lua debugger> s
- * Stopped at 8, stop reason = step over
- -> 8 local result = {}
- lua debugger> s
- * Stopped at 10, stop reason = step over
- -> 10 for m, n in ipairs(args) do
- lua debugger> s
- * Stopped at 11, stop reason = step over
- -> 11 local is_repeat = redis.call("sismember", key, n)
- lua debugger> s
- <redis> sismember Book HTML
- <reply> 1
- * Stopped at 12, stop reason = step over
- -> 12 if tonumber(is_repeat) == 1 then
- lua debugger> s
- * Stopped at 13, stop reason = step over
- -> 13 table.insert(result, 1, n)
- lua debugger> s
- * Stopped at 10, stop reason = step over
- -> 10 for m, n in ipairs(args) do
- lua debugger> s
- * Stopped at 16, stop reason = step over
- -> 16 return result
- lua debugger> s
- 1) "HTML"
- (Lua debugging session ended)
- 127.0.0.1:6379>
说明:最后返回元素为:HTML
Tinywan 不是集合中的元素
- $ redis-cli --ldb-sync-mode --eval member.lua Book , Tinywan
- Lua debugging session started, please use:
- quit -- End the session.
- restart -- Restart the script in debug mode again.
- help -- Show Lua script debugging commands.
- * Stopped at 2, stop reason = step over
- -> 2 local key = KEYS[1]
- lua debugger> s
- * Stopped at 4, stop reason = step over
- -> 4 local args = ARGV
- lua debugger> s
- * Stopped at 6, stop reason = step over
- -> 6 local i = 0
- lua debugger> s
- * Stopped at 8, stop reason = step over
- -> 8 local result = {}
- lua debugger> s
- * Stopped at 10, stop reason = step over
- -> 10 for m, n in ipairs(args) do
- lua debugger> s
- * Stopped at 11, stop reason = step over
- -> 11 local is_repeat = redis.call("sismember", key, n)
- lua debugger> s
- <redis> sismember Book Tinywan
- <reply> 0
- * Stopped at 12, stop reason = step over
- -> 12 if tonumber(is_repeat) == 1 then
- lua debugger> s
- * Stopped at 10, stop reason = step over
- -> 10 for m, n in ipairs(args) do
- lua debugger> s
- * Stopped at 16, stop reason = step over
- -> 16 return result
- lua debugger> s
- (empty list or set)
- (Lua debugging session ended)
- 127.0.0.1:6379>
说明:最后返回元素为:empty
注意: KEYS和ARGV中间的 ',' 两边的空格,不能省略。
非调试模式
- www@iZ2zec3dge6rwz2uw4tveuZ:~/lua$ redis-cli --eval member.lua Book , Tinywan
- (empty list or set)
- www@iZ2zec3dge6rwz2uw4tveuZ:~/lua$ redis-cli --eval member.lua Book , JAVA
- 1) "JAVA"
- www@iZ2zec3dge6rwz2uw4tveuZ:~/lua$ redis-cli --eval member.lua Book , HTML
- 1) "HTML"
- www@iZ2zec3dge6rwz2uw4tveuZ:~/lua$ redis-cli --eval member.lua Book , HTML5
- (empty list or set)
4、访问频率控制
实现一个访问频率控制,某个ip在短时间内频繁访问页面,需要记录并检测出来,就可以通过Lua脚本高效的实现
limit.lua脚本
- local times = redis.call("incr", KEYS[1])
- if times == 1 then
- redis.call("expire", KEYS[1], ARGV[1])
- end
- if times > tonumber(ARGV[2]) then
- return 0
- end
- return 1
5、实现分布式锁
- <?php
- /**.-------------------------------------------------------------------------------------------------------------------
- * | Github: https://github.com/Tinywan
- * | Blog: http://www.cnblogs.com/Tinywan
- * |--------------------------------------------------------------------------------------------------------------------
- * | Author: Tinywan(ShaoBo Wan)
- * | DateTime: 2018/9/13 22:28
- * | Mail: 756684177@qq.com
- * | Desc: 使用Redis实现分布式锁
- * '------------------------------------------------------------------------------------------------------------------*/
- class RedisLock
- {
- /**
- * 获取锁
- * @param string $lock_name 锁名
- * @param int $acquire_time 重复请求次数
- * @param int $lock_timeout 请求超时时间
- * @return bool|string
- */
- public static function acquireLock($lock_name, $acquire_time = 3, $lock_timeout = 120)
- {
- $identifier = md5($_SERVER['REQUEST_TIME'] . mt_rand(1, 10000000));
- $lock_name = 'LOCK:' . $lock_name;
- $lock_timeout = intval(ceil($lock_timeout));
- $end_time = time() + $acquire_time;
- while (time() < $end_time) {
- $script = <<<luascript
- local result = redis.call('setnx',KEYS[1],ARGV[1]);
- if result == 1 then
- redis.call('expire',KEYS[1],ARGV[2])
- return 1
- elseif redis.call('ttl',KEYS[1]) == -1 then
- redis.call('expire',KEYS[1],ARGV[2])
- return 0
- end
- return 0
- luascript;
- $result = location_redis()->evaluate($script, array($lock_name, $identifier, $lock_timeout), 1);
- if ($result == '1') {
- return $identifier;
- }
- usleep(100000); // 函数延迟代码执行若干微秒
- }
- return false;
- }
- /**
- * 释放锁
- * @param string $lock_name 锁名
- * @param string $identifier 获取锁返回的标识
- * @return bool
- */
- public static function releaseLock($lock_name, $identifier)
- {
- $lock_name = 'LOCK:' . $lock_name;
- while (true) {
- $script = <<<luascript
- local result = redis.call('get',KEYS[1]);
- if result == ARGV[1] then
- if redis.call('del',KEYS[1]) == 1 then
- return 1;
- end
- end
- return 0
- luascript;
- $result = location_redis()->evaluate($script, array($lock_name, $identifier), 1);
- if ($result == 1) {
- return true;
- }
- break;
- }
- //进程已经失去了锁
- return false;
- }
- }
测试脚本
- private function redisLua1()
- {
- for ($i = 0; $i < 100000; $i++) {
- $order = rand(1, 3);
- $order_no = 'order_' . $order;
- Log::debug('[异步脚本] 开始...' . $order_no);
- $orderLock = RedisLock::acquireLock($order_no);
- if (!$orderLock) {
- Log::debug('[异步脚本] 获取锁失败 ' . $order_no);
- continue;
- } else {
- Log::debug('[异步脚本] 获取锁成功 ' . $order_no);
- }
- // 处理业务逻辑
- Log::debug('[异步脚本] 处理业务 ' . $order_no);
- sleep(2);
- // 释放锁
- $orderUnLock = RedisLock::releaseLock($order_no, $orderLock); // 7f62708bb826c034850783efdba127b3
- if (!$orderUnLock) {
- Log::debug('[异步脚本] 释放锁失败 ' . $order_no);
- } else {
- Log::debug('[异步脚本] 释放锁成功 ' . $order_no . PHP_EOL);
- }
- }
- }
-
- private function redisLua2()
- {
- for ($i = 0; $i < 100000; $i++) {
- $order = rand(1, 3);
- $order_no = 'order_' . $order;
- Log::debug('[命令行] 开始...' . $order_no);
- $orderLock = RedisLock::acquireLock($order_no);
- if (!$orderLock) {
- Log::debug('[命令行] 获取锁失败 ' . $order_no);
- continue;
- } else {
- Log::debug('[命令行] 获取锁成功 ' . $order_no);
- }
- // 处理业务逻辑
- Log::debug('[命令行] 处理业务 ' . $order_no);
- sleep(2);
- // 释放锁
- $orderUnLock = RedisLock::releaseLock($order_no, $orderLock);
- if (!$orderUnLock) {
- Log::debug('[命令行] 释放锁失败 ' . $order_no);
- } else {
- Log::debug('[命令行] 释放锁成功 ' . $order_no . PHP_EOL);
- }
- }
- }
开启连个任务跑
模拟异步通知
- php think crontab redisLua1
模拟主动查询
- php think crontab redisLua2
测试结果
5、大数据
Redis大批量增加数据
command.txt
- SET Key0 Value0
- SET Key1 Value1
- SET Key2 Value2
- SET Key3 Value3
- SET Key4 Value4
- SET Key5 Value5
- SET Key6 Value6
- ...
- ...
- SET Key100000 Value100000
批量插入
- $ cat command.txt | redis-cli -h 127.0.0.1 -p 6379 -n 0 --pipe
- All data transferred. Waiting for the last reply...
- Last reply received from server.
- errors: 0, replies: 11
查看插入的数据
- 127.0.0.1:6379> keys Ke*
- 1) "Key2"
- 2) "Key7"
- 3) "Key10"
- 4) "Key9"
- 5) "Key3"
- 6) "Key1"
- 7) "Key8"
- 8) "Key6"
- 9) "Key5"
- 10) "Key4"
...
官方教程