본문 바로가기
🏢 DB

RDB 동시성 문제를 해결하는 RDB Lock, Redis 활용방법과 의사결정

by kukim 2022. 12. 14.

이 글은 최상용님의 '재고시스템으로 알아보는 동시성 이슈 해결방법' 인프런 강의 토대로 개인 생각을 덧붙여 작성되었습니다. (공유 허락 o) 원본 저장소는 이곳과 실습한 저장소는 이곳에서 확인하실 수 있습니다. 🙇‍♂️ 잘못된 내용이나 의견 있다면 편하게 말씀해주세요.🙏🏻

 

 

동시성 문제(이슈)란 여러 스레드/프로세스/서버가 동시에 같은(공유) 데이터를 변경하면서 발생하는 문제라고 볼 수 있습니다.

 

예를 들어 하나의 자바 프로세스가 있습니다. 여러 스레드가 동시에 하나의 필드를 수정할 때 동시성 문제가 발생할 수 있습니다.

또 다른 예로 선착순 100명에게 전달해주는 티켓이 있습니다. 이를 구현하는 Spring 서버 여러 대가 존재합니다. 100명 티켓 데이터가 DB에 저장되어 있을 때, 여러 서버가 동시에 수정할 때 동시성 문제가 발생할 수 있습니다.

 

이 글에서는 하나의 자바 프로세스의 동시성 문제가 아닌 여러 Spring 서버가 RDB에 접근할 때 발생하는 동시성 문제를 해결하는 몇 가지 방법에 대해 소개하고자 합니다. (하나의 자바 프로세스 동시성 문제가 궁금하시다면 이전 글 : 자바의 동시성 프로그래밍, 가변 데이터를 동기화하는 3가지 방법(+a. 자바 기본 타입의 원자성에 대하여)을 참고해주세요)

 

목차

1. synchronized 해결과 문제점

 

2. RDB Lock

2.1. Pessimistic Lock(비관적 락) 해결과 장단점

2.2. Optimistic Lock(낙관적 락) 해결과 장단점

2.3. Named Lock 해결과 장단점

 

3. Redis 

3.1. Redis - Lettuce 클라이언트를 사용하여 redis command setnx 활용한 해결과 장단점

3.2. Redis - Redisson 라이브러리를 사용하여 pub/sub을 활용한 해결과 장단점

 

마치며. 무엇을 선택해야 하는가? 

- RDB Lock vs Redis

- RDB Transcation isolation level


 

1. synchronized 해결과 문제점

단일 서버(spring boot)라면 클라이언트들의 요청들이 하나의 서버 자원(Local only)에서 작동하게 됩니다. 따라서 Java의 동시성 프로그래밍을 활용하여 해결할 수 있습니다. 예를 들어 여러 클라이언트들은 요청은 여러 스레드가 되어 동시성 문제가 발생할 수 있습니다. 이때 스레드의 동시성 문제를 막기 위해 메서드에 synchroized 키워드를 사용해 메서드 단위의 을 걸어 동시성 문제를 해결할 수 있습니다. (소스코드 : Solution 1. synchronized)

 

문제점

서버 2대 이상이라면 더 이상 Local 동시성 범주를 벗어나기에 여전히 동시성 문제, race condition이 발생하게 됩니다.

 

해결 방법

여러 서버에 대한 동시성 문제를 해결하기 위해서는 분산락이 필요합니다. 

 

+a) 분산락

분산락이란 여러 서버에서 공유된 자원(데이터)을 동시성 제어하기 위한 기술입니다.

 

아래에서 분산락을 구현하는 두 가지 방법(2. RDB Lock, 3. Redis)을 소개합니다.


2. RDB Lock

2.1. Pessimistic Lock(비관적 락) 해결과 장단점

Pessimistic Lock(비관적 락)은 트랜잭션에서 변경하려는 레코드, row에 대해 락을 거는 방법입니다. (비관적이란 뜻은 여러 트랜잭션이 하나의 row를 변경하는 경우가 많아 이를 비관적이다.로 표현합니다.) JPA를 사용해 비관적락 쓰기(PESSMISTIC_WRITE)를 쓰는 경우 "SELECT ... FOR UPDATE" 으로 락을 걸게 됩니다. 락이 걸린 row는 다른 트랜잭션에서 update, delete 할 수 없습니다. (소스코드 : Solution 2.1: DB 활용 - PessimisticLock)

 

장점

- 충돌이 빈번하게 발생하는 경우 비관적 락이 효과적

- RDB row 자체를 락하기 때문에 데이터 정합성을 보장

 

단점

- RDB row 자체를 락하기 때문에 성능이 안 좋을 수 있다.

2.2. Optimistic Lock(낙관적 락) 해결과 장단점

Optimistic Lock(낙관적 락)은 트랜잭션에 락없이 version이란 column을 만들어 version의 값을 비교하며 동시성을 해결합니다. (낙관적이란 뜻은 트랜잭션 간의 충돌이 매우 적어 낙관적이다.로 표현합니다.) (소스코드 : Solution 2.2: DB 활용 - OptimisticLock)

 

장점

- RDB row에 물리적인 락을 걸지 않기 때문에 락적인 성능이 좋다.

- 충돌이 적은 경우 optimistic Lock이 유리

 

단점

- 만약 업데이트 실패 시 (트랜잭션 충돌 시) 재요청하는 로직을 구현해야 한다.

 

