springboot使用RedisTemplate执行lua脚本
- 业务场景
-
-
- pom文件中引入redis 依赖
- redis 配置类
- 实现的lua脚本
- 实现的java代码
- 实现功能过程中遇到的一些坑
-
业务场景
不同的地区办理业务生成的文件编号格式必须要求为:当前年月+5位数字,且数字是从00001开始递增。最开始想的解决办法是生成自增序列去实现,但是由于地区数量太多,不能每个地区都整一个序列,所以这个方案就不行了。于是想到用redis去实现这个业务场景。由于对redis和lua脚本的不熟悉,所以此次做一个开发过程的问题记录。
pom文件中引入redis 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis 配置类
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(cf);
// 6.序列化类,对象映射设置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
// key采用String的序列化方式
redisTemplate.setKeySerializer(stringSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringSerializer);
// value序列化方式采用jackson
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
实现的lua脚本
local isExists = redis.call('exists',KEYS[1])
if isExists == 1 then
redis.call('hincrby',KEYS[1],KEYS[2],ARGV[3])
redis.call('hset',KEYS[1],KEYS[3],ARGV[2])
end
if isExists == 0 then
redis.call('hset',KEYS[1],KEYS[2],ARGV[1],KEYS[3],ARGV[2])
end
local fileNoMap = {}
fileNoMap['bh'] = redis.call('hget',KEYS[1],KEYS[2])
fileNoMap['id'] = redis.call('hget',KEYS[1],KEYS[3])
return cjson.encode(fileNoMap)
实现的java代码
@RequestMapping("generateBusinessFileNo")
@ResponseBody
public ResultModel generateBusinessFileNo(String areaCode,String process,String businessId){
String result = "";
Map<String,String> fileNoMap = Maps.newHashMap();
DefaultRedisScript<Map> redisScript = new DefaultRedisScript<>();
//设置返回值类型
redisScript.setResultType(Map.class);
//设置lua脚本文件路径
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redis_genFileNo.lua")));
List<String> keys = new ArrayList<>();
//使用业务环节编码和地区编码作为 key
String key = process+areaCode;
keys.add(key);
//获取当前redis中的businessFileNo
String exist_businessFileNo = (String) redisTemplate.opsForHash().get(key,"business.file.no");
String exist_businessId = (String)redisTemplate.opsForHash().get(key,"business.id");
String year = String.valueOf(LocalDate.now().getYear());
String businessFileNo = year+"00001";
long increment=1;
//如果当前redis中的businessFileNo不为空且年份和当前年份不一致,编号从00001重新开始
if(StringUtil.isNotEmpty(exist_businessFileNo)&&!(exist_businessFileNo.substring(0,4)).equals(year)){
fileNoMap.put("business.file.no",businessFileNo);
fileNoMap.put("business.id",businessId);
result = JSON.toJSONString(fileNoMap);
}else {
//判断当前业务id是否和是已经写入redis的id,如果不相同,则执行lua脚本
if(!businessId.equals(exist_businessId)){
keys.add("business.file.no");
keys.add("business.id");
Object executeObj = redisTemplate.opsForHash().getOperations().execute(redisScript, keys,businessFileNo,businessId,increment);
result = String.valueOf(executeObj);
System.out.println(result);
}else{
//id相同不执行lua脚本,直接返回当前redis中的值
Map<String,String> entries = redisTemplate.opsForHash().entries(key);
result = JSON.toJSONString(entries);
}
}
return ResultModel.ok().data(result);
}
实现功能过程中遇到的一些坑
此前的工作中未接触过lua脚本,在完成这个功能的时候也查询了相关的文章,找到的都是仅限于redistemplate 操作 简单的set和get
最初的想法是直接采用String 数据类型进行操作,后面发现String类型不能满足要求,需要把文件编号和当前的业务id绑定一起存放,否则就没有依据条件来判断这个请求是当前的业务重新调用还是新的业务请求需要把文件id自增
最初使用String类型的脚本
local isExists = redis.call('exists',KEYS[1])
if isExists == 1 then
redis.call('incr',KEYS[1])
end
if isExists == 0 then
redis.call('set',KEYS[1],ARGV[1])
end
local fileNo = redis.call('get',KEYS[1])
return fileNo
第一个问题是redisTemplate.execute() 执行完脚本后发现取到的值是空的,但是在redis 客户端执行命令后发现数据其实已经写入到redis了,只是取值的时候有问题。由于公司的这个redis 配置类没有写全,都没有对key和value做序列化处理导致的
第二个问题是lua 脚本的问题:
当时想的是直接用 redis.call(‘hgetall’,KEYS[[1]]) 把值所有的key value 一起返回,但执行过后发现只能返回一个值,其他的像是被覆盖了,后面百度了一下要返回json串 的脚本写法才知道该怎么做
第三个问题是 执行lua脚本 后发现id没有自增,取到的值还是空的。执行的lua脚本如下:
`
local isExists = redis.call('exists',KEYS[1])
if isExists == 1 then
redis.call('hincrby',KEYS[1],KEYS[2],ARGV[3])
redis.call('hset',KEYS[1],KEYS[3],ARGV[2])
end
if isExists == 0 then
redis.call('hset',KEYS[1],KEYS[2],ARGV[1],KEYS[3],ARGV[2])
end
local fileNoMap = {}
fileNoMap['bh'] = redis.call('hget',KEYS[1],KEYS[2])
fileNoMap['id'] = redis.call('hget',KEYS[1],KEYS[3])
return cjson.encode(fileNoMap)
后面改成了下面的代码就执行正常了
`local fileNoMap = {}
local fileNo
local fileId
local isExists = redis.call('exists',KEYS[1])
if isExists == 1 then
redis.call('hset',KEYS[1],KEYS[3],ARGV[2])
fileNo = redis.call('hincrby',KEYS[1],KEYS[2],ARGV[3])
fileId = redis.call('hget',KEYS[1],KEYS[3])
end
if isExists == 0 then
redis.call('hset',KEYS[1],KEYS[2],ARGV[1],KEYS[3],ARGV[2])
fileNo = redis.call('hget',KEYS[1],KEYS[2])
fileId = redis.call('hget',KEYS[1],KEYS[3])
end
fileNoMap['bh'] = fileNo
fileNoMap['id'] = fileId
return cjson.encode(fileNoMap)
第四个问题
此次实现了业务场景,当初是考虑到并发场景,所以选择用了lua脚本,现在业务代码是redisTemplate.opsForHash.get() /set() 和 redisTemplate 执行lua脚本一起使用的,不知道在并发的情况下会不会出现问题,后面准备写个多线程压一下看看会出现什么情况,如果在并发情况下有问题,就要考虑加锁了
此处追加:(在集群环境下不要使用) 由于在测试环境使用的是单机版本redis,所以执行lua脚本的时候一切正常,但是正式环境使用的是redis集群,所以就会出现:(error) CROSSSLOT Keys in request don’t hash to the same slot 这边使用的是要使用到hset/hget操作,涉及到的key 有多个,在最后执行的时候会出现key不在同一个槽的情况。
解决办法:可以使用redis 分布式锁去进行操作,如果是String 操作的get ,可以在执行脚本的时候,使用多个 value 去给脚本中key 传值