前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在Redis中使用简单强大的Lua脚本

在Redis中使用简单强大的Lua脚本

作者头像
Java识堂
发布2019-11-04 12:39:01
2.3K0
发布2019-11-04 12:39:01
举报
文章被收录于专栏:Java识堂Java识堂

Redis分布式锁加锁

前段时间写Redis分布式锁,想着在小灰文章的基础上再总结一下,这样能有更深的印象,顺便把Lua脚本分享一下,如果项目中使用Redis比较多,那么Lua脚本一定是会用到的,因为它简单强大。

建议先看一下小灰之前写的文章

漫画:什么是分布式锁?

最开始的分布式锁是使用setnx+expire命令来实现的。setnx设置成功返回1,表示获取到锁,返回0,表示没有获取到锁,同时为了避免显示释放锁失败,导致资源永远也不释放,获取到锁后还会用expire命令设置锁超时的时间。

但有个问题就是setnx+expire不是原子性的,有可能获取到锁后,还没执行expire命令,也没执行释放锁的操作,服务就挂了,这样这个资源就永远也不会访问到了。

为了解决这个问题,Redis 2.6.12版本以后,为set命令增加了一系列的参数,我们此时用NX和PX参数就可以解决这个问题。

所以现在Redis分布式锁的加锁命令如下

代码语言:javascript
复制
SET resource_name random_value NX PX 30000

NX只会在key不存在的时候给key赋值,PX通知Redis保存这个key 30000ms,当资源被锁定超过这个时间时,锁将自动释放

random_value最好是全局唯一的值,保证释放锁的安全性

代码语言:javascript
复制
# 设置成功返回OK
127.0.0.1:6379> SET lock1 100 NX PX 30000
OK
127.0.0.1:6379> SET lock1 100 NX PX 30000
(nil)

当某个key不存在时才能设置成功。这就可以让多个并发线程同时去设置同一个key,只有一个能设置成功。而其他线程设置失败,也就是获得锁失败

Redis分布式锁解锁

解锁不能简单的使用如下命令

代码语言:javascript
复制
del resource_name 

因为有可能节点A加锁后执行超时,锁被释放了。节点B又重新加锁,A正常执行到del命令的话就把节点B的锁给释放了。所以在解锁之前先判断一下是不是自己加的锁,是自己加的锁再释放,不是就不释放。所以伪代码如下

代码语言:javascript
复制
if (random_value .equals(redisClient.get(resource_name))) {
    del(key)
}

因为判断和解锁是2个独立的操作,不具有原子性,还是有可能会出问题。所以解锁的过程要执行如下的Lua脚本,通过Lua脚本来保证判断和解锁具有原子性。

代码语言:javascript
复制
if redis.call("get", KEYS[1]) == ARGV[1] then 
    return redis.call("del", KEYS[1]) 
else 
    return 0 
end

如果key对应的value一致,则删除这个key,通过这个方式释放锁是为了避免Client释放了其他Client申请的锁

到此你已经彻底理解了该如何实现一个分布式锁了,以及为什么要这样做的原因

加锁执行命令

代码语言:javascript
复制
SET resource_name random_value NX PX 30000

解锁执行脚本

代码语言:javascript
复制
if redis.call("get", KEYS[1]) == ARGV[1] then 
    return redis.call("del", KEYS[1]) 
else 
    return 0 
end

可能有小伙伴对Lua脚本不太熟悉,所以下面就是介绍Lua脚本的部分

Redis执行Lua脚本的命令

从Redis2.6开始,内嵌Lua环境,通过EVAL和EVALSHA命令可以执行脚本

代码语言:javascript
复制
EVAL script numkeys key [key...] arg [arg...]

参数

解释

script

脚本

numkeys

键的个数

key [key…]

key列表,键名通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)

arg [arg…]

参数列表,参数通过全局变量 ARGV 数组,用 1 为基址的形式访问( ARGV[1] , ARGV[2] ,以此类推)

EVAL命令

先演示一下

代码语言:javascript
复制
EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 first second

输出为

代码语言:javascript
复制
1) "key1"
2) "key2"
3) "first"
4) "second"
EVALSHA命令

EVAL命令是直接执行给定的脚本

EVALSHA命令可以根据给定的sha1校验码,执行缓存在服务器中的脚本 首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,EVALSHA命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。而脚本也会常驻在服务端,脚本功能得到了复用。

通过 SCRIPT LOAD 命令可以将脚本缓存到服务器,这个命令会返回脚本的sha1值

代码语言:javascript
复制
SCRIPT LOAD script
代码语言:javascript
复制
EVALSHA sha1 numkeys key [key ...] arg [arg ...] 

sha1为脚本sha1值

在Lua脚本中调用Redis方法

有2种方式redis.call()和redis.pcall()

redis.call()与redis.pcall()非常类似,唯一的区别是,如果Redis命令调用发生了错误,redis.call() 将抛出一个Lua类型的错误,再强制EVAL命令把错误返回给命令的调用者,而redis.pcall()将捕获错误并返回表示错误的Lua表类型

代码语言:javascript
复制
127.0.0.1:6379> set testKey testValue
OK
127.0.0.1:6379> get testKey
"testValue"

用脚本实现上述功能

代码语言:javascript
复制
127.0.0.1:6379> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 scriptKey scriptValue
OK
127.0.0.1:6379> eval "return redis.call('get', KEYS[1])" 1 scriptKey
"scriptValue"

用SCRIPT LOAD命令缓存脚本

代码语言:javascript
复制
127.0.0.1:6379> SCRIPT LOAD "return redis.call('set', KEYS[1], ARGV[1])"
"55b22c0d0cedf3866879ce7c854970626dcef0c3"
127.0.0.1:6379> evalsha 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 sha1Key sha1Value
OK
127.0.0.1:6379> get sha1Key
"sha1Value"

本文只介绍了一个Lua脚本的使用,Lua脚本的流程控制(循环,判断等)就不再介绍,很快就能学会。当你有一些复杂的操作Redis的功能想和其他人共享,或者实现命令的原子性时,就可以考虑用Lua脚本来实现

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java识堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis分布式锁解锁
  • Redis执行Lua脚本的命令
    • EVAL命令
      • EVALSHA命令
      • 在Lua脚本中调用Redis方法
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档