redis4.0之Lua脚本新姿势

本文涉及的产品
云原生多模数据库 Lindorm,多引擎 多规格 0-4节点
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
简介: 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
目录
相关文章
|
27天前
|
缓存 NoSQL Redis
Redis 脚本
10月更文挑战第18天
31 3
|
3月前
|
NoSQL Redis
Redis 执行 Lua保证原子性原理
Redis 执行 Lua 保证原子性原理
336 1
|
1月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
59 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
1月前
|
缓存 分布式计算 NoSQL
大数据-43 Redis 功能扩展 Lua 脚本 对Redis扩展 eval redis.call redis.pcall
大数据-43 Redis 功能扩展 Lua 脚本 对Redis扩展 eval redis.call redis.pcall
29 2
|
1月前
|
NoSQL Java 关系型数据库
阿里 P7二面:Redis 执行 Lua,到底能不能保证原子性?
Redis 和 Lua,两个看似风流马不相及的技术点,为何能产生“爱”的火花,成为工作开发中的黄金搭档?技术面试中更是高频出现,Redis 执行 Lua 到底能不能保证原子性?今天就来聊一聊。 
85 1
|
2月前
|
存储 JSON Ubuntu
如何使用 Lua 脚本进行更复杂的网络请求,比如 POST 请求?
如何使用 Lua 脚本进行更复杂的网络请求,比如 POST 请求?
|
3月前
|
缓存 监控 NoSQL
【Azure Redis 缓存】使用Python代码获取Azure Redis的监控指标值 (含Powershell脚本方式)
【Azure Redis 缓存】使用Python代码获取Azure Redis的监控指标值 (含Powershell脚本方式)
|
3月前
|
NoSQL Shell Redis
redis一键巡检脚本分享
redis一键巡检脚本分享
43 0
|
6月前
|
存储 NoSQL Redis
Redis的Lua脚本有什么作用?
Redis Lua脚本用于减少网络开销、实现原子操作及扩展指令集。它能合并操作降低网络延迟,保证原子性,替代不支持回滚的事务。通过脚本,代码复用率提高,且可自定义指令,如实现分布式锁,增强Redis功能和灵活性。
250 1
|
5月前
|
消息中间件 NoSQL Java
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
227 0

相关产品

  • 云数据库 Tair(兼容 Redis)