Redis 中使用 Lua 脚本

Redis 本身已经提供了丰富的命令,但是直接用来处理一些复杂业务时可能还不够方便,会有一定的局限性。因此,在 Redis2.6 版本开始提供了对 Lua 脚本的支持,Lua 脚本的使用还是比较广泛的,比如商品秒杀、分布式锁等,使用 Lua 脚本可以带来以下的好处:

  • 原子性,Redis 会将 Lua 脚本当做一个整体去执行,无需额外开启事务,可以在高并发场景下保持数据的一致性。
  • 复用,编写好的 Lua 脚本可以保存在本地,然后发送到 Redis 服务器执行;还可以把脚本缓存到 Redis 服务器,会返回脚本内容的 SHA1 校验和的编码,然后通过编码调用脚本,这里后边会详细介绍。
  • 减少网络开销,如果脚本内容很多,还是建议将脚本缓存到服务器,避免直接从本地发送脚本到服务器时在网络传输上造成性能损失。

一、基本用法

为了让例子更加的贴近实际应用,这里实现一个简单版的分布式锁。这里先用Jedis操作。

public class LuaService {
    public void test1() {
        JedisPool jedisPool = new JedisPool("localhost", 6379);
        Jedis jedis = jedisPool.getResource();
        jedis.auth("shehuan");
        try {
            // 1、获取一个随机字串
            String value = UUID.randomUUID().toString();
            // 2、获取锁
            // 为了防止获取锁和设置锁的过期时间之间,由于某种原因 Redis 挂了,没成功设置过期时间,导致锁不能被释放,
            // 需要让 setnx、expire 命令一起执行,保证原子性
            String lock = jedis.set("lock", value, new SetParams().nx().ex(5L));
            // 3、判断是否成功获取到锁
            if (lock != null && "OK".equals(lock)) {
                // 4、获取到锁了,获取到锁执行具体的业务,例如修改库存
                jedis.set("stock", "1000");
                // 5、释放锁
                // 简单粗暴的方式是直接 jedis.del("lock")
                // 但这样这可能会有问题,如果第一个线程获取锁后执行的业务比较耗时,在到达锁的过期时间后还没执行完,但此时锁已经释放,
                // 此时,第二个线程获取到了锁,在第二个线程执行业务的过程中,第一个线程的业务执行结束了,它去把锁释放掉了,
                // 但第一个线程释放掉的是第二个线程获取到的锁,造成错误释放,这样乱套了,
                // 所以需要保证每个线程只能释放自己获得的锁,不能释放别人的, 释放锁的操作就可以通过 Lua 脚本实现
                // todo
            } else {
                // 6、没获取到锁,可以停止操作,或稍后再继续尝试
                System.out.println("没获取到锁");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        }
    }
}

上边详细的介绍了分布式锁的实现过程,以及可能出现的问题,最终,我们决定删除锁的操作使用 Lua 脚本实现,对应的脚本如下:

if redis.call("get", KEYS[1]) == ARGV[1] then
   return redis.call("del", KEYS[1])
else
   return 0
end

Lua 脚本中执行具体的 Redis 命令,需要使用redis.call()方法,KEYS表示客户端发起脚本执行命令时携带的 Redis key 的一个集合,ARGV则是其它参数的一个集合,主意下标从1开始。结合我们的业务,这里的KEYS[1]则表示lockARGV[1]则是一个随机字符串。整个脚本的含义就是,如果客户端传递的lockvalue和 Redis 中存储的一致,就删除lock

Lua 脚本的语法还是比较简单的,具体内容可以自行学习。

前边的准备工作基本结束了,文章开始说过执行脚本有两种途径,下边我们具体来看:

1、客户端直接发送脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList("lock"), Collections.singletonList(value));
if (result != null && (Long) result != 0) {
    System.out.println("成功释放锁");
}

这里使用jedis.eval()发送脚本到 Redis 服务器执行,后两个参数分别是 key 的集合,以及 value 参数的集合。

2、Redis 服务器缓存脚本

先将脚本以文件形式放到 Redis 里,例如这样:


然后通过如下命令让 Redis 服务器缓存脚本:

redis-cli -x -a 连接redis的密码 script load < xxx.lua

script load命令会在 Redis 服务器缓存 Lua 脚本,并且脚本内容经过 SHA-1 签名算法处理后,会返回脚本内容的 SHA1 校验和的编码,然后在端调用时,传入编码字符串作为参数,这样 Redis 服务器就会执行对应缓存的脚本了,就不用了每次发送具体的脚本内容了。

Object result = jedis.evalsha("e58196026fa94e1c49d14a835c57e941f9ae4202", Collections.singletonList("k1"), Collections.singletonList(value));
if ((Long) result != 0) {
    System.out.println("成功释放锁");
}

还有两个比较有用的命令:

  • script exists 使用 sha1 判断是否存在对应的缓存脚本;
  • script flush删除全部的缓存脚本。

除了使用上边的命令缓存脚本、生成脚本的 SHA1 校验和的编码,还可以使用 Jedis 实现,但最终的 SHA1 编码内容是不同的:

String sha1 = jedis.scriptLoad(script);

二、在 SpringBoot 中使用

实际的项目中,可能更多的会在 SpringBoot 项目中整合 Redis,此时执行 Lua 脚本的基本流程如下:

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置脚本内容
redisScript.setScriptText(script);
// 设置脚本返回值类型
redisScript.setResultType(Long.class);
// 执行脚本
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList("lock"), value);
if (result != null && result != 0) {
    System.out.println("成功释放锁");
}

核心的类就是DefaultRedisScript,它实现了RedisScript接口。execute()方法最后一个参数是可变类型的,用来传递多个 value 参数。初次执行execute()方法时,其内部会自动缓存 Lua 脚本到 Redis 服务器;同时每次执行脚本时会根据脚本内容自动计算出对应的 SHA1 校验和的编码,去匹配、执行缓存的脚本。

具体的 SHA1 校验和的编码,可以在execute()方法执行后,使用redisScript.getSha1()查看。使用 SpringBoot 方式 执行 Lua 脚本生成的 SHA1 校验和的编码和前边直接使用 Jedis 生成的一致。

无论用那种方式在 Redis 中使用 Lua 脚本,其中的原理都是类似的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容