Redis 从 2.6 开始内嵌了 Lua 环境来支持用户扩展功能. 通过 Lua 脚本, 我们可以原子化地执行多条 Redis 命令.
Redis 中的 Lua 脚本
在 Redis 中执行 Lua 脚本需要用到 EVAL 和 EVALSHA 和 SCRIPT *** 这几个命令, 下面分别来介绍一下:
-
EVAL: 执行 Lua 脚本EVAL script numkeys key[key ...] arg [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"- script 就是 Lua 脚本本身
- numkeys 表示脚本中涉及到的 Redis Key 的数量
- key[key ...] 表示脚本中涉及到的所有 Redis Key
- arg [arg ...] 表示脚本中涉及到的所有参数(变量), 不限制个数
在 Lua 脚本中可以通过
KEYS[]数组加上脚标访问具体的 Redis Key, 通过ARGV[]数据加脚标访问传入的参数(变量). 注意, 脚标都是从 1 开始的. -
EVALSHA: 从缓存中执行 Lua 脚本EVAL sha1 numkeys key[key ...] arg [arg ...] 127.0.0.1:6379> SCRIPT LOAD "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" "a42059b356c875f0717db19a51f6aaca9ae659ea" 127.0.0.1:6379> evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"EVALSHA和EVAL的参数差不多. 只是把脚本改成了缓存中脚本的 sha1 值, 其余没有区别. -
SCRIPT LOAD: 将脚本缓存到服务器中. -
SCRIPT FLUSH: 清空服务器中的所有脚本 -
SCRIPT EXISTS: 判断脚本是否存在于服务器中 -
SCRIPT KILL: 停止当前正在执行的脚本
在 Redis 中执行的 Lua 脚本必须是纯函数形式. 也就是说, 给定一段脚本并且传入相同的参数, 写入 Redis 中的数据也必须是一致的. Redis 会拒绝随机性的写入, 因为这会造成数据的不一致性.
Lua 脚本的持久化和主从复制(Redis 5.0 以下)
Redis 允许在 Lua 脚本中通过 redis.call() 和 redis.pcall() 来执行 Redis 命令. 如果 Lua 脚本对 Redis 中的数据进行了更改, 那么除了更新数据库中的数据之外, 还会执行另外两个操作:
- 把这段 Lua 脚本写入到 AOF 文件中, 保证 Redis 在重启时候可以执行该脚本
- 把这段 Lua 脚本复制给从库执行, 保证主从数据一致性
127.0.0.1:6379> eval "redis.call('set', KEYS[1], ARGV[1]); return redis.call('get', KEYS[1])" 1 mykey myvalue
"myvalue"
# 查看 AOF 文件
➜ cat appendonly.aof
*5
$4
eval
$70
redis.call('set', KEYS[1], ARGV[1]); return redis.call('get', KEYS[1])
$1
1
$5
mykey
$7
myvalue
所以, 如果 Redis 接受随机性写入的话, Redis 在重启前后或者在主从库之间就会存在数据不一致的现象, 当然, 这是不被允许的.
Redis 防止随机写入(Redis 5.0 以下)
比如, 我在 Lua 脚本中获取当前时间并将当前时间 SET 到一个 KEY 中, Redis 就会拒绝操作并抛出一个异常; 也就是说, Redis 会拒绝存在随机写入的 Lua 脚本执行.

127.0.0.1:6379> eval "local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0
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 中一共有 10 个随机类命令: SPOP, SRANDMEMBER, SSCAN, ZSCAN, HSCAN, SCAN, RANDOMKEY, LASTSAVE, PUBSUB, TIME.
当一些返回数据是无序的命令, 比如 SMEMBERS 在 Lua 中被调用时, 返回的数据都是进行过排序后返回的, 所以得到的数据顺序都是一致的.
并且 Redis 修改了 Lua 脚本中的随机数生成函数(math.random, math.randomseed)使得新脚本执行的时候, 返回的种子都是一样的. 所以在 Lua 脚本中, 如果未使用 math.randomseed ,仅仅使用 math.random, 生成的随机数序列都是一样的.
Redis 允许随机写入的情况
等下, 不是说为了保证 Redis 重启前后和主从之间的数据一致性, Redis 会拒绝执行执行存在随机写入的 Lua 脚本执行吗? 怎么又可以了呢?

-
5.0 版本之前 Redis 3.2 提供了
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 "1552060128"在 Lua 脚本中, 从调用
redis.replicate_commands()开始到脚本结束, 这一部分脚本所产生的 Redis 命令会被包在一个MULTI / EXEC事务中, 并发给 AOF 或者从库. 当然, 只有对数据库中的数据产生变化的 Redis 命令才会被生成并包装进MULTI / EXEC事务.# AOF 文件 ➜ cat appendonly.aof *1 $5 MULTI *3 $3 set $3 now $10 1552114016 *1 $4 EXEC注意: Redis 只是会将调用
redis.replicate_commands()后面的部分放进事务中. 在其前面的部分如果调用了写操作是会破坏数据的一致性的, 此时,redis.replicate_commands()并不会生效. 见🌰:127.0.0.1:6379> eval "redis.call('set', 'key', 'value') 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_a8c3ce5ccbfc3074b49ea277b7370ded0c2d354b): @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. 127.0.0.1:6379> keys * 1) "now" 2) "key" # AOF 文件 ➜ cat appendonly.aof *3 $4 eval $156 redis.call('set', 'key', 'value') redis.replicate_commands(); local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now') $1 0所以, 如果在 Lua 脚本中需要进行随机写入的话, 建议在脚本的开头就调用
redis.replicate_commands() -
5.0 版本及以后版本默认开启
Redis 对于随机写入的持久化和主从复制的控制
Redis 3.2 还引入了另一个机制: 可以自行决定是否持久化或者进行主从复制, 可以通过 redis.set_repl(***) 设置, 参数可以为:
redis.REPL_ALL: 开启 AOF 持久化和主从复制(默认)redis.REPL_AOF: 仅开启 AOF 持久化redis.REPL_REPLICA: 仅开启主从复制redis.REPL_SLAVE: 同redis.REPL_REPLICAredis.REPL_NONE: 都不开启 一般redis.set_repl(***)很少用到, 因为这会造成重启前后和主从库之间数据不一致. 保留默认的redis.REPL_ALL就可以了.