2.3. Named Lock 해결과 장단점

Named Lock, 이름을 가진 메타데이터 락으로 row에 락을 거는 것이 아닌 이름에 lock을 겁니다. MySQL의 경우 GET_LOCK으로 락을 겁니다. 2.1, 2.2 방법과 다른 점은 트랜잭션이 끝날 때 락이 풀리지 않기 때문에 RELEASE_LOCK 으로 락을 해제하거나 타임아웃이 끝나야 풀립니다. 또한 이 과정을 spring boot에서 사용한다면 비즈니스 로직 처리하는 트랜잭션을 제외하고 락을 걸고, 해제하는 쿼리까지 보내야 하기 때문에 커넥션 풀이 많이 필요하게 됩니다. (소스코드 : Solution 2.3: DB 활용 - Named Lock)

 

장점

- Pessmisitic Lock에 비해 타임아웃 구현이 간단합니다.

- 데이터 삽입 시 정합성 보장

 

단점

- 트랜잭션 종료 시 락 해제(별도 세션 관리) 로직이 필요

 

2. 정리

분산락을 구현하는 RDB Lock의 세 가지(비관적 락, 낙관적 락, 이름있는 락)방법에 대해 알아보았습니다.


3. Redis 

Redis를 활용한 분산락 구현 두 가지 방법을 소개합니다.

3.1. Redis - Lettuce 클라이언트를 사용하여 redis command setnx 활용한 해결과 장단점

Redis command인 setnx(set not exist) 명령어를 활용할 수 있습니다. Redis에 key 값이 없으면 데이터에 접근하고, key 값이 있으면 대기하고 다시 key이 있는지 확인합니다. 대기하고 다시 시도하는 방법을 spin lock 방식이라고도 합니다. 이 방법은 RDB Lock의 named lock과 유사합니다. 락을 하고, 해제하고 반복하듯 Redis key가 없는지 있는지 반복합니다. 이 구현을 Spring Boot Redis Client인 Lettuce 을 활용하여 구현합니다. (소스코드 : Solution3.1: Redis 활용 - Lettuce + spin lock)

 

장점

- 세션 관리 별도 불필요

- 비교적 간단한 구현

- RDB Lock보다 빠름

 

단점

- spin lock 방식으로 Redis 자체에 부하가 발생하니 적절한 로직(sleep 주기)이 필요함

- Redis를 관리해야 함

3.2. Redis - Redisson 라이브러리를 사용하여 pub/sub 을 활용한 해결과 장단점

setnx 명령어는 계속해서 key가 있는지 요청하기에 Redis에 부하가 발생할 수 있다고 앞에 말씀드렸습니다. Redis의 pub/sub을 활용하여 분산락을 구현할 수 있습니다. pub/sub을 만들고 그 안에 key를 넣고 먼저 sub 한 트랜잭션이 접근합니다. 이 구현을 Redisson 라이브러리가 구현하고 있어 손쉽게 사용할 수 있습니다. lock 관련 클래스도 제공해주기에 손쉽게 구현할 수 있습니다. (소스코드 : Solution3.2: Redis 활용 - Redisson)

 

장점

- setnx 방법에 비해 레디스 부하가 적음

 

단점

- setnx 구현 방법보다 구현이 복잡

- Redisson 라이브러리 사용해야 함

 

3. 정리

Lettuce + setnx 방식은 구현이 비교적 간단하고 외부 라이브러리를 사용하지 않아도 됩니다. 하지만 spin lock 방식이기 때문에 동시에 많은 트랜잭션이 대기 중이라면 redis에 부하가 갈 수 있습니다.

Redisson pub/sub 방식은 락 획득 재시도를 기본으로 제공하고 pub/sub 방식 구현이기 때문에 redis에 부하가 덜 갑니다. 하지만 별도의 라이브러리를 사용합니다.

 

재시도가 필요하지 않은 경우라면 구현이 간단한 Lettuce + setnx 방식

재시도가 필요한 경우라면 Redisson + pub/sub 방식을 추천합니다.


마치며. 무엇을 선택해야 하는가? 

RDB Lock vs Redis

RDB Lock : RDB가 Lock 트래픽까지 견디고, Redis를 사용할 환경이 안된다면(비용, 운영 ...) RDB Lock 방법도 좋습니다. 하지만 Redis 보다 성능이 좋지 못합니다.

 

Redis : 이미 Redis를 활용 중이라면 RDB Lock 보다 성능이 좋습니다. 하지만 비용이 발생합니다.

 

RDB Transaction isolation level 

트랜잭션 격리 수준을 변경하는 것으로 동시성 문제를 해결할 수 있지 않나? 생각해보았지만 단순한 문제는 아닌 것 같습니다. 비즈니스 로직이 특정 격리 수준과 같을 때는 바꿔도 문제없어 보이지만 특정 기능 구현을 위해 트랜잭션 레벨을 모두 변경한다면 성능이나 여러 수정할 부분이 많아 보이기에 일반적인 해결방법이라고 생각하진 않습니다.


Reference

- 최상용님의 '재고시스템으로 알아보는 동시성 이슈 해결방법

- https://github.com/sangyongchoi/stock-example/blob/main/README.md

- https://github.com/ku-kim/stock-example

- https://stackoverflow.com/questions/129329/optimistic-vs-pessimistic-locking

- https://jeong-pro.tistory.com/241

 

 

 

 

댓글