이 전까지의 레디스는 Redis를 세션 저장소 및 캐시로 사용하는 전형적인 방법 중 하나이다. 이를 통해 세션 및 캐시를 효율적으로 관리하고 Redis의 고성능을 활용할 수 있다.
Redis single thread
레디스는 사용자들이 실행한 명령어들을 이벤트 루프(event loop) 방식으로 처리한다. 즉, 클라이언트가 실행한 명령어들을 Event Queue에 적재하고 싱글 스레드로 하나씩 처리한다. 메모리를 사용하기 때문에 싱글 스레드로 데이터를 빠르게 처리할 수 있다.
하지만 레디스 6.0을 지나면서 레디스는 ThreadedIO 가 추가 되면서 Multi Thread 로 동작한다.
레디스는 싱글 스레드 였기 때문에 대규모 데이터 처리에도 Atomic 연산을 보장했다.
하지만 멀티스레드를 지원하면 Atomic 한 연산을 보장할 수 없다라는 의문이 들 수 있다.
Atomic : 더 이상 쪼개질 수 없는 연산 ex) 예를들어 i++ 은 atomic하지 않다. 왜냐하면 변수 값을읽고 -> 1더하고 -> 덮어쓰기이렇게 3가지 방법으로 나누어지기 때문이다.
Redis 6.0
레디스 6.0 이 추가가 되면서
많은 사람들의 불만이었더 멀티 스레드를 지원하는 ThreadedIO 기능이 추가되었다.
하지만 Atomic 한 연산을 보장하기 위해
- 클라이언트가 전송한 명령을 네트웍으로 읽어서 파싱하는 부분
- 명령이 처리된 결과 메시지를 클라이언트에게 네트웍으로 전달하는 부분
이 부분에만 멀티쓰레드로 동작이 된다.
즉 Atomic 한 연산이 보장된다.
6.0 이전의 레디스 구조
여기서는 하나의 이벤트 루프에서 IO Multiplexing을 이용해서 Read/Write 이벤트를 받아오고, Read 이벤트가 발생하면 네트웍에서 패킷을 읽고, 명령이 완성되면 실행이 된다.
실제로 IO Multiplexing 작업을 하고 나면, 이 이벤트들이 발생한 클라이언트를 다음과 같은 리스트에 저장한다.
레디스의 연산은 기본적으로 atomic한 연산을 보장한다.
하지만 여러 서버에서 레디스를 호출하게 되면 잘못된 값을 보여줄 수 있는데 이러한 문제를 대비하여 Atomic을 보장하는 명령어를 제공한다.
INCR: 키에 저장된 값을 1씩 증가
DECR: 키에 저장된 값을 1씩 감소
INCRBY: 키에 저장된 값을 주어진 값만큼 증가
DECRBY: 키에 저장된 값을 주어진 값만큼 감소
HINCRBY: 해시 필드의 값을 주어진 값만큼 증가.
HINCRBYFLOAT: 해시 필드의 값을 부동 소수점으로 주어진 값만큼 증가.
BITOP: 비트 연산을 수행.
APPEND: 문자열을 키에 추가.
SETNX: 키에 값이 없는 경우에만 값을 설정.
이 명령어 들은 여러 서버가 동시에 명령을 날려도 synchronized 키워드를 쓴 것처럼 명령어가 묶여서 실행되기에 Atomic이 보장됩니다.
이러한 명령어들을 사용했음에도 불구하고 여려 명령어들을 섞어 사용하면 Atomic이 깨는 경우가 존재한다.
이러한 경우를 위해 분산락을 사용하여야 한다 .
분산락이란 여러 서버가(프로세스) 공유 데이터를 제어하기 위한 기술
Redisson 에서는 분산락을 다음과 같이 구현했다
Lock에 타임아웃, 스핀 락을 사용하지 않기, Lua 스크립트
- 1. Lock에 구현된 타임아웃
- 테이블이나 레코드, 데이터베이스 객체가 아닌 사용자가 지정한 문자열에 대해 락을 획득하고 반납하는 잠금으로, 한 세션이 Lock을 획득한다면, 다른 세션은 해당 세션이 Lock을 해제한 이후 획득할 수 있다. Lock에 이름을 지정하여 어플리케이션 단에서 제어가 가능하다.
- 단점은 락이 해제되는 시간, 락 획득, 반납등 로직들을 구현을 해줘야한다.
- 2. 스핀락이란 락을 걸지 못하면 무한 루프를 돌면서 계속 락을 얻으려고 시도하는 동기화 기법이다. 분산락과 다르게 1회성 요청이 아니라 계속 요청을 하기 때문에 서버에 무리가 가게된다.
- Redisson은 락 획득 시 스핀 락 방식이 아닌 pub/sub 방식을 이용한다.
- pub/sub 방식은 락이 해제될 때마다 subscribe중인 클라이언트에게 "이제 락 획득을 시도해도 된다."라는 알림을 보내기 때문에, 클라이언트에서 락 획득을 실패했을 때, redis에 지속적으로 락 획득 요청을 보내는 과정이 사라지고, 이에 따라 부하가 발생하지 않게 된다.
- 또한 Redisson은RLock이라는 락을 위한 인터페이스를 제공한다. 이 인터페이스를 이용하여 비교적 손쉽게 락을 사용할 수 있다.
- 3. Lua Script 를 사용해서 Atomic 연산을 보장한다.
그 중에 Lua Script 은
EVAL 명령어를 통해 사용할 수 있으며, 이 스크립트는 레디스 레벨에서 연산의 Atomic이 보장되기에 안전하게 사용할 수 있다. 혹은
Lua Script를 스프링 resources 밑에 위치 시키고 빈으로 등록시키므로써 사용할 수 있다.
그 중에 Lua Script에 대한 간단한 예제를 설명해 보겠다.
2개의 빈을 등록하려고 한다.
첫 번째는 1을 올리는 IncrScript
두 번째는 넘어온 인자만큼 올라가고 그것을 복사하여 다른 키에 저장하는 것이다.
두개의 스크립트를 만들어준다.
-- incr.lua
redis.call("INCR", KEYS[1])
-- keys 첫번째꺼 ARGV[1] 만큼올림
redis.call('INCRBY', KEYS[1], ARGV[1])
-- 올린가 value에 저장
local value = redis.call('GET', KEYS[1])
-- key[2] 에 복사
redis.call('SET', KEYS[2], value)
-- 출력위해 리턴
return tonumber(value)
그리고 2개의 빈 등록을 해준다.
@Component
@Slf4j
public class RedisLuaConfig {
@Bean
public RedisScript<Long> IncrAndCopyScript() {
Resource script = new ClassPathResource("scripts/incrAndCopy.lua");
log.info("IncrAndCopyScript 등록 : {} ", script);
return RedisScript.of(script, Long.class);
}
@Bean
public RedisScript<Void> IncrScript() {
Resource script = new ClassPathResource("scripts/incr.lua");
log.info("IncrScript 등록 : {} ", script);
return RedisScript.of(script);
}
}
2개의 API 를 만들어준다.
@RestController
public class RedisController {
private final RedisLuaService redisLuaService;
public RedisController(RedisLuaService redisLuaService) {
this.redisLuaService = redisLuaService;
}
@GetMapping("/redisincr")
public String redisincr(String key, boolean isException) {
redisLuaService.incr("a");
return "ok";
}
@GetMapping("/incrAndCopy")
public RedisDTO incrAndCopy(String originkey, String newkey, Integer count) {
return redisLuaService.incrAndCopy("a","b",3);
}
}
첫 번째는 A 를 1 올리는것
두 번째는 A를 3만큼 올리고 A의 Value를 B 에 복사하는것
@Service
@RequiredArgsConstructor
public class RedisLuaService{
private final StringRedisTemplate redisTemplate;
private final RedisScript<Long> incrAndCopyScript;
private final RedisScript<Void> incrScript;
public void incr(String key) {
redisTemplate.execute(incrScript, List.of(key));
}
public RedisDTO incrAndCopy(String originkey, String newkey, Integer count) {
Long value = redisTemplate.execute(incrAndCopyScript, List.of(originkey, newkey), String.valueOf(count));
return new RedisDTO(newkey, String.valueOf(value));
}
}
Service 단을 구현해 줬다.
execute 부분에서 두 번째 인자가 scripts 에서 KEY[?] 이고, 세 번째 인자가 ARGS[?] 로 들어가게 된다.
Integer로 해준 이유는 null 값을 허용해주기 위해서다.
결과 값
https://f-lab.kr/blog/redis-command-for-atomic-operation
댓글