-
[Spring Boot + Kotlin] Redis Luascript 적용프로젝트/redis 2023. 11. 8. 00:01728x90
개요
- luascript에 대한 간단한 개념을 학습
- Kotlin + Spring Boot Application 환경에서 LuaScript 적용
- 동시성제어를 테스트코드를 통해 확인
- luascript 실행 중 예외가 발생하면 어떻게 처리되는지 확인
Redis Luascript란?
LuaScript란 Lua 프로그래밍 언어로 작성된 사용자 지정 스크립트를 Redis 서버 내에서 직접 실행할 수 있는 기능입니다.
Atomic 한 작업으로 Redis 데이터에 대한 복잡한 작업을 수행할 수 있는 강력한 방법입니다.
Transaction, Pipeline 과는 다르게 중간 결과를 조작할 수 있습니다.
예를 들어 Transaction 내부에서는 데이터를 GET해오면 null이 반환됩니다.
중간 결과인 데이터를 통해 조건부로 명령을 실행할 수 있다는 장점이 있습니다.
LuaScript Bean 등록
@Configuration class LuaScriptConfig { @Bean fun stockDeceaseScript(): RedisScript<Long>{ val scriptSource = ClassPathResource("redis-scripts/stockDecrease.lua") return RedisScript.of(scriptSource, Long::class.java) } }
ClassPath의 경로를 불러와 RedisScript 객체를 만들어냅니다.
Spring Boot에서는 src/main/resources/redis-scripts/stockDecrease.lua 파일을 가져오기 위해서는 ClassPathResource를 활용할 수 있습니다.
첫 번째 인자로는 classpath에서 가져온 script를 넣어주고 두 번째 인자는 반환타입으로 Long 타입으로 반환받겠다는 것을 의미합니다.
RedisScript.of 메서드
static <T> RedisScript<T> of(Resource resource, Class<T> resultType) { Assert.notNull(resource, "Resource must not be null"); Assert.notNull(resultType, "ResultType must not be null"); DefaultRedisScript<T> script = new DefaultRedisScript<>(); script.setResultType(resultType); script.setLocation(resource); return script; }
내부 구현을 보면 반환타입과, script의 Localtion를 받아 기본전략인 DefaultRedisScript 클래스로 반환됩니다.
LuaScript 작성
-- HGET 명령어를 활용하여 현재 재고를 가져와 stockCount 변수에 저장합니다. local stockCount = tonumber(redis.call('hget', KEYS[1], ARGV[1])) -- 만약 stockCount가 1보다 작은 경우라면 error를 발생시킵니다. if stockCount < 1 then error("Inventory cannot be less than 0. Please replenish inventory") end -- hincryby 명령어를 활용하여 재고를 -1 감소시킵니다. redis.call('hincrby', KEYS[1], ARGV[1], -1) -- 차감 이후 남아있는 재고를 반환합니다. return stockCount - 1
src/main/resources/redis-scripts/stockDecrease.lua의 경로로 파일을 하나 생성합니다.
luascript에 대한 간단한 설명
- --는 주석을 의미합니다.
- KEYS와 ARGV의 경우 인덱스는 1부터 시작하며 Spring Boot Application으로부터 주입받는 외부 변수를 의미합니다.
해당예제에서는 항상 재고를 -1 시켰지만 외부에서 변수로받아 유연하게 재고를 감소시킬 수 있습니다.
RedisTemplate를 활용하여 LuaScript 호출
@Component class RedisTransactionWithLuaScript( private val stockDecreaseScript: RedisScript<Long>, private val stringRedisTemplate: StringRedisTemplate, ) { fun luaScriptDeceaseStock() { val stockCount = runCatching { stringRedisTemplate.execute( stockDecreaseScript, //resources에 작성된 script listOf(PRODUCT_ITEM_ID_KEY), //keys를 넘긴다 KEY[1]부터 시작 STOCK_ID //args를 넘긴다 ARGV[1]부터 시작 ) }.getOrElse { throw StockEmptyException("재고가 소진되었습니다.") } logger.info { "luaScript 호출 후 남은 재고 = $stockCount" } } companion object { const val PRODUCT_ITEM_ID_KEY = "productItem" const val STOCK_ID = "104" } } class StockEmptyException(message: String): RuntimeException(message)
Bean으로 등록했던 RedisScript를 주입받습니다.
이후에는 KEYS와 ARGV 인자를 넘겨줍니다.
RedisTemplate의 execute 메서드
@Override public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) { return scriptExecutor.execute(script, keys, args); }
script, keys, args를 인자로 받아 scriptExecutor가 이를 대신 수행해 줍니다.
테스트 코드로 동시성제어 검증
@Test fun `luaScript를 활용하여 Atomic 연산을 수행하여 30개의 재고가 존재할 때 50개의 요청을 동시에 보내면 0개의 재고가 남아있어야 한다`(){ val numberOfThreads = 50 val executorService = newFixedThreadPool(numberOfThreads) val latch = CountDownLatch(numberOfThreads) repeat(numberOfThreads) { executorService.submit { try { redisTransactionWithLuaScript.luaScriptDeceaseStock() } finally { latch.countDown() } } } latch.await() val actual = stringRedisTemplate.opsForHash<String, String>().get("productItem", "104") assertEquals(actual, "0") }
재고의 경우에는 이미 30개를 세팅해 준 상태에서 진행하였습니다.
테스트가 잘 통과되는 모습을 확인할 수 있습니다.
runCatching으로 예외를 잡지 않는다면
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR user_script:6: Inventory cannot be less than 0. Please replenish inventory script: 128214c0c509947d73c0c678c4eeb89b40f67309, on @user_script:6.
재고가 0일 때 LuaScript에서 Error를 반환하며 위와 같은 예외가 발생합니다.
만약 LuaScript를 실행하다가 중간에 예외가 나면 나머지 연산들은 반영될까?
redis.call('SET', KEYS[1], 'stringValue') redis.call('DECR', KEYS[1]) redis.call('SET', KEYS[1], 'changeValue')
위와 같은 luaScript를 작성하여 2번째 명령어에서 의도적으로 예외를 발생시켰습니다.
TestCode로 검증해 보면 자연스럽게 예외가 발생합니다.
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range script:
Redis에 결과는?
- stringValue라는 값만 존재하고 changeValue로 변환되지는 않았습니다.
이로써 예외가 발생하기 전까지의 연산만 반영되고 예외가 발생한 이후의 연산은 반영되지 않는 것을 확인할 수 있습니다.
Redis Transaction에서는 예외가 발생해도 연산이 반영되는 것과 차이점이 존재합니다.
참고자료
https://www.baeldung.com/spring-classpath-file-access
https://www.vinsguru.com/redis-lua-script-with-spring-boot/
'프로젝트 > redis' 카테고리의 다른 글
Redis Cluster + Spring Boot + Lettuce client 이론과 세팅 (0) 2024.08.05 분산락을 위한 Redis RedLock 톺아보기 (4) 2024.07.20 [Spring Boot + Kotlin] Redis Transaction 실습 (0) 2023.11.07 Redis Pipeline, Transaction, Lua script 차이 (0) 2023.11.06 공식문서로 알아보는 Redis pipeline (0) 2023.11.05