1. 분산락을 적용하게 된 상황
만약 여러 사용자가 동시에 같은 시간대이거나 겹치는 시간대에 예약 진행 시 어떻게 중복 예약을 막을 수 있을지 문제였다.
해당 예약 테이블은 아래와 같이 생성 되어있다. 하나의 회의 테이블은 이미 예약된 데이터와 시간이 겹치지 않아야만 예약이 가능하다.

동시에 100명의 사용자가 똑같은 (시간 + 장소)를 예약했을 때 8개의 중복 데이터가 발생하였다.

2. MySQL을 이용한 분산락을 구현한 이유
분산락을 구현하는 방법에 대해 검색해보면 Redis를 이용한 방법이 많이 보인다.
하지만 Redis를 이용하게 되면 추가적인 인프라 구축비용이나 유지보수 비용이 발생한다.
따라서, 나는 기존에 사용하고 있던 MySQL에서 제공하는 NAMED LOCK을 이용하여 Lock에 이름을 지정할 수 있기 때문에 해당 Lock의 이름을 이용하여 애플리케이션단에서 제어가 가능하다는 장점을 이용하기로 했다.
3. Named Lock에 대한 간단 설명
Named Lock은 이름을 가진 metadata lock이라고 할 수 있다. 이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션은 이 Lock을 획득할 수 없도록 한다. 데이터 삽입시에 정합성을 맞춰야 하는 경우 사용 가능하다.

✅ 주의할 점으로는 transaction이 종료될 때 lock이 자동으로 해제되지 않는다는 것이다. 별도의 명령어로 해제를 수행해 주거나 선점시간이 끝나야 해제된다.
- 이름과 함께 lock을 획득한다. 해당 lock 은 다른 세션에서 획득 및 해제가 불가능하다.
- mysql에서는 get_lock()을 통해 lock을 얻고, release_lock()이라는 명령어를 통해 named lock을 해제할 수 있다.
- Pessimistic, Optimistic lock은 item에 대해서 lock을 걸었다면 named Lock은 별도 MySQL 서버 메모리 공간에 lock을 건다.
GET_LOCK(name, timeout)
- 입력받은 이름으로 timeout초 동안 잠금 획득을 시도한다.
- 만약에라도 잠금을 획득할 때까지 무한대로 대기하도록 하려면 timeout을 음수로 설정하면 된다.
- 한 session에서 잠금을 유지하고 있는 동안에는 다른 session에서 동일한 이름의 잠금을 획득할 수 없다.
- GET_LOCK()을 이용하여 획득한 잠금은 Transaction이 commit 또는 rollback 되더라도 스스로 해제되지 않는다.
- GET_LOCK의 결과값은 1, 0, null을 반환한다.
- 1 : 잠금을 획득하는데 성공
- 0 : timeout 초 동안 잠금 획득 실패
- null : 잠금 획득 중 에러가 발생 (ex : Out of Memory, 현재 스레드가 강제 종료됨)
- MySQL 5.7 버전 이후부터는 동시에 여러개의 잠금을 획득 가능하고, 잠금 이름 글자수가 60자로 제한되었음을 유의하자.
RELEASE_LOCK(name)
- 입력받은 이름의 잠금을 해제한다.
- RELEASE_LOCK의 결과값은 1, 0, null을 반환한다.
- 1 : 잠금을 성공적으로 해제
- 0 : 잠금이 해제되지는 않았지만, 현재 쓰레드에서 획득한 잠금이 아닌경우
- null : 잠금이 존재하지 않음
- RELEASE_ALL_LOCKS() 명령을 통해 현재 세션에서 유지되고 있는 모든 잠금을 해제하고 해제한 잠금 개수를 반환받을 수 있다.
ETC
- IS_FREE_LOCK(name)
- 입력한 이름에 해당하는 잠금이 획득 가능한지 확인한다.
- 1 : 입력한 이름의 잠금이 없음
- 0 : 입력한 이름의 잠금이 있음
- null: 에러 발생 (ex: 잘못된 인자)
- 입력한 이름에 해당하는 잠금이 획득 가능한지 확인한다.
- IS_USED_LOCK(name)
- 입력한 이름의 잠금이 사용중인지 확인한다.
- 입력받은 이름의 잠금이 존재하면 connection id를 반환하고, 없으면 null을 반환한다.
4. 분산락 구현
1. DataSource 분리
Lock을 얻기 위한 DataSource를 분리함으로써, Lock이 필요하지 않은 요청들에 대해서 커넥션 풀을 보장해줄 수 있다.
왜냐하면, 커넥션 풀만 더 늘려봤자 Lock이 필요한 요청들에게 모두 점유당해버린다면 Lock이 필요하지 않은 요청들은 커넥션 풀에 반환되기 까지 대기해야 하는 문제가 생기므로 ConnectionPool을 분리하는 것이다.

