专栏首页码农小胖哥的码农生涯一网打尽Redis Lua脚本并发原子组合操作

一网打尽Redis Lua脚本并发原子组合操作

1. 前言

Redis 是高性能的 KV 内存数据库,除了做缓存中间件的基本作用外还有很多用途,比如胖哥以前分享的Redis GEO 地理位置信息计算。Redis 提供了丰富的命令来供我们使用以实现一些计算。Redis 的单个命令都是原子性的,有时候我们希望能够组合多个 Redis 命令,并让这个组合也能够原子性的执行,甚至可以重复使用。Redis 开发者意识到这种场景还是很普遍的,就在 2.6 版本中引入了一个特性来解决这个问题,这就是 Redis 执行 Lua 脚本。

2. Lua

Lua 也算一门古老的语言了,玩魔兽世界的玩家应该对它不陌生,WOW 的插件就是用 Lua 脚本编写的。在高并发的网络游戏中 Lua 大放异彩被广泛使用。

Lua 广泛作为其它语言的嵌入脚本,尤其是 C/C++,语法简单,小巧,源码一共才 200 多 K,这可能也是 Redis 官方选择它的原因。

另一款明星软件 Nginx 也支持 Lua,利用 Lua 也可以实现很多有用的功能。

3. Lua 并不难

Redis 官方指南也指出不要在 Lua 脚本中编写过于复杂的逻辑。

为了实现一个功能就要学习一门语言,这看起来就让人有打退堂鼓的感觉。其实 Lua 并不难学,而且作为本文的场景来说我们不需要去学习 Lua 的完全特性,要在 Redis 中轻量级使用 Lua 语言。这对掌握了 Java 这种重量级语言的你来说根本不算难事。这里胖哥只对 Redis 中的涉及到的基本语法说一说。

Lua 的简单语法

Lua 在 Redis 脚本中我个人建议只需要使用下面这几种类型:

  1. nil
  2. boolean 布尔值
  3. number 数字
  4. string 字符串
  5. table

声明类型

声明类型非常简单,不用携带类型。

--- 全局变量
name = 'felord.cn'
--- 局部变量
local age = 18

Redis 脚本在实践中不要使用全局变量,局部变量效率更高。

table 类型

前面四种非常好理解,第五种table需要简单说一下,它既是数组又类似 Java 中的HashMap(字典),它是 Lua 中仅有的数据结构。

数组不分具体类型,演示如下

