阅读 196

redis分布式缓存(八)一一 高并发原子性操作( Redis+Lua)

一、什么是lua ?

Lua 是一个简洁、轻量、可扩展的脚本语言,它的特性有:

  • 轻量:源码包只有核心库,编译后体积很小。

  • 高效:由C编写的,启动快、运行快。

  • 内嵌:可内嵌到各种编程语言或系统中运行,提升静态语言的灵活性。

二、Redis为什么要使用LUA ?

  1. 原子性:将redis的多个操作合成一个脚本,然后整体执行,在脚本的执行中,不会出现资源竞争的情况。

  2. 减少网络通信:把多个命令合成一个lua脚本,redis统一执行脚本。

  3. 复用性:client发送的脚本会永久存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑。

三、lua的语法入门

EVAL script numkeys key [key ...] arg [arg ...] 复制代码

  • script: 参数是一段 Lua脚本程序。脚本不必(也不应该)定义为一个Lua函数。

  • numkeys: 用于指定key参数的个数。

  • key [key ...]: 代表redis的key,从 EVAL 的第三个参数开始算起,表示在脚本中所用到的Redis键(key)。

    • 在Lua中,这些键名参数可以通过全局变量 KEYS 数组,用1为基址的形式访问( KEYS[1] ,KEYS[2],依次类推)。

  • arg [arg ...]: 代表lua的入参,在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

    • 特别注意:lua的数组坐标不是从0开始,是从1开始

四、Redis管理Lua脚本 

命令作用
EVAL script numkeys key [key ...] arg [arg ...]执行 Lua 脚本
EVALSHA sha1 numkeys key [key ...] arg [arg ...]执行 Lua 脚本
redis-cli -a 密码 --eval Lua脚本路径 key [key …] , arg [arg …]linux(window中有些函数报错的,如KEYS[2],ARGV[1]获取不了)中执行Lua脚本文件:如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi


SCRIPT exists sha1 [sha1 …]此命令用于判断sha1是否已经加载到Redis内存中 
SCRIPT FLUSH从脚本缓存中移除所有脚本
SCRIPT KILL杀死当前正在运行的 Lua 脚本
SCRIPT LOAD script添加到脚本缓存中,但并不立即执行这个脚本
127.0.0.1:6379> script load "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" "a42059b356c875f0717db19a51f6aaca9ae659ea" 127.0.0.1:6379> script exists a42059b356c875f0717db19a51f6aaca9ae659ea 1) (integer) 1 127.0.0.1:6379> 复制代码

127.0.0.1:6379> EVALSHA  a42059b356c875f0717db19a51f6aaca9ae659ea 2  key1 key2 ljw1 ljw2 1) "key1" 2) "key2" 3) "ljw1" 4) "ljw2" 127.0.0.1:6379> EVAL  "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2  key1 key2 ljw1 ljw2 1) "key1" 2) "key2" 3) "ljw1" 4) "ljw2" 复制代码

127.0.0.1:6379>  script kill (error) NOTBUSY No scripts in execution right now. 127.0.0.1:6379> script flush OK 127.0.0.1:6379> script exists a42059b356c875f0717db19a51f6aaca9ae659ea 1) (integer) 0 复制代码

对以上脚本的详细说明:

  1. eval为redis的关键字

  2. 双引号的内容代表lua脚本

  3. 2代表numkeys参数的个数,即有多少个key

  4. key1 和 key2代表 KEYS[1],KEYS[2]的入参

  5. ljw1 ljw2 是ARGV[1],ARGV[2]的入参

五、案例分析

