상황
레디스는 각각의 명령어들은 원자적으로 실행이 되지만, 비즈니스로직을 작성하다 보면 여러 명령어들을 한 번에 처리해야 하는 경우가 생긴다.
이를 위해, 레디스를 루아스크립트를 이용해서 여러 명령어를 원자적으로 처리할 수 있도록 지원하고 있다.
레디스에서 LuaScript 사용하기
127.0.0.1:6379> eval "return 'Hello world!'" 0
"Hello world!"
레디스로 스크립트를 실행하는 명령어는 EVAL 이며, 그 다임 인수로 “…”를 감싸서 루아스크립트를 작성하면 된다.
또한 그 다음 인수로는 이후 입력될 KEY 개수이고, 키 개수만큼 입력한 다음의 인수로는 인수를 입력받는다.
EVAL <루아스크립트> <이후 입력될 KEY 개수> [<키1> <키2>, ..., <인수1> <인수2>, ...]
정리하면 위와 같이 입력할 수 있다.
또한 위의 예제에서 볼 수 있듯, 문자열은 ‘…’로 감싸면 된다.
키나 인수를 이용하는 방법은 KEYS[i], ARGV[i] 키워드를 이용하면 된다.
이때 주의할 점은, i는 0이 아닌 1부터 시작된다는 점과, 무조건 대문자로 적어야 한다는 점이다.
127.0.0.1:6379> eval "return 'hello, ' .. ARGV[1]" 0 'world'
"hello, world"
위의 명령어를 풀어서 설명해보자면, ‘hello, ‘ 와 ARGV[1] 문자열을 결합하여 리턴하며 (루아 스크립트에서.. 는 문자열을 결합하는 명령어다), 키는 0개이며, 인수로는 ‘world’가 입력된다.
이때 ARGV[1]는 1번째 인수인 world가 넣어지며, 따라서 “hello, world” 가 리턴된다.
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> get a
"b"
127.0.0.1:6379> eval "return 'hello, ' .. redis.call('get', KEYS[1]) .. ARGV[1]" 1 a 'aby'
"hello, baby"
그리고 redis.call(’명령어’, ARGV…)를 통해 레디스에 명령을 내릴 수 있는데, 이를 응용해 보면
우선 set a b 명령을 통해 키 a에 값 b를 넣고, 레디스에 get a를 루아스크립트로 해보도록 하자.
루아스크립트 이후의 인수로 1 a ‘aby’로 해뒀는데, 이는 키의 개수가 1개이며, 키는 a, 인수는 ‘aby’라는 의미다.
그러면 redis.call(’get’, KEYS [1]) 는 첫 번째 키인 a가 들어가서 redis.call(’get’, a)가 되어, get a 명령을 수행한다.
또 ARGV[1]은 ‘aby’로 대체되므로, 리턴되는 문자열은 ‘hello, ‘ + ‘b’ + ‘aby’가 되어 ‘hello, baby’가 출력된다.
Spring Data Redis에서 LuaScript 사용하기
-- resources/script.lua
redis.call('set', KEYS[1], 'b')
return 'hello, ' .. redis.call('get', KEYS[1]) .. ARGV[1]
우선 스프링 프로젝트의 resources 디렉터리에 script.lua를 만들어주었다.
외부에서 입력받은 키에 'b'를 넣고, 리턴으로 'hello, ' + 'b' + ARGV[1] 가 되는, 위의 CLI 예제랑 비슷한 예제로 구성해 보았다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
config.setPassword(password);
return new LettuceConnectionFactory(config);
}
@Bean
public StringRedisTemplate stringRedisTemplate() {
return new StringRedisTemplate(redisConnectionFactory());
}
@Bean
public RedisScript<String> script() { // 여기 !!!
Resource script = new ClassPathResource("/script.lua");
return RedisScript.of(script, String.class);
}
}
그다음, 레디스 설정 부분이다. 윗 부분은 다들 아실테니, 맨 밑의 빈을 보면 된다. 위에서 만들어둔 script.lua를 가져오는데, 이때 RedisScript<T>의 T는 스크립트의 리턴 타입이다.
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
@SpringBootTest
public class RedisScriptTest {
@Autowired
private RedisScript<String> script;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void test() {
// given
String key1 = "a";
String argv1 = "aby";
// when
String returnValue = stringRedisTemplate.execute(script, List.of(key1), argv1);
// then
Assertions.assertThat(returnValue).isEqualTo("hello, baby");
}
}
그 다음 테스트를 구성해 보았다.
스크립트를 빈으로 등록해 두어서 주입을 받을 수 있고, 키를 a로 두고 인수를 aby로 두어 위의 CLI 예제랑 똑같이 넣어보았다.
그러면 "hello, baby"가 되어야 한다.
결과는 성공!!
참고 링크
'TIL ✍️' 카테고리의 다른 글
TIL #123 : 배치 작업에는 꼭 정렬 하기 (0) | 2024.11.17 |
---|---|
TIL #122 : 읽기전용/쓰기전용DB를 @Transactional의 readOnly로 구분하기 (0) | 2024.11.16 |
TIL #121 : 스프링 배치 5버전에서 멀티 dataSource 중 하나 지정하기 (0) | 2024.11.16 |
TIL #120 : 멀티 데이터소스 환경에서 이중 커넥션 점유 막기 (1) | 2024.11.15 |
TIL #119 : Jacoco 테스트 커버리지 항목에 롬복이 생성한 코드 무시하기 (0) | 2024.11.13 |