Lua 5.1.5  Copyright (C) 1994-2012 Lua.org, PUC-Rio
> arr_table = {'felord.cn','Felordcn',1}
> print(arr_table[1])
felord.cn
> print(arr_table[3])
1
> print(#arr_table)
3

作为字典:

Lua 5.1.5  Copyright (C) 1994-2012 Lua.org, PUC-Rio
> arr_table = {name = 'felord.cn', age = 18}
> print(arr_table['name'])
felord.cn
> print(arr_table.name)
felord.cn
> print(arr_table[1])
nil
> print(arr_table['age'])
18
> print(#arr_table)
0

混合模式:

Lua 5.1.5  Copyright (C) 1994-2012 Lua.org, PUC-Rio
> arr_table = {'felord.cn','Felordcn',1,age = 18,nil}
> print(arr_table[1])
felord.cn
> print(arr_table[4])
nil
> print(arr_table['age'])
18
> print(#arr_table)
3

# 取 table 的长度不一定精准,慎用。同时在 Redis 脚本中避免使用混合模式的 table,同时元素应该避免包含空值nil。在不确定元素的情况下应该使用循环来计算真实的长度。

判断

判断非常简单,格式为:

local a = 10
if a < 10  then
 print('a小于10')
elseif a < 20 then
 print('a小于20,大于等于10')
else
 print('a大于等于20')
end

数组循环

local arr = {1,2,name='felord.cn'}

for i, v in ipairs(arr) do
    print('i = '..i)
    print('v = '.. v)
end

print('-------------------')

for i, v in pairs(arr) do
    print('p i = '..i)
    print('p v = '.. v)
end

打印结果:

i = 1
v = 1
i = 2
v = 2
-----------------------
p i = 1
p v = 1
p i = 2
p v = 2
p i = name
p v = felord.cn

返回值

像 Python 一样,Lua 也可以返回多个返回值。不过在 Redis 的 Lua 脚本中不建议使用此特性,如果有此需求请封装为数组结构。在 Spring Data Redis 中支持脚本的返回值规则可以从这里分析:

public static ReturnType fromJavaType(@Nullable Class<?> javaType) {

   if (javaType == null) {
      return ReturnType.STATUS;
   }
   if (javaType.isAssignableFrom(List.class)) {
      return ReturnType.MULTI;
   }
   if (javaType.isAssignableFrom(Boolean.class)) {
      return ReturnType.BOOLEAN;
   }
   if (javaType.isAssignableFrom(Long.class)) {
      return ReturnType.INTEGER;
   }
   return ReturnType.VALUE;
}

胖哥在实践中会使用 ListBooleanLong三种,避免出现幺蛾子。

到此为止 Redis Lua 脚本所需要知识点就完了,其它的函数、协程等特性也不应该在 Redis Lua 脚本中出现,用到内置函数的话搜索查询一下就行了。

在接触一门新的技术时先要中规中矩的使用,如果你想玩花活就意味着更高的学习成本。

4. Redis 中的 Lua

接下来就是 Redis Lua 脚本的实际操作了。

EVAL 命令

Redis 中使用EVAL命令来直接执行指定的 Lua 脚本。

EVAL luascript numkeys key [key ...] arg [arg ...]
  • EVAL 命令的关键字。
  • luascript Lua 脚本。
  • numkeys 指定的 Lua 脚本需要处理键的数量,其实就是 key数组的长度。
  • key 传递给 Lua 脚本零到多个键,空格隔开,在 Lua 脚本中通过 KEYS[INDEX]来获取对应的值,其中1 <= INDEX <= numkeys
  • arg是传递给脚本的零到多个附加参数,空格隔开,在 Lua 脚本中通过ARGV[INDEX]来获取对应的值,其中1 <= INDEX <= numkeys

接下来我简单来演示获取键hello的值得简单脚本:

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> EVAL "return redis.call('GET',KEYS[1])" 1 hello
"world"
127.0.0.1:6379> EVAL "return redis.call('GET','hello')"
(error) ERR wrong number of arguments for 'eval' command
127.0.0.1:6379> EVAL "return redis.call('GET','hello')" 0
"world"

从上面的演示代码中发现,KEYS[1]可以直接替换为hello,但是 Redis 官方文档指出这种是不建议的,目的是在命令执行前会对命令进行分析,以确保 Redis Cluster 可以将命令转发到适当的集群节点

numkeys无论什么情况下都是必须的命令参数。

call 函数和 pcall 函数

在上面的例子中我们通过redis.call()来执行了一个SET命令,其实我们也可以替换为redis.pcall()。它们唯一的区别就在于处理错误的方式,前者执行命令错误时会向调用者直接返回一个错误;而后者则会将错误包装为一个我们上面讲的table表格:

127.0.0.1:6379> EVAL "return redis.call('no_command')" 0
(error) ERR Error running script (call to f_1e6efd00ab50dd564a9f13e5775e27b966c2141e): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script
127.0.0.1:6379> EVAL "return redis.pcall('no_command')" 0
(error) @user_script: 1: Unknown Redis command called from Lua script

这就像 Java 遇到一个异常,前者会直接抛出一个异常;后者会把异常处理成 JSON 返回。

值转换

由于在 Redis 中存在 Redis 和 Lua 两种不同的运行环境,在 Redis 和 Lua 互相传递数据时必然发生对应的转换操作,这种转换操作是我们在实践中不能忽略的。例如如果 Lua 脚本向 Redis 返回小数,那么会损失小数精度;如果转换为字符串则是安全的。

127.0.0.1:6379> EVAL "return 3.14" 0
(integer) 3
127.0.0.1:6379> EVAL "return tostring(3.14)" 0
"3.14"

根据胖哥经验传递字符串、整数是安全的,其它需要你去仔细查看官方文档并进行实际验证

原子执行

Lua 脚本在 Redis 中是以原子方式执行的,在 Redis 服务器执行EVAL命令时,在命令执行完毕并向调用者返回结果之前,只会执行当前命令指定的 Lua 脚本包含的所有逻辑,其它客户端发送的命令将被阻塞,直到EVAL命令执行完毕为止。因此 LUA 脚本不宜编写一些过于复杂了逻辑,必须尽量保证 Lua 脚本的效率,否则会影响其它客户端。

脚本管理

SCRIPT LOAD

加载脚本到缓存以达到重复使用,避免多次加载浪费带宽,每一个脚本都会通过 SHA 校验返回唯一字符串标识。需要配合EVALSHA命令来执行缓存后的脚本。

127.0.0.1:6379> SCRIPT LOAD "return 'hello'"
"1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"
127.0.0.1:6379> EVALSHA 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 0
"hello"

SCRIPT FLUSH

既然有缓存就有清除缓存,但是遗憾的是并没有根据 SHA 来删除脚本缓存,而是清除所有的脚本缓存,所以在生产中一般不会再生产过程中使用该命令。

SCRIPT EXISTS

以 SHA 标识为参数检查一个或者多个缓存是否存在。

127.0.0.1:6379> SCRIPT EXISTS 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b  1b936e3fe509bcbc9cd0664897bbe8fd0cac1012
1) (integer) 1
2) (integer) 0

SCRIPT KILL

终止正在执行的脚本。但是为了数据的完整性此命令并不能保证一定能终止成功。如果当一个脚本执行了一部分写的逻辑而需要被终止时,该命令是不凑效的。需要执行SHUTDOWN nosave在不对数据执行持久化的情况下终止服务器来完成终止脚本。

其它一些要点

了解了上面这些知识基本上可以满足开发一些简单的 Lua 脚本了。但是实际开发中还是有一些要点的。

  • 务必对 Lua 脚本进行全面测试以保证其逻辑的健壮性,当 Lua 脚本遇到异常时,已经执行过的逻辑是不会回滚的。
  • 尽量不使用 Lua 提供的具有随机性的函数,参见相关官方文档。
  • 在 Lua 脚本中不要编写function函数,整个脚本作为一个函数的函数体。
  • 在脚本编写中声明的变量全部使用local关键字。
  • 在集群中使用 Lua 脚本要确保逻辑中所有的key分到相同机器,也就是同一个插槽(slot)中,可采用Redis Hash Tag技术。
  • 再次重申 Lua 脚本一定不要包含过于耗时、过于复杂的逻辑。

5. 总结

本文对 Redis Lua 脚本的场景以及编写 Redis Lua 脚本所需要的 Lua 编程语法进行了详细的讲解和演示,也对 Redis Lua 脚本在实际开发中需要注意的一些要点进行了分享。希望能够帮助你掌握此技术。今天的分享就到这里,下次我将分享如何在实际 Redis 开发中使用 Lua 脚本,所以这一篇一定要进行掌握。多多关注:码农小胖哥 获取更多编程知识干货。

本文分享自微信公众号 - 码农小胖哥(Felordcn),作者:码农小胖哥

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-10-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Spring Redis中使用Lua脚本实现高并发原子操作

    在上一文中我对 Lua 语言的一些简单的语法及其在 Redis 中的操作进行了介绍,但是在 Java 开发中我们还需要进一步的学习才能使这种技术落地。今天就结合...

    码农小胖哥
  • 利用Redis的Geo功能实现查找附近的位置

    老板突然要上线一个需求,获取当前位置方圆一公里的业务代理点。明天上线!当接到这个需求的时候我差点吐血,这时间也太紧张了。赶紧去查相关的技术选型。经过一番折腾,终...

    码农小胖哥
  • 利用Redis的Geo功能实现查找附近的位

    老板突然要上线一个需求,获取当前位置方圆一公里的业务代理点。明天上线!当接到这个需求的时候我差点吐血,这时间也太紧张了。赶紧去查相关的技术选型。经过一番折腾,终...

    码农小胖哥
  • 高性能伪事务之Lua in Redis

    Redis2.6加入了对Lua脚本的支持。Lua脚本可以被用来扩展Redis的功能,并提供更好的性能。

    sunsky
  • Redis进阶应用:Redis+Lua脚本实现复合操作

    Redis是高性能的key-value数据库,在很大程度克服了memcached这类key/value存储的不足,在部分场景下,是对关系数据库的良好补充。得益于...

    宜信技术学院
  • 聊一聊 QTL 定位的原理

    通过前两周的《本地化适应是怎么发生的?》和《突变是否影响个体的适应性?》了解了群体的核酸多样性后,我们接下来就开始要着手进行功能基因的定位了。工欲善其事,必先利...

    企鹅号小编
  • 你会见证11位时间戳的出现!

    zhaoolee
  • 字节码增强:原理与实战

    本文由一个拦截器逻辑的使用场景及演变历程,引入字节码增强技术。介绍字节码的本质,字节码增强的原理及JVM 启动过程中的 Agent 加载、生效流程,并对常见字节...

    2020labs小助手
  • SAP Spartacus里的@mixin visible-focus

    第一个scss文件:feature-libs\organization_index.scss

    Jerry Wang
  • 通过一个完整例子彻底学会protobuf序列化原理

    Protobuf是我们在网络传输中经常会用到的协议,优点是版本间兼容性强,对数据序列化时的极致压缩使得Protobuf包体积比xml、json等格式要小很多,节...

    horstxu

扫码关注云+社区

领取腾讯云代金券