redis分布式缓存(八)一一 高并发原子性操作( Redis+Lua)
一、什么是lua ?
Lua 是一个简洁、轻量、可扩展的脚本语言,它的特性有:
轻量:源码包只有核心库,编译后体积很小。
高效:由C编写的,启动快、运行快。
内嵌:可内嵌到各种编程语言或系统中运行,提升静态语言的灵活性。
二、Redis为什么要使用LUA ?
原子性
:将redis的多个操作合成一个脚本,然后整体执行,在脚本的执行中,不会出现资源竞争的情况。减少网络通信
:把多个命令合成一个lua脚本,redis统一执行脚本。复用性
: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 复制代码
对以上脚本的详细说明:
eval为redis的关键字
双引号的内容代表lua脚本
2代表numkeys参数的个数,即有多少个key
key1 和 key2代表 KEYS[1],KEYS[2]的入参
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 复制代码
--eval 告诉redis-cli 要执行后面的lua脚本,compareAndSet.lua脚本的目录位置
user:101 是redis要操作的key,在lua脚本中用KEYS[1]就能拿到
"," 逗号后面的ljw101 是lua的参数,lua脚本中用ARGV[1]就能拿到
与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