2. 분리된 Named Lock용 DataSource를 이용한 JDBC 구현
✅ JdbcTemplate을 이용하지 않은 이유는?
JdbcTemplate의 특성상 GET_LOCK을 수행하는 쿼리가 실행되고 트랜잭션이 종료되게 되면 poll에서 얻어온 Connection을 반환하게 된다.
1. 그래서 RELEASE_LOCK을 실행할 때 GET_LOCK을 실행할때와 다른 Connection을 얻어오는 문제가 있기에 LOCK을 제대로 반환 못할 수도 있다.
2. 또한 동시에 동일한 Lock 이름으로 요청한 다른 스레드에서 GET_LOCK에서 반환했던 Connection을 획득하여 잠금을 풀어버릴 위험도 존재한다.
따라서 나는 분리된 DataSource를 주입받아 JDBC로 직접 구현하였다. JDBC를 이용해 직접 구현하였기에 Connection을 직접 관리할 수 있어서 GET_LOCK과 RELEASE_LOCK이 모두 동일한 Connection을 사용할 수 있었고 획득한 LOCK을 정상적으로 반환할 수 있게 게 되었다.

executeWithLock() 메서드에서는 @Transactional을 붙이지 않았다. 왜냐하면 Lock을 얻는 부분과 예약 로직을 수행하는 부분에서 사용하는 ConnectionPool을 각기 다르게 하기 위해 @Transactional을 붙이지 않았다.
한번의 호출로 Lock을 얻고, Lock을 해제하는 작업을 하기 위해 예약 비지니스 로직은 Supplier 인터페이스를 이용한 콜백으로 수행되도록 구현했다.
GET_LOCK과 RELEASE_LOCK은 1, 0, null의 값을 반환한다.
따라서 getLock과 releaseLock 메서드에서 반환값이 0, null인 경우를 체크하여 잠금이 해제됐는지까지 확인해야하고, 적절히 ✅ 예외처리를 해주어야 한다.
timeout 시간을 짧게 주었을 경우는 락이 풀려버리는 경우가 발생할 수 있고 시간을 너무 길게 주면 release를 하지 못했을 경우 서비스속도저하로 이어질 수 있다. 일반적으로 2~5초 사이면 적당하다.
5. 동시성 테스트 + TPS 측정
1.Jmeter를 이용한 테스트

운영서버에 대해서 100명이 동시에 중복 예약하는 상황을 테스트했다.
테스트 한 결과 하나의 요청만 성공하고 다른 요청들은 실패함으로써 동시성 처리가 잘되었음을 볼 수 있었다.
2. Junit을 이용한 테스트



위와 같이 100명이 동시에 중복되는 예약을 요청하는 테스트에서 1개의 예약만 DB에 삽입되었음을 볼 수 있었다.
'👨👩👧👦 Project > 📺 KIOSEK' 카테고리의 다른 글
하나의 물리 서버에 React 빌드파일과 Spring Boot Jar Tomcat WAS 배포를 위한 NGINX 설정 (0) | 2023.05.10 |
---|---|
Amd64를 지원하지 않는 Docker Image Error (0) | 2023.03.12 |
ignore uri를 무시하고 JwtTokenFilter를 계속 거치는 버그 (0) | 2023.03.12 |
application.yml 변경사항이 build시에 적용되지 않는 문제 (0) | 2023.03.11 |
자동 반납 처리 및 제재 스케줄링 기능 구현 (0) | 2022.12.07 |
1. 분산락을 적용하게 된 상황
만약 여러 사용자가 동시에 같은 시간대이거나 겹치는 시간대에 예약 진행 시 어떻게 중복 예약을 막을 수 있을지 문제였다.
해당 예약 테이블은 아래와 같이 생성 되어있다. 하나의 회의 테이블은 이미 예약된 데이터와 시간이 겹치지 않아야만 예약이 가능하다.

