Execute lua script using RedisTemplate

Posted by doug007 on Thu, 27 Jan 2022 17:48:41 +0100

Business scenario

The document number format generated by handling business in different regions must be: current month and year + 5 digits, and the number increases from 00001. The initial solution is to generate self increasing sequences, but because there are too many regions to complete a sequence in each region, this scheme will not work. So I thought of using redis to realize this business scenario. For redis and lua
I'm not familiar with the script, so I'll make a problem record of the development process this time.

Introducing redis dependency into pom file

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

redis configuration class

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. Serialization class, object mapping settings
		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();
		// The key adopts the serialization method of String
		redisTemplate.setKeySerializer(stringSerializer);
		// The key of hash is also serialized by String
		redisTemplate.setHashKeySerializer(stringSerializer);
		// value serialization adopts jackson
		redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
		// The value serialization method of hash adopts jackson
		redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
		redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
		
		return redisTemplate;
	}
}	

lua script implemented

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)

Implemented java code

@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<>();
		//Set return value type
		redisScript.setResultType(Map.class);
		//Set lua script file path
		redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redis_genFileNo.lua")));
		List<String> keys = new ArrayList<>();
		//Use business phase code and region code as key s
		String key = process+areaCode;
		keys.add(key);
		//Get the businessFileNo in the current redis
		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;
		//If the businessFileNo in the current redis is not empty and the year is inconsistent with the current year, the number starts from 00001
		if(StringUtil.isNotEmpty(exist_businessFileNo)&&!(exist_businessFileNo.substring(0,4)).equals(year)){
			fileNoMap.put("business.file.no",businessFileNo);
			fileNoMap.put("business.id",businessId);
			redisTemplate.opsForHash().values(fileNoMap);
		}else {
			//Judge whether the current business id is the same as the id that has been written to redis. If not, execute the lua script
			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{
				//If the id is the same, the lua script will not be executed, and the value in the current redis will be returned directly
				Map<String,String> entries = redisTemplate.opsForHash().entries(key);
				result = JSON.toJSONString(entries);
			}
		}
		return ResultModel.ok().data(result);
	}

Some pits encountered in the process of realizing functions

I haven't touched lua script in my previous work. When I finished this function, I also queried relevant articles and found set and get that are only limited to redistemplate operation
The initial idea was to directly use the String data type for operation. Later, it was found that the String type could not meet the requirements, and the file number needs to be bound and stored with the current business id. otherwise, there is no condition to judge whether the request is a re call of the current business or a new business request, and the file id needs to be increased automatically

Initially, a script of type String was used

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

The first problem is redistemplate After executing the script, execute() finds that the value obtained is empty. However, after the redis client executes the command, it finds that the data has actually been written to redis, but there is a problem when taking the value. This is because the redis configuration class of the company is not fully written and the key and value are not serialized

The second problem is the lua script:
At that time, I wanted to use redis Call ('hgetall ', KEYS[[1]]) returns all the key value s together, but after execution, it is found that only one value can be returned, and the others seem to be overwritten. Later, baidu asked how to write the script to return the json string

The third problem is that after executing the lua script, it is found that the id does not increase automatically, and the obtained value is still empty. The lua script executed is as follows:
`

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)

After that, the following code will execute normally

`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)

Fourth question
The business scenario was implemented this time. At first, the concurrency scenario was considered, so lua script was selected. Now the business code is redisTemplate opsForHash. Get() / set() and redisTemplate execute lua scripts together. I don't know if there will be problems in the case of concurrency. Later, I'm going to write a multi-threaded press to see what happens. If there are problems in the case of concurrency, locking should be considered

Topics: Java Redis lua