Redis - Lua 脚本

Redis 从 2.6 开始内嵌了 Lua 环境来支持用户扩展功能. 经过 Lua 脚本, 咱们能够原子化地执行多条 Redis 命令.redis

Redis 中的 Lua 脚本


在 Redis 中执行 Lua 脚本须要用到 EVALEVALSHASCRIPT *** 这几个命令, 下面分别来介绍一下:shell

  1. 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 开始的.数组

  2. 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"
    复制代码

    EVALSHAEVAL 的参数差很少. 只是把脚本改为了缓存中脚本的 sha1 值, 其他没有区别.bash

  3. SCRIPT LOAD: 将脚本缓存到服务器中.服务器

  4. SCRIPT FLUSH: 清空服务器中的全部脚本并发

  5. SCRIPT EXISTS: 判断脚本是否存在于服务器中app

  6. SCRIPT KILL: 中止当前正在执行的脚本dom

在 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 脚本执行吗? 怎么又能够了呢?

从 Redis 3.2 开始(5.0 之后是默认开启), 提供了另一种持久化和主从复制的方案能够容许随机写入. 相较于前一种直接复制 Lua 脚本并从新执行脚本这一方案, 第二种方案不复制 Lua 脚本, 而且脚本只会运行一次, 运行完后对数据库产生的数据变化会生成 Redis 命令用于持久化和主从同步. 因为 Lua 脚本只会执行一次, 因此就不存在以前执行屡次形成的随机性不一致现象, 天然容许随机行操做了.

  1. 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()

  2. 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_REPLICA
  • redis.REPL_NONE: 都不开启 通常 redis.set_repl(***) 不多用到, 由于这会形成重启先后和主从库之间数据不一致. 保留默认的 redis.REPL_ALL 就能够了.

参考

相关文章
相关标签/搜索