redis4.0之Lua脚本新姿势

本文涉及的产品
云原生多模数据库 Lindorm,多引擎 多规格 0-4节点
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 MongoDB,通用型 2核4GB
简介: Redis内嵌了Lua环境来支持用户扩展功能,但是出于数据一致性考虑,要求脚本必须是纯函数的形式,对于随机性的写入Redis是拒绝的。从Redis 3.2开始Lua脚本支持随机性写入,最近在总结4.0的新特性,索性就都归到4.0里,方便查阅。

前言

Redis内嵌了Lua环境来支持用户扩展功能,但是出于数据一致性考虑,要求脚本必须是纯函数的形式,也就是说对于一段Lua脚本给定相同的参数,写入Redis的数据也必须是相同的,对于随机性的写入Redis是拒绝的。

从Redis 3.2开始Lua脚本支持随机性写入,最近在总结4.0的新特性,索性就都归到4.0里,方便查阅。

Redis中的Lua脚本

1. Lua脚本简介

在Redis中使用Lua脚本不可避免的要用到以下三个命令:EVAL、EVALSHA和SCRIPT,下面我们来简单介绍一下:

    1. EVAL script numkeys key [key ...] arg [arg ...]
    script参数就是一段Lua脚本

    numkeys参数用于指明后面key的数量

    key键名,在Lua脚本中可以通过全局变量KEYS[]数组访问,其数量已经由numkeys指明

    arg为辅助参数,在Lua脚本中可以通过全局变量ARGV[]数组访问,其个数并没有限制
    1. EVALSHA sha1 numkeys key [key ...] arg [arg ...]
    EVALSHA与EVAL基本一致,只不过其中script脚本替换成了这段脚本的sha1校验码

    sha1校验码通常是取SCRIPT LOAD的返回值
    1. SCRIPT subcommand
    SCRIPT LOAD script: 将一段Lua脚本加载到Redis中缓存起来,并返回其sha1校验码

    SCRIPT EXISTS sha1 [sha1 ...]: 判断给定的一个或多个sha1校验码在Redis中是否有对应的Lua脚本

    SCRIPT FLUSH: 清空所有已加载的Lua脚本

    SCRIPT KILL: 杀死正在运行的Lua脚本,当且仅当这个脚本没有执行过任何写命令时才生效,如果执行过写命令那么就只能等待这个脚本执行完毕,或者执行SHUTDOWN NOSAVE来关闭Redis

关于Lua脚本的用法此处就不详细展开了,具体可以参考官网的文档:https://redis.io/commands/eval

2. Lua脚本的持久化及主从复制

Redis允许在Lua脚本中调用redis.call()或者redis.pcall()来执行Redis命令,如果Lua脚本对Redis的数据做了更改,那么除了执行脚本本身以外还需要两个额外的操作:

  1. 把这段Lua脚本持久化到AOF文件中,保证Redis重启时可以回放执行过的Lua脚本。
  2. 把这段Lua脚本复制给备库执行,保证主备库的数据一致性。

由于上述两步,现在就很容易理解为什么Redis要求Lua脚本必须是纯函数的形式了,想象一下给定一段Lua脚本和输入参数却得到了不同的结果,这就会造成重启前后和主备库之间的数据不一致,Redis不允许对数据一致性的破坏。

3. Redis如何防止随机写入

上一节我们介绍了Lua脚本的持久化及主从复制,很明显随机写入会对数据一致性造成破坏,那么本节就来介绍Redis是如何防止Lua脚本中随机写入的。

首先我们来执行一段脚本,尝试把time命令返回的当前时间写入到键now中,看下会怎样:

127.0.0.1:6379> eval "local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0
(error) ERR Error running script (call to f_e745355f11745192bd45376618a34bec9145653b): @user_script:1: @user_script: 1: Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode.

不出意外的被拒绝了,这是因为在Redis中time命令是一个随机命令(时间是变化的),在Lua脚本中调用了随机命令之后禁止再调用写命令,Redis中一共有10个随机类命令:

spop、srandmember、sscan、zscan、hscan、randomkey、scan、lastsave、pubsub、time

熟悉Lua的读者也许会问,那要是使用math.random()来生成随机数呢?

Redis是允许在Lua脚本中使用随机数发生器的,不过大家也应该知道生成的其实都是伪随机序列,除非显示调用math.randomseed()。通常情况下都会选择系统时间来作为math.randomseed()的参数,然而Redis在初始化Lua环境时出于安全考虑并没有加载os库,所以os.time无法使用,而Redis的time命令属于随机命令就又回到了上面的问题。

  • 这里小插曲下,Redis自己实现了随机数发生器,替换掉了math.randomseed()和math.random(),以保证在不同运行环境下生成的伪随机数序列总是相同的。

4. redis.replicate_commands()

综上所述,Redis无法在Lua脚本中进行随机写入,是因为受到了持久化和主从复制的制约,而制约的根本原因是持久化和复制的粒度是整个Lua脚本,如果能够只把发生更改的数据做持久化和主从复制,那么就可以化随机为确定,进一步丰富Lua在Redis中的使用。

OK,从新版本开始,Redis提供了redis.replicate_commands()
函数来实现这一功能,把发生数据变更的命令以事务的方式做持久化和主从复制,从而允许在Lua脚本内进行随机写入,下面来举例说明:

127.0.0.1:6379> eval "redis.replicate_commands(); local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0
"1504460040"