修改用户名称,如果用户不存在redis中,则新增,存在则修改

    /**      * 修改用户名称      * @param uid 用户id      * @param uname 用户名称      */     @GetMapping(value = "/updateUser")     public void updateUser(Integer uid,String uname) {         String key="user:"+uid;         //优化点:第一次发送redis请求         String old=this.stringRedisTemplate.opsForValue().get(key);         if(StringUtils.isEmpty(old)){             //优化点:第二次发送redis请求             this.stringRedisTemplate.opsForValue().set(key,uname);             return;         }         if(old.equals(uname)){             log.info("{}不用修改", key);         }else{             log.info("{}从{}修改为{}", key,old,uname);             //优化点:第二次发送redis请求             this.stringRedisTemplate.opsForValue().set(key,uname);         }     } 复制代码

问题分析

以上代码,看似简单,但是在高并发的情况下,还是有一点性能瓶颈,在性能方面主要是发送了2次redis请求

那如何优化呢?

我们可以采用lua技术,把2次redis请求合成一次。

方案优化:lua脚本

-- 成功设置返回1 没设置返回0 -- 如果redis没找到,就直接写进去 if redis.call('get', KEYS[1]) == nil then    redis.call('set', KEYS[1], ARGV[1]);    return 1 end -- 如果旧值不等于新值,就把新值设置进去 if redis.call('get', KEYS[1]) ~= ARGV[1]  then    redis.call('set', KEYS[1], ARGV[1]);    return 1 else    return 0 end 复制代码

linux命令行执行lua脚本

把以上代码保存为compareAndSet.lua

##要在linux环境中执行命令,window中获取不了KEYS, ARGV ./redis-cli  --eval compareAndSet.lua user:101 , ljw101 复制代码

  1. --eval 告诉redis-cli 要执行后面的lua脚本,compareAndSet.lua脚本的目录位置

  2. user:101 是redis要操作的key,在lua脚本中用KEYS[1]就能拿到

  3. "," 逗号后面的ljw101 是lua的参数,lua脚本中用ARGV[1]就能拿到

  4. 与redis-cli中不同,此处不需要指定KEYS的数量,但是需要用英文逗号隔开KEYS和ARGV参数,逗号前后至少保留1个空格,否则报错

注意: ","两边的空格不能省略,否则报错,要在linux环境中执行,window中获取不了KEYS, ARGV

执行效果如下:

[root@node2 src]# ./redis-cli  --eval compareAndSet.lua user:101 , ljw101 (integer) 1  //第一次执行,redis没找到,就把值设置进去 [root@node2 src]# ./redis-cli  --eval compareAndSet.lua user:101 , ljw101 (integer) 0  //第二次执行,旧值和新值相同,返回0 复制代码

SpringBoot整合lua脚本

步骤1:编写lua文件,并存储于resources/lua

把以下内容存储于resources/lua/compareAndSet.lua

-- 成功设置返回1 没设置返回0 -- 如果redis没找到,就直接写进去 if redis.call('get', KEYS[1]) == nil then    redis.call('set', KEYS[1], ARGV[1]);    return 1 end -- 如果旧值不等于新值,就把新值设置进去 if redis.call('get', KEYS[1]) ~= ARGV[1]  then    redis.call('set', KEYS[1], ARGV[1]);    return 1 else    return 0 end 复制代码

步骤2:创建lua脚本对象

创建DefaultRedisScript对象,用于存放lua脚本,把resources/lua/compareAndSet.lua脚本内容,存储在DefaultRedisScript对象里面。

@Configuration public class LuaConfiguration {   @Bean     public DefaultRedisScript<Long> compareAndSetScript() {         DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();         redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/compareAndSet.lua")));         redisScript.setResultType(Long.class);         return redisScript;     } } 复制代码

步骤3:SpringBoot执行lua脚本

@Resource private DefaultRedisScript<Long> compareAndSetScript; @GetMapping(value = "/updateuserlua") public void updateUserLua(Integer uid,String uname) {     String key="user:"+uid;     //设置redis的key     List<String> keys = Arrays.asList(key);     //执行lua脚本,execute方法有3个参数,第一个参数是lua脚本对象,第二个是key列表,第三个是lua的参数数组     Long n = this.stringRedisTemplate.execute(this.compareAndSetScript, keys, uname);     if (n == 0) {         log.info("{}不用修改", key);     } else {         log.info("{}修改为{}", key,uname);     } } 复制代码

步骤4:体验

http://127.0.0.1:8080/doc.html

2021-11-03 15:50:25.145  INFO 11160 --- [nio-9090-exec-1] c.a.r.c.CompareAndSetController          : user:1002修改为ljw 2021-11-03 15:50:45.538  INFO 11160 --- [nio-9090-exec-3] c.a.r.c.CompareAndSetController          : user:1002不用修改 2021-11-03 15:50:49.248  INFO 11160 --- [nio-9090-exec-2] c.a.r.c.CompareAndSetController          : user:1002不用修改 2021-11-03 15:51:59.796  INFO 11160 --- [nio-9090-exec-5] c.a.r.c.CompareAndSetController          :


作者:小伙子vae
链接:https://juejin.cn/post/7026517224705753101


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