「每天一道面试题」 Redis脚本(redis经典面试题)
mhr18 2024-11-10 09:50 15 浏览 0 评论
Redis脚本
Redis为什么引入Lua
Redis 是高性能的 key-value 内存数据库,在部分场景下,是对关系数据库的良好补充。Redis 提供了非常丰富的指令集,官网上提供了 200 多个命令。但是某些特定领域,需要扩充若干指令原子性执行时,仅使用原生命令便无法完成。Redis 意识到上述问题后,在 2.6 版本推出了 lua 脚本功能,允许开发者使用 Lua 语言编写脚本传到 Redis 中执行。
用户可以向 Redis 服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。
使用Lua脚本好处
减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延。
原子操作:Redis 会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。
复用:客户端发送的脚本会永久存在 redis 中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。
可嵌入性:可嵌入 JAVA,C# 等多种编程语言,支持不同操作系统跨平台交互。
什么是Lua
Lua 是一种轻量小巧的脚本语言,用标准 C 语言 编写并以源代码形式开放。其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。因为广泛的应用于:游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件等。
比如:Lua 脚本用在很多游戏上,主要是 Lua 脚本可以嵌入到其他程序中运行,游戏升级的时候,可以直接升级脚本,而不用重新安装游戏。
Redis Lua相关命令
命令 | 描述 |
EVAL | 执行 Lua 脚本 |
EVALSHA | 执行 Lua 脚本 |
SCRIPT EXISTS | 查看指定的脚本是否已经被保存在缓存当中 |
SCRIPT FLUSH | 从脚本缓存中移除所有脚本 |
SCRIPT KILL | 杀死当前正在运行的 Lua 脚本 |
SCRIPT LOAD | 将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本 |
EVAL命令
语法
EVAL script numkeys key [key …] arg [arg …]
参数
命令 | 描述 |
script | 参数是一段 Lua5.1 脚本程序。脚本不必(也不应该 1 )定义为一个 Lua 函数 |
numkeys | 指定后续参数有几个 key,即:key [key …] 中 key 的个数。如没有 key,则为 0 |
key [key …] | 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在 Lua 脚本中通过 KEYS[1], KEYS[2] 获取 |
arg [arg …] | 附加参数。在 Lua 脚本中通过 ARGV[1], ARGV[2] 获取。 |
案例
案例一:
# 例1:numkeys=1,keys数组只有1个元素key1,arg数组无元素
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"
案例二:
# 例2:numkeys=0,keys数组无元素,arg数组元素中有1个元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"
案例三:
# 例3:numkeys=2,keys数组有两个元素key1和key2,arg数组元素中有两个元素first和second
# 其实{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}表示的是Lua语法中“使用默认索引”的table表,
# 相当于java中的map中存放四条数据。Key分别为:1、2、3、4,而对应的value才是:KEYS[1]、KEYS[2]、ARGV[1]、ARGV[2]
# 举此例子仅为说明eval命令中参数的如何使用。项目中编写Lua脚本最好遵从key、arg的规范。
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
案例四:
# 例4:使用了redis为lua内置的redis.call函数
# 脚本内容为:先执行SET命令,在执行EXPIRE命令
# numkeys=1,keys数组有一个元素userAge(代表redis的key)
# arg数组元素中有两个元素:10(代表userAge对应的value)和60(代表redis的存活时间)
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 44
通过上面的例 4,我们可以发现,脚本中使用 redis.call() 去调用 redis 的命令。在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是: redis.call() 和 redis.pcall()。
这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误,差别为,当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:
127.0.0.1:6379> lpush foo a
(integer) 1
127.0.0.1:6379> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value
和 redis.call() 不同, redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:
127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value
SCRIPT LOAD命令和EVALSHA命令
SCRIPT LOAD命令语法
SCRIPT LOAD script
SCRIPT EVALSHA命令语法
EVALSHA sha1 numkeys key [key …] arg [arg …]
这两个命令放在一起讲的原因是:EVALSHA 命令中的 sha1 参数,就是 SCRIPT LOAD 命令执行的结果。SCRIPT LOAD 将脚本 script 添加到 Redis 服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。
在脚本被加入到缓存之后,在任何客户端通过 EVALSHA 命令,可以使用脚本的 SHA1 校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行 SCRIPT FLUSH 为止。
案例
## SCRIPT LOAD加载脚本,并得到sha1值
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"
## EVALSHA使用sha1值,并拼装和EVAL类似的numkeys和key数组、arg数组,调用脚本。
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 43
SCRIPT EXISTS命令
语法
SCRIPT EXISTS sha1 [sha1 …]
说明
给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中。
案例
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe346
1) (integer) 0
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345 6aeea4b3e96171ef835a78178fceadf1a5dbe366
1) (integer) 1
2) (integer) 0
SCRIPT FLUSH命令
语法
SCRIPT FLUSH
说明
清除 Redis 服务端所有 Lua 脚本缓存。
案例
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT FLUSH
OK
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 0
SCRIPT KILL命令
语法
SCRIPT KILL
说明
杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。
假如当前正在运行的脚本已经执行过写操作,那么即使执行 SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用 SHUTDOWN NOSAVE 命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。
Redis执行Lua文件
编写Lua脚本文件
local key = KEYS[1]
local val = redis.call("GET", key);
if val == ARGV[1]
then
redis.call('SET', KEYS[1], ARGV[2])
return 1
else
return 0
end
执行Lua脚本文件
# 执行命令: redis-cli -a 密码 --eval Lua脚本路径 key [key …] , arg [arg …]
如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi
"--eval" 而不是命令模式中的 "eval",一定要有前端的两个 -,脚本路径后紧跟 key [key …],相比命令行模式,少了 numkeys 这个 key 数量值。
key [key …] 和 arg [arg …] 之间的 “ , ”,英文逗号前后必须有空格,否则死活都报错。
## Redis客户端执行
127.0.0.1:6379> set userName zhangsan
OK
127.0.0.1:6379> get userName
"zhangsan"
## linux服务器执行
## 第一次执行:compareAndSet成功,返回1
## 第二次执行:compareAndSet失败,返回0
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 0
脚本超时
Redis 的配置文件中提供了如下配置项来规定最大执行时长:
# Lua脚本最大执行时间,默认5秒
Lua-time-limit 5000
但这里有个坑,当一个脚本达到最大执行时长的时候,Redis 并不会强制停止脚本的运行,仅仅在日志里打印个警告,告知有脚本超时。为什么不能直接停掉呢?
因为 Redis 必须保证脚本执行的原子性,中途停止可能导致内存的数据集上只修改了部分数据。如果时长达到 Lua-time-limit 规定的最大执行时间,Redis 只会做这几件事情:
- 日志记录有脚本运行超时。
- 开始允许接受其他客户端请求,但仅限于 SCRIPT KILL 和 SHUTDOWN NOSAVE 两个命令,其他请求仍返回 busy 错误。
脚本死循环怎么办
Redis 的指令执行是个单线程,这个单线程还要执行来自客户端的 lua 脚本。如果 lua 脚本中来一个死循环,是不是 Redis 就完蛋了?Redis 为了解决这个问题,它提供了 script kill 指令用于动态杀死一个执行时间超时的 lua 脚本。
不过 script kill 的执行有一个重要的前提,那就是当前正在执行的脚本没有对 Redis 的内部数据状态进行修改,因为 Redis 不允许 script kill 破坏脚本执行的原子性。比如脚本内部使用了 redis.call("set", key, value) 修改了内部的数据,那么 script kill 执行时服务器会返回错误。
Script Kill的原理
lua 脚本引擎功能太强大了,它提供了各式各样的钩子函数,它允许在内部虚拟机执行指令时运行钩子代码。比如每执行 N 条指令执行一次某个钩子函数,Redis 正是使用了这个钩子函数。
脚本的安全性
如生成随机数这一命令,如果在 master 上执行完后,再在 slave 上执行会不一样,这就破坏了主从节点的一致性。为了解决这个问题, Redis 对 Lua 环境所能执行的脚本做了一个严格的限制,所有脚本都必须是无副作用的纯函数(pure function)。所有刚才说的那种情况压根不存在。Redis 对 Lua 环境做了一些列相应的措施:
- 不提供访问系统状态状态的库(比如系统时间库)。
- 禁止使用 loadfile 函数。
- 如果脚本在执行带有随机性质的命令(比如 RANDOMKEY ),或者带有副作用的命令(比如 TIME )之后,试图执行一个写入命令(比如 SET ),那么 Redis 将阻止这个脚本继续运行,并返回一个错误。
- 如果脚本执行了带有随机性质的读命令(比如 SMEMBERS ),那么在脚本的输出返回给 Redis 之前,会先被执行一个自动的字典序排序,从而确保输出结果是有序的。
- 用 Redis 自己定义的随机生成函数,替换 Lua 环境中 math 表原有的 math.random 函数和 math.randomseed 函数,新的函数具有这样的性质:每次执行 Lua 脚本时,除非显式地调用 math.randomseed ,否则 math.random 生成的伪随机数序列总是相同的。
Redis脚本应用举例
在我们项目中,有一个两对玩家进行 PK 的功能,这时候,我们需要存储每个玩家的 Id 与当前 PK 状态的映射关系,因为每组玩家都有好几个,同时,我们还要存储 PK 房间与 PK 的详细信息的映射,这些信息都是存储在 Redis 里面的,并且都需要是原子操作,因此,我们使用了 Lua 脚本来实现的。
原文参考:嗨客网(www.haicoder.net)Redis脚本
相关推荐
- Docker安装详细步骤及相关环境安装配置
-
最近自己在虚拟机上搭建一个docker,将项目运行在虚拟机中。需要提前准备的工具,FinallShell(远程链接工具),VM(虚拟机-配置网络)、CentOS7(Linux操作系统-在虚拟机上安装)...
- Linux下安装常用软件都有哪些?做了一个汇总列表,你看还缺啥?
-
1.安装列表MySQL5.7.11Java1.8ApacheMaven3.6+tomcat8.5gitRedisNginxpythondocker2.安装mysql1.拷贝mysql安装文件到...
- Nginx安装和使用指南详细讲解(nginx1.20安装)
-
Nginx安装和使用指南安装1.检查并安装所需的依赖软件1).gcc:nginx编译依赖gcc环境安装命令:yuminstallgcc-c++2).pcre:(PerlCompatibleRe...
- docker之安装部署Harbor(docker安装hacs)
-
在现代软件开发和部署环境中,Harbor作为一个企业级的容器镜像仓库,提供了高效、安全的镜像管理解决方案。通过Docker部署Harbor,可以轻松构建私有镜像仓库,满足企业对镜像存储、管理和安全性...
- 成功安装 Magento2.4.3最新版教程「技术干货」
-
外贸独立站设计公司xingbell.com经过多次的反复实验,最新版的magento2.4.3在oneinstack的环境下的详细安装教程如下:一.vps系统:LinuxCentOS7.7.19...
- 【Linux】——从0到1的学习,让你熟练掌握,带你玩转Linu
-
学习Linux并掌握Java环境配置及SpringBoot项目部署是一个系统化的过程,以下是从零开始的详细指南,帮助你逐步掌握这些技能。一、Linux基础入门1.安装Linux系统选择发行版:推荐...
- cent6.5安装gitlab-ce最新版本-11.8.2并配置邮件服务
-
cent6.5安装gitlab-ce最新版本-11.8.2并配置邮件服务(yum选择的,时间不同,版本不同)如果对运维课程感兴趣,可以在b站上搜索我的账号:运维实战课程,可以关注我,学习更多免费的运...
- 时隔三月,参加2020秋招散招,终拿字节跳动后端开发意向书.
-
3个月前头条正式批笔试4道编程题只AC了2道,然后被刷了做了200多道还是太菜了,本来对字节不抱太大希望,毕竟后台竞争太大,而且字节招客户端开发比较多。后来看到有散招免笔试,抱着试一试的心态投了,然而...
- Redisson:Java程序员手中的“魔法锁”
-
Redisson:Java程序员手中的“魔法锁”在这个万物互联的时代,分布式系统已经成为主流。然而,随着系统的扩展,共享资源的争夺成为了一个棘手的问题。就比如你想在淘宝“秒杀”一款商品,却发现抢的人太...
- 【线上故障复盘】RPC 线程池被打满,1024个线程居然不够用?
-
1.故障背景昨天晚上,我刚到家里打开公司群,就看见群里有人讨论:线上环境出现大量RPC请求报错,异常原因:被线程池拒绝。虽然异常量很大,但是异常服务非核心服务,属于系统旁路,服务于数据核对任务,即使...
- 小红书取消大小周,有人不高兴了!
-
小红书宣布五一节假日之后,取消大小周,恢复为正常的双休,乍一看工作时长变少,按道理来说大家应该都会很开心,毕竟上班时间缩短了,但是还是有一些小红书的朋友高兴不起来,心情很复杂。因为没有了大小周,以前...
- 延迟任务的多种实现方案(延迟机制)
-
场景订单超时自动取消:延迟任务典型的使用场景是订单超时自动取消。功能精确的时间控制:延时任务的时间控制要尽量准确。可靠性:延时任务的处理要是可靠的,确保所有任务最终都能被执行。这通常要求延时任务的方案...
- 百度java面试真题(java面试题下载)
-
1、SpingBoot也有定时任务?是什么注解?在SpringBoot中使用定时任务主要有两种不同的方式,一个就是使用Spring中的@Scheduled注解,另一个则是使用第三方框架Q...
- 回归基础:访问 Kubernetes Pod(concurrent.futures访问数据库)
-
Kubernetes是一头巨大的野兽。在它开始有用之前,您需要了解许多概念。在这里,学习几种访问集群外pod的方法。Kubernetes是一头巨大的野兽。在它开始有用之前,您需要了解许多不同的...
- Spring 缓存神器 @Cacheable:3 分钟学会优化高频数据访问
-
在互联网应用中,高频数据查询(如商品详情、用户信息)往往成为性能瓶颈。每次请求都触发数据库查询,不仅增加服务器压力,还会导致响应延迟。Spring框架提供的@Cacheable注解,就像给方法加了一...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- Docker安装详细步骤及相关环境安装配置
- Linux下安装常用软件都有哪些?做了一个汇总列表,你看还缺啥?
- Nginx安装和使用指南详细讲解(nginx1.20安装)
- docker之安装部署Harbor(docker安装hacs)
- 成功安装 Magento2.4.3最新版教程「技术干货」
- 【Linux】——从0到1的学习,让你熟练掌握,带你玩转Linu
- cent6.5安装gitlab-ce最新版本-11.8.2并配置邮件服务
- 时隔三月,参加2020秋招散招,终拿字节跳动后端开发意向书.
- Redisson:Java程序员手中的“魔法锁”
- 【线上故障复盘】RPC 线程池被打满,1024个线程居然不够用?
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)