可以看到,相同的脚本只是在开头插入了redis.replicate_commands()就可以成功把时间写入;这是因为执行了redis.replicate_commands()之后,Redis就开始使用multi/exec来包围Lua脚本中调用的命令,持久化和复制的只是*1\r\n$5\r\nMULTI\r\n*3\r\n$3\r\nset\r\n$3\r\nnow\r\n$10\r\n1504460595\r\n*1\r\n$4\r\nEXEC\r\n而不是整个Lua脚本,那么AOF文件和备库中拿到的就是一个确定的结果。

并且在Lua脚本中读多写少的情况下,只持久化和复制写命令,可以节省重启和备库的CPU时间。

5. redis.replicate_commands()注意事项

replicate_commands虽好但是也不能乱用,有几个事项还是需要注意的:

    1. 在写命令之前调用redis.replicate_commands()
    上一节说道调用redis.replicate_commands()之后Redis开始用事务来替代整个Lua脚本做持久化和主从复制。
    但是Redis并没有缓存redis.replicate_commands()之前的命令,如果在此之前调用了写命令是会破坏数据一致性的(因为Redis不支持undo操作,无法回滚到执行Lua脚本的初始状态)。
    此时redis.replicate_commands()并不会生效。

127.0.0.1:6379> eval "redis.call('set','foo','bar'); redis.replicate_commands(); local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0
(error) ERR Error running script (call to f_7dd09c943ce6841d59c54b1f4618f9cc670c7b74): @user_script:1: @user_script: 1: Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode.

    可以看到在redis.replicate_commands()之前先调用了写命令会造成失败。
    所以建议如果想进行随机写入的话,在脚本一开始就调用redis.replicate_commands()。
    1. 当有大流量写入时不建议用redis.replicate_commands()
    诚然redis.replicate_commands()丰富了Lua的用法,但是不可避免的也会有些副作用。
    比如有时候会放大主备复制的流量:

127.0.0.1:6379> eval "for i=1,10000,1 do redis.call('set',i,i) end" 0

    以上这段脚本会进行1万次的循环写入,在不调用redis.replicate_commands()的情况下只会给备库复制这段脚本,而调用之后就会进行1万次的写命令复制,增加了主从复制的流量。
    所以在不需要进行随机写入时,如果有可能造成大流量写入的话尽量不要使用redis.replicate_commands()。
    1. 慎用redis.set_repl()
    redis.replicate_commands()可以和redis.set_repl()配合,来控制写命令是否进行持久化和主从复制:
    redis.set_repl(redis.REPL_ALL) -- 既持久化也主从复制。
    redis.set_repl(redis.REPL_AOF) -- 只持久化不主从复制。
    redis.set_repl(redis.REPL_SLAVE) -- 只主从复制不持久化。
    redis.set_repl(redis.REPL_NONE) -- 既不持久化也不主从复制。
    默认REPL_ALL,当设置为其他模式时会有数据不一致的风险,所以不建议使用redis.set_repl(),使用redis.replicate_commands()来进行随机写入足矣。

后记

基于4.0的云redis已正式上线,登陆阿里云控制台即可开通:https://www.aliyun.com/product/kvstore

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
4天前
|
弹性计算 NoSQL Shell
redis没设置密码,莫名被设置了4个sh脚本
阿里云ECS实例上未设密码的Redis服务被发现含有未知来源的SH脚本,这些脚本定时从外部URL下载并执行代码。这可能是服务器遭受恶意攻击的迹象。建议立即检查系统日志,确认是否被黑,并移除这些脚本。同时,为Redis设置密码,加强安全防护。若不确定,可寻求专业安全团队帮助。
31 2
|
4天前
|
存储 缓存 NoSQL
深入浅出Redis(十):Redis的Lua脚本
深入浅出Redis(十):Redis的Lua脚本
|
4天前
|
存储 NoSQL 调度
Redis Lua脚本:原子性的真相揭秘
【4月更文挑战第20天】
67 0
Redis Lua脚本:原子性的真相揭秘
|
4天前
|
缓存 NoSQL Java
lua脚本在redis中的使用场景
lua脚本在redis中的使用场景
|
4天前
|
NoSQL Java Redis
lua脚本做redis的锁
这段内容是关于使用Redis实现分布式锁的Java代码示例。`RedisLock`类包含`lock`和`unlock`方法,使用`StringRedisTemplate`和Lua脚本进行操作。代码展示了两种加锁方式:一种带有过期时间,另一种不带。还提到了在加锁和解锁过程中的异常处理,并提供了相关参考资料链接。
20 3
|
4天前
|
存储 NoSQL Redis
Redis的Lua脚本有什么作用?
Redis Lua脚本用于减少网络开销、实现原子操作及扩展指令集。它能合并操作降低网络延迟,保证原子性,替代不支持回滚的事务。通过脚本,代码复用率提高,且可自定义指令,如实现分布式锁,增强Redis功能和灵活性。
47 1
|
4天前
|
存储 NoSQL 关系型数据库
使用lua脚本操作redis
使用lua脚本操作redis
51 0
|
4天前
|
NoSQL Java Redis
Redis进阶-lua脚本
Redis进阶-lua脚本
69 0
|
4天前
|
缓存 NoSQL Java
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性
66 0
|
4天前
|
算法 NoSQL Java
springboot整合redis及lua脚本实现接口限流
springboot整合redis及lua脚本实现接口限流
96 0

相关产品

  • 云数据库 Redis 版