redis4.0之Lua脚本新姿势

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 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

目录
相关文章
|
4月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
1108 7
|
12月前
|
缓存 NoSQL Redis
Redis 脚本
10月更文挑战第18天
143 3
|
8月前
|
缓存 NoSQL 搜索推荐
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。
426 6
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
|
7月前
|
缓存 NoSQL 测试技术
Redis压测脚本及持久化机制
Redis压测脚本及持久化机制简介: Redis性能压测通过`redis-benchmark`工具进行,可评估读写性能。持久化机制包括无持久化、RDB(定期快照)和AOF(操作日志),以及两者的结合。RDB适合快速备份与恢复,但可能丢失数据;AOF更安全,记录每次写操作,适合高数据安全性需求。两者结合能兼顾性能与安全性,建议同时开启并定期备份RDB文件以确保数据安全。
147 9
|
8月前
|
NoSQL Redis 数据库
Redis 功能扩展 Lua 脚本 对Redis扩展 eval redis.call redis.pcall
通过本文的介绍,我们详细讲解了 Lua 脚本在 Redis 中的作用、`eval` 命令的使用方法以及 `redis.call` 和 `redis.pcall` 的区别和用法。通过合理使用 Lua 脚本,可以实现复杂的业务逻辑,确保操作的原子性,并减少网络开销,从而提高系统的性能和可靠性。
284 13
|
10月前
|
监控 安全
公司用什么软件监控电脑:Lua 脚本在监控软件扩展功能的应用
在企业环境中,电脑监控软件对保障信息安全、提升效率至关重要。Lua 脚本在此类软件中用于扩展功能,如收集系统信息、监控软件使用时长及文件操作,向指定服务器发送数据,支持企业管理和运营。
160 6
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
189 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
缓存 NoSQL Redis
Redis命令——脚本
Redis 脚本使用 Lua 解释器来执行脚本。 Reids 2.6 版本通过内嵌支持 Lua 环境。执行脚本的常用命令为 EVAL。
1451 0
|
缓存 NoSQL Redis
redis必杀命令:脚本
题记: Redis 脚本使用 Lua 解释器来执行脚本。 Reids 2.6 版本通过内嵌支持 Lua 环境。执行脚本的常用命令为 EVAL。
1320 0
|
5月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?

相关产品

  • 云数据库 Tair(兼容 Redis)