O0oO0Oo / Coin

비트코인 프로젝트 리팩토링
1 stars 0 forks source link

feat : implement order cancellation func in trade module #17

Closed O0oO0Oo closed 10 months ago

O0oO0Oo commented 10 months ago

이슈 개요

문제

주문을 취소하기 위해 Redis 에서 삭제를 할 때, 주문처리 과정 중 읽기 전, 처리가 완료된 후 라면 문제가 없지만, 거래 모듈에서 주문을 처리하기 위해 읽는 중, 처리 중 이라면 어떻게 주문을 취소해야 할까.

현재 거래 모듈의 주문 처리 과정은

  1. 현재 가격에 일치하는 주문 읽기
  2. 다른 스레드가 접근 못하게
  3. 주문 처리
  4. 처리된 주문 저장하기 위해 큐에 넣기
  5. 처리가 완료되면 주문 삭제, 락 해제

주문을 취소하려면 유저 모듈에서 다음과 같이 해야한다.

  1. MySQL 에서 주문을 찾아 삭제(MySQL은 soft delete 다)
  2. Redis 에서 처리되지 않도록 주문을 찾아 삭제

1. 락을 기다렸다가 삭제

처리 중인 경우, 처리 전 후로 모두 안전하지만 삭제 요청이 많이 왔을때 처리 시간이 느리면 문제가 될것이다.

2. 확인하고 삭제 유무 판단하기 ✔️

취소하려는 주문이 현재 처리중인 주문인지 확인하고 가능여부 리턴. 이 경우가 가장 이상적이지만. 다음과 같은 문제가 있다

  1. (거래 모듈) 락 획득
  2. (유저 모듈) 삭제 요청, 락이 걸려있는것을 확인, 처리중인 기록이 없음
  3. (거래 모듈) 처리하려는 주문들의 가격과 시간 기록 Redisson 을 사용하고 있기 때문에, tryLock 과 처리중인 가격과 시간에 대해 일괄적으로 처리할 수 있는 방법을 찾아봐야 할 것 같다.
    • 위의 방법을 해결하기 위해 1번과 3번을 루아 스크립트로 결합하여 사용하기

3. 레디스에서 삭제하지 않고 삭제되어야할 정보를 저장

취소할 주문의 정보를 저장하고 있다가, 해당 주문이 읽혀지고 처리 될 때 제외시킨다.

재현 단계

1번 방식을 선택 - [ ] 비동기 방식으로 구현 - 취소 요청이 수락되었습니다. 라고 반환 - [ ] Transactional - [ ] 락을 확인 - [ ] 없다면 삭제 - [ ] 있다면 대기 후 삭제

2번 방식 선택 - 락이 없기 때문에 (거래 모듈) 주문 처리하는데 지장이 없다.

예상 동작

락을 확인하고 없으면 삭제, 있다면 대기 후 삭제한다.

  1. (거래 모듈) 주문 읽기 (락 획득시도, 기록, 주문 읽기)
  2. (유저 모듈) 주문 삭제 (락 확인, 기록 읽기, 삭제 유무)

실제 동작

  1. 스크립트를 작성하여 해결함
  2. script1 락, 기록: 락, 어떤 주문목록을 처리중인지 기록
  3. script2 언락, 기록삭제 : 언락, 기록했던것을 삭제
  4. script3 주문 취소 : 락 유무 확인, 락이 없으면 삭제, 있다면 어떤 주문을 처리중인지 확인하고 처리 가능여부 리턴

추가 정보

아래 댓글에서 말했듯이 RLock 을 사용 안하고 스크립트로 구현한 기능을 사용하면 Redisson 을 사용한 이유, 그리고 MSA 구조로 확장하여 거래 모듈을 2개 이상 운영했을때 문제가 발생할것이다.

O0oO0Oo commented 10 months ago

1번 취소 이유

락을 획득하는 방법을 테스트한 결과 한 종류의 코인에 대해 (유저 모듈) 많은 주문 취소 요청이 왔을 때, (거래 모듈) 처리하기 위해 락 획득 할 수 있는가이다. 레디스를 클러스터로 구성했을때 트래픽이 증가해 수평, 수직 확장을 한다고하더라도 똑같은 문제가 발생하게 된다.

O0oO0Oo commented 10 months ago

2번 방식 채택

lua 스크립트를 작성하고, 변경 사항이 있지만, 주문 처리중에도 삭제는 안전하게 수행된다.

O0oO0Oo commented 10 months ago

2번 방식

기존코드의 수정이 적고 RLock 를 사용할 수 있도록, 프록시나 상속을 통해 해결할지 아니면, Lock 기능 클래스를 만들지.

전자의 방식으로 할 경우 RedissClient 에서는 CommandAsyncExecutor commandExecutor; 를 리플렉션으로 받아야 하며, RedissonLock 에서도 신경써야 할 부분이 많다.

O0oO0Oo commented 10 months ago
// 락 존재 확인
if redis.call('EXISTS', KEYS[1]) then 
    // 기록 존재 확인
    if redis.call('EXISTS', KEYS[2]) then
        // 비교
        if redis.call('HGET', KEYS[2], 'timestamp') < ARGV[1] then
            // ZREM 삭제 -> 
            return redis.call('ZREM', KEYS[3], ARGV[2]) 
        end 
    end 
end 
return false 

// ZREM 삭제

  1. mysql 에는 있고 redis 에 없는 경우 에러발생 return redis.call('ZREM', KEYS[3], ARGV[2])

  2. 아래와 같이 해야하는가 redis.call('ZREM', KEYS[3], ARGV[2]) return true

O0oO0Oo commented 10 months ago

프로젝트 초기에 Redisson 을 도입했던 이유는

멀티스레드로 Redis에서 읽을때, 후에 모듈을 분리하여 Trade 서비스를 여러개 올렸을 때, 동일한 종류의 거래 처리를 막기위해 lock을 사용하려고 했다.

현재는 tryLock() 으로 대기할 필요 없이 바로 넘어가지만, 처음 시스템을 설계했을때는 락을 얻기위해 대기하는것을 가정했기에, 기존의 redis를 방식은 스핀 락, Redissonc 은 pub/sub 방식이라 부하가 적어 도입하게 되었다.

하지만 지금처럼 Redisson 의 lock 기능을 사용하지 않고 스크립트로 구현한 방식이라면, 사용한 이유가 없지않나싶다.