高并发原子性操作( Redis+Lua)
「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」
一、什么是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 : user:1002修改为ljw-hello
复制代码




近期评论