-
락이 필요한 경우 어떻게 처리할까 고민분석과탐구 2023. 6. 9. 07:03
락이 필요한 경우 어떻게 처리할까 고민
데이터베이스에서 락을 쓰면 실수할 여지가 꽤 있는데, 문제가 발생했을 때 데드락 같이 여파가 큰 문제가 발생한다. 격리 수준에 따른 락의 차이를 고려하면서, 데드락을 방지하기 위해 테이블의 접근 순서는 이래야 하는 등의 일은 너무나도 실수가 발생할 여지가 크다. 그런데, 락은 필요하다. 상품의 재고, 티켓팅, 주문 처리 등등 모두 한 번에 하나의 실행흐름이 접근해서 처리해야 하기 때문이다. 그래서 어떻게 처리하나 찾아보니, 인프런에서 좋은 답변을 찾았다.
인프런의 질문답변 게시판에서 얻은 힌트
요약하자면 쇼핑몰 같은 시스템에서 락이 필요해 보이는데, 어떻게 해야 하냐는 것이다. 답변을 일부 발췌하였다.
락을 거는 것은 요즘 같은 실시간 애플리케이션에서는 여러 가지 위험이 많습니다.(데드락 문제 등등) 그래서 각 데이터베이스 특성에 맞는 락 사용 방법을 명확하게 이해하고, 또 락이 걸린 로우를 조회했을 때 어떤 예외가 발생하는지 등등 다양한 테스트를 꼭 해보아야 합니다. 저는 가급적이면 락을 거는 방법은 피하고, 낙관적 락이나, CAS 스타일의 방식, 또는 RDB나 REDIS 등을 사용해서 해당 주문번호를 한 번에 한명한 변경할 수 있도록 로직의 입구에서 막는 방법을 선호합니다.
위 답변을 바탕으로 Redis를 사용한 접근 방법을 생각해보았다. 우선 위 답변에서 키워드들을 보자.
낙관적 락
낙관적 락은 디비의 락을 사용하지 않고, 각 row에 대한 변화 정보(1씩 증가하는 숫자)를 저장해 두고 데이터를 가져올 때 버전을 저장했다가 데이터를 수정할 때 해당 버전을 이용하여 버전이 같으면 업데이트하는 식의 접근을 말한다. 만약 버전이 다르다면 다른 곳에서 해당 데이터를 수정한 것이니, 현재 진행중이던 업데이트를 중단하는 것이다. 업데이트를 중단한 후에 다시 하던 로직을 진행할 것인지 아니면 다른 로직을 진행할 것인지 정할 필요가 있다.
CAS(Compare and swap)
CAS는 Compare and swap으로 어떤 데이터의 값과 그 데이터의 현재 값을 비교하여 같으면 새로운 값으로 대체하고 아니면 교체하지 않는 알고리즘이다. 낙관적 락과 어떤 차이가 있을까. 그것은 비교 후 대체라는 연산이 하나의 atomic operation으로 이루어진다는 점이다. atomic operation은 여러 실행 흐름에서 동시에 실행해도 race가 발생하지 않도록 보장된 동작이다.
C스타일 코드를 적으면 다음과 같다. p에 접근하여 값을 교체할 수 있는 것은 한 번에 하나의 스레드이다. 여러 스레드에서 cas 함수를 동시에 실행하더라도 하나만이 성공한다.
function cas(int* p, int old, int new) { if(*p == old) { *p = new; return true; } return false; }
CAS를 서버와 디비에서 사용하려면, 이 둘을 모두 포괄할 수 있는 atomic operation이 필요하다. atomic operation하지 않은 CAS라면 낙관적 락이랑 별 차이는 없다.
Redis
마지막으로 Redis를 사용하는 처리다. Redis는 싱글 스레드로 구현된 인메모리 데이터베이스 겸 메세지 브로커이다. 여기서 중요한 점은 싱글 스레드라는 점이다. 외부에서 다량의 요청이 들어왔을 때 하나하나씩 처리한다는 점이 왜 중요할까.
앞서서 정의했던 문제 상황을 다시 떠올려보자. 서비스를 운영할 때 락이 필요한 상황은 분명하게 존재하는데, 데이터베이스의 격리 수준에 따른 락 접근 법을 고려하면서 데드락 상황을 막기 위한 처리 등을 모두 고려하다보면, 실수할 여지가 너무나 많다는 점이었다. 이 문제의 기저에는 데이터베이스에 동시에 접근해서 처리한다는 것이다. 데이터베이스가 싱글 스레드이고 한 번에 하나의 요청만을 처리한다면, 더 이상 락을 신경 쓸 필요가 없어질 수 있다.
적어도 락이 필요한 순간의 연산은 레디스에 접근해서 처리하는 접근 방향은 어떨까.
Redis를 이용하여 락이 필요한 상황을 해소
상황을 하나 가정해 보자. 주문을 처리하는 과정이다. 주문은 소비자가 결제를 마치면 주문완료 상태가 될 것이고, 접수대기상태에서 접수완료 혹은 주문취소로 변할 것이다.
이때 문제가 발생할 수 있는 지점은 접수대기 상태의 주문에 대하여 접수완료와 주문취소 요청이 동시에 발생할 수 있다는 점이다. 사장님은 접수완료 누르고 주문을 진행중인데, 소비자는 주문취소가 안내되는 상황이 나올 수도 있는 것이다. 이 문제를 레디스를 이용하면 어떻게 원만하게 처리할 수 있을까?
다음과 같이 접근하면 될 것 같다.
- 주문완료 하면 주문 정보를 MySQL과 Redis에 삽입한다. MySQL에는 주문ID, 주문상태(대기), 그리고 주문에 필요한 각종정보들을 추가한다. Redis에는 order라는 key를 가진 집합에 주문ID를 추가한다.
- 접수완료, 취소 핸들러(서버에서 요청을 받아 처리를 진행하는 스레드일 것)는 오로지 Redis만을 바라본다. 그리고 다음 연산을 진행한다. 핸들러의 관점이다.
- 요청으로 받은 주문ID를 이용하여 해당 주문 ID가 있는지 확인. 필요한 명령어 SISMEMBER order 주문ID. 주문이 있다면 1을 리턴하고 없다면 0을 리턴한다. 여기까진 여러 핸들러가 다수여도 문제없다.
- 리턴 0이라면, 처리 중인 주문이거나 처리가 완료된 상태이니 사용자에게 주문 상태를 새로 받을 수 있도록 한다. 역시 핸들러가 다수여도 문제없다.
- 리턴 1이라면, 주문을 삭제한다. 필요한 명령어 SREM order 주문ID. 리턴값이 삭제 갯수이이다. 따라서 0이라면 지금 처리하는 핸들러보다 먼저 처리를 시도한 핸들러가 있다는 소리이므로 2로 돌아간다. 1이라면 현재 핸들러가 처리를 담당하게 된다. 다수의 핸들러가 접근했지만 문제없다.
- 완료라면 완료 로직을, 취소라면 취소 로직을 진행한다. 그리고 MySQL의 주문상태를 변경한다. 여기도 문제없다.
보완할점
위의 접근 방식을 살펴봤을 때 허점이 발생할 수 있는 지점은 다음과 같다.
- 핸들러가 Redis에서 주문을 삭제했는데, 핸들러에 문제가 발생하여 종료되었고 Redis에는 데이터가 없는 상태.
- Redis하나로는 처리량이 낮는 상황이 발생한다면, 어떻게 할 것인가?
- MySQL 하나만 쓰는 상황에서 Redis가 추가 되었다. 추가 비용은 얼마나 발생할까.
참고
- 레디스 문서
- 인프런 질문답변: 강사님 락관련 질문이 있습니다.
'분석과탐구' 카테고리의 다른 글
객체지향 프로그래밍 (0) 2023.10.22 JWT와 세션 (0) 2023.07.22 도커로 MySQL 다루기 (0) 2023.06.09 도커를 이용하여 PHP 개발 환경 설정하기 (0) 2023.05.03 CDN은 어떻게 저비용으로 콘텐츠를 전달하나 (0) 2023.05.03