동시에 100명의 사용자가 똑같은 (시간 + 장소)를 예약했을 때 8개의 중복 데이터가 발생하였다.

2. MySQL을 이용한 분산락을 구현한 이유
분산락을 구현하는 방법에 대해 검색해보면 Redis를 이용한 방법이 많이 보인다.
하지만 Redis를 이용하게 되면 추가적인 인프라 구축비용이나 유지보수 비용이 발생한다.
따라서, 나는 기존에 사용하고 있던 MySQL에서 제공하는 NAMED LOCK을 이용하여 Lock에 이름을 지정할 수 있기 때문에 해당 Lock의 이름을 이용하여 애플리케이션단에서 제어가 가능하다는 장점을 이용하기로 했다.
3. Named Lock에 대한 간단 설명
Named Lock은 이름을 가진 metadata lock이라고 할 수 있다. 이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션은 이 Lock을 획득할 수 없도록 한다. 데이터 삽입시에 정합성을 맞춰야 하는 경우 사용 가능하다.

✅ 주의할 점으로는 transaction이 종료될 때 lock이 자동으로 해제되지 않는다는 것이다. 별도의 명령어로 해제를 수행해 주거나 선점시간이 끝나야 해제된다.
- 이름과 함께 lock을 획득한다. 해당 lock 은 다른 세션에서 획득 및 해제가 불가능하다.
- mysql에서는 get_lock()을 통해 lock을 얻고, release_lock()이라는 명령어를 통해 named lock을 해제할 수 있다.
- Pessimistic, Optimistic lock은 item에 대해서 lock을 걸었다면 named Lock은 별도 MySQL 서버 메모리 공간에 lock을 건다.
GET_LOCK(name, timeout)
- 입력받은 이름으로 timeout초 동안 잠금 획득을 시도한다.
- 만약에라도 잠금을 획득할 때까지 무한대로 대기하도록 하려면 timeout을 음수로 설정하면 된다.
- 한 session에서 잠금을 유지하고 있는 동안에는 다른 session에서 동일한 이름의 잠금을 획득할 수 없다.
- GET_LOCK()을 이용하여 획득한 잠금은 Transaction이 commit 또는 rollback 되더라도 스스로 해제되지 않는다.
- GET_LOCK의 결과값은 1, 0, null을 반환한다.
- 1 : 잠금을 획득하는데 성공
- 0 : timeout 초 동안 잠금 획득 실패
- null : 잠금 획득 중 에러가 발생 (ex : Out of Memory, 현재 스레드가 강제 종료됨)
- MySQL 5.7 버전 이후부터는 동시에 여러개의 잠금을 획득 가능하고, 잠금 이름 글자수가 60자로 제한되었음을 유의하자.
RELEASE_LOCK(name)
- 입력받은 이름의 잠금을 해제한다.
- RELEASE_LOCK의 결과값은 1, 0, null을 반환한다.
- 1 : 잠금을 성공적으로 해제
- 0 : 잠금이 해제되지는 않았지만, 현재 쓰레드에서 획득한 잠금이 아닌경우
- null : 잠금이 존재하지 않음
- RELEASE_ALL_LOCKS() 명령을 통해 현재 세션에서 유지되고 있는 모든 잠금을 해제하고 해제한 잠금 개수를 반환받을 수 있다.
ETC
- IS_FREE_LOCK(name)
- 입력한 이름에 해당하는 잠금이 획득 가능한지 확인한다.
- 1 : 입력한 이름의 잠금이 없음
- 0 : 입력한 이름의 잠금이 있음
- null: 에러 발생 (ex: 잘못된 인자)
- 입력한 이름에 해당하는 잠금이 획득 가능한지 확인한다.
- IS_USED_LOCK(name)
- 입력한 이름의 잠금이 사용중인지 확인한다.
- 입력받은 이름의 잠금이 존재하면 connection id를 반환하고, 없으면 null을 반환한다.
4. 분산락 구현
1. DataSource 분리
Lock을 얻기 위한 DataSource를 분리함으로써, Lock이 필요하지 않은 요청들에 대해서 커넥션 풀을 보장해줄 수 있다.
왜냐하면, 커넥션 풀만 더 늘려봤자 Lock이 필요한 요청들에게 모두 점유당해버린다면 Lock이 필요하지 않은 요청들은 커넥션 풀에 반환되기 까지 대기해야 하는 문제가 생기므로 ConnectionPool을 분리하는 것이다.

