一、redis使用lua脚本的优势
1. 原子性操作: Lua脚本在Redis中执行是原子操作,可以保证多个命令的执行不会被其他命令插入,确保数据操作的一致性和完整性。在实际使用中,尤其并发场景,想要保持原子性如果是使用redis,可以使用lua脚本来保证原子性。
2. 减少网络通信: 将多个操作合并为一个Lua脚本,可以减少客户端与Redis服务器之间的网络通信次数,提高系统的性能和效率。
3. 复杂业务逻辑支持: Lua脚本可以支持复杂的业务逻辑,在数据库中执行操作,提高了Redis的功能性和灵活性。
4. 执行效率高: Lua脚本是在Redis服务器端执行的,不需要将数据传输到客户端再传输回来,可以减少数据传输的时间,提高执行效率。
5. 可以减少内存开销: 通过Lua脚本可以在Redis中实现常见的数据处理逻辑,减少了客户端的内存开销
二、代码实现
说明: 本来准备放在 ape-common-redis 中的,但是想想可能每个模块都有自己的相关需求,就放在了业务模块,不过可以将Lua初始化部分抽取为工具类(此处后期可自行优化)!
2.1 RedisLua工具类
说明: 用于初始化RedisLua脚本的初始环境,以及编写了一个 CAS 的Demo!
package com.ssm.user.redislua;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
//RedisLua工具类用于初始化RedisLua脚本的初始环境
@Component
@Slf4j
public class CompareAndSetLua {
@Resource
private RedisTemplate redisTemplate;
private DefaultRedisScript<Boolean> casScript;
//初始化环境
@PostConstruct
public void init() {
casScript = new DefaultRedisScript<>();
casScript.setResultType(Boolean.class);
casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua")));
}
//调用Lua脚本
public boolean compareAndSet(String key, Long oldValue, Long newValue) {
//lua中接收key的为KEYS数组类型,所以我们要传递集合类型
List<String> keys = new ArrayList<>();
keys.add(key);
//casScript在bean初始化时已赋值
Boolean result = (Boolean) redisTemplate.execute(casScript, keys, oldValue, newValue);
return result;
}
}
重点解释
-
DefaultRedisScript 是Spring Data Redis提供的一个类,用于封装Redis脚本的执行
-
@PostConstruct注解 在Spring容器初始化这个Bean之后自动执行。这样做的目的是在Bean的实例化之后,立即执行一些初始化操作,比如加载Redis脚本。
-
setResultType(Boolean.class) 设置脚本执行后返回的结果类型
-
setScriptSource 指定脚本的来源
-
ClassPathResource 用于加载类路径下的资源
2.2 相关Lua脚本
--在此key下,如果传入的oldValue和存在的值相同,则更新为newValue,否则不变!
local key = KEYS[1]
local oldValue = ARGV[1]
local newValue = ARGV[2]
local redisValue = redis.call('get', key)
if (redisValue == false or tonumber(redisValue) == tonumber(oldValue))
then
redis.call('set', key, newValue)
return true
else
return false
end
2.3 运行测试
package com.ssm.user;
import com.ssm.user.redislua.CompareAndSetLua;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@SpringBootTest(classes = {UserApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@Slf4j
public class RedisLuaTest {
@Resource
private RedisTemplate redisTemplate;
@Resource
private CompareAndSetLua compareAndSetLua;
@Test
public void redisLuaTest() {
//先往redis存入一个数据(RedisUtils中未封装 set Long类型的值)
ValueOperations<String, Long> valueOperations = redisTemplate.opsForValue();
valueOperations.set("age", 18L);
log.info("age的值为:{}", valueOperations.get("age"));
boolean result = compareAndSetLua.compareAndSet("age", 18L, 19L);
if (result) {
log.info("修改成功!age的值为:{}", valueOperations.get("age"));
}
}
}