天天看点

redis4.0之Lua脚本新姿势前言Redis中的Lua脚本后记

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

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

在redis中使用lua脚本不可避免的要用到以下三个命令:eval、evalsha和script,下面我们来简单介绍一下:

eval script numkeys key [key ...] arg [arg ...]

evalsha sha1 numkeys key [key ...] arg [arg ...]

script subcommand

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

把这段lua脚本持久化到aof文件中,保证redis重启时可以回放执行过的lua脚本。

把这段lua脚本复制给备库执行,保证主备库的数据一致性。

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

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

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

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

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

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

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

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

ok,从新版本开始,redis提供了redis.replicate_commands()

函数来实现这一功能,把发生数据变更的命令以事务的方式做持久化和主从复制,从而允许在lua脚本内进行随机写入,下面来举例说明:

可以看到,相同的脚本只是在开头插入了redis.replicate_commands()就可以成功把时间写入;这是因为执行了redis.replicate_commands()之后,redis就开始使用multi/exec来包围lua脚本中调用的命令,持久化和复制的只是<code>*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</code>而不是整个lua脚本,那么aof文件和备库中拿到的就是一个确定的结果。

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

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

在写命令之前调用redis.replicate_commands()

当有大流量写入时不建议用redis.replicate_commands()

慎用redis.set_repl()

继续阅读