2. 분리된 Named Lock용 DataSource를 이용한 JDBC 구현
✅ JdbcTemplate을 이용하지 않은 이유는?
JdbcTemplate의 특성상 GET_LOCK을 수행하는 쿼리가 실행되고 트랜잭션이 종료되게 되면 poll에서 얻어온 Connection을 반환하게 된다.
1. 그래서 RELEASE_LOCK을 실행할 때 GET_LOCK을 실행할때와 다른 Connection을 얻어오는 문제가 있기에 LOCK을 제대로 반환 못할 수도 있다.
2. 또한 동시에 동일한 Lock 이름으로 요청한 다른 스레드에서 GET_LOCK에서 반환했던 Connection을 획득하여 잠금을 풀어버릴 위험도 존재한다.
따라서 나는 분리된 DataSource를 주입받아 JDBC로 직접 구현하였다. JDBC를 이용해 직접 구현하였기에 Connection을 직접 관리할 수 있어서 GET_LOCK과 RELEASE_LOCK이 모두 동일한 Connection을 사용할 수 있었고 획득한 LOCK을 정상적으로 반환할 수 있게 게 되었다.

executeWithLock() 메서드에서는 @Transactional을 붙이지 않았다. 왜냐하면 Lock을 얻는 부분과 예약 로직을 수행하는 부분에서 사용하는 ConnectionPool을 각기 다르게 하기 위해 @Transactional을 붙이지 않았다.
한번의 호출로 Lock을 얻고, Lock을 해제하는 작업을 하기 위해 예약 비지니스 로직은 Supplier 인터페이스를 이용한 콜백으로 수행되도록 구현했다.
GET_LOCK과 RELEASE_LOCK은 1, 0, null의 값을 반환한다.
따라서 getLock과 releaseLock 메서드에서 반환값이 0, null인 경우를 체크하여 잠금이 해제됐는지까지 확인해야하고, 적절히 ✅ 예외처리를 해주어야 한다.
timeout 시간을 짧게 주었을 경우는 락이 풀려버리는 경우가 발생할 수 있고 시간을 너무 길게 주면 release를 하지 못했을 경우 서비스속도저하로 이어질 수 있다. 일반적으로 2~5초 사이면 적당하다.
5. 동시성 테스트 + TPS 측정
1.Jmeter를 이용한 테스트

운영서버에 대해서 100명이 동시에 중복 예약하는 상황을 테스트했다.
테스트 한 결과 하나의 요청만 성공하고 다른 요청들은 실패함으로써 동시성 처리가 잘되었음을 볼 수 있었다.
2. Junit을 이용한 테스트



위와 같이 100명이 동시에 중복되는 예약을 요청하는 테스트에서 1개의 예약만 DB에 삽입되었음을 볼 수 있었다.
'👨👩👧👦 Project > 📺 KIOSEK' 카테고리의 다른 글
하나의 물리 서버에 React 빌드파일과 Spring Boot Jar Tomcat WAS 배포를 위한 NGINX 설정 (0) | 2023.05.10 |
---|---|
Amd64를 지원하지 않는 Docker Image Error (0) | 2023.03.12 |
ignore uri를 무시하고 JwtTokenFilter를 계속 거치는 버그 (0) | 2023.03.12 |
application.yml 변경사항이 build시에 적용되지 않는 문제 (0) | 2023.03.11 |
자동 반납 처리 및 제재 스케줄링 기능 구현 (0) | 2022.12.07 |