programming study/B-redis, rabbitmq

공유 자원에 대한 동시성 처리(ex) 좋아요 기능, 재고 시스템 상품 구매, 주식 매매)

gu9gu 2023. 5. 2. 20:18

redis 분산 락을 사용하는 이유

(1) 토스ㅣSLASH 22 - 애플 한 주가 고객에게 전달 되기까지 - YouTube

상품을 구매하거나 주식을 매매하는 시스템에서는 재고나 잔고와 같은 공유자원을 동시에 갱신하는 트랙잭션이 여러개 발생합니다. 이 때 공유 자원에 대해서 각 사용자의 수정사항이 무시되지 않고 데이터의 일관성을 유지시키고 데드락을 방지하기 위해 안전한 동시성 처리가 필요합니다.

대표적인 방법으로는 디비 락을 걸어서 처리할 수 있습니다.

하지만 성능저하와 데드락이 많이 발생할 수 있고 MSA구조인 경우 각 서비스 모듈 간의 결합도가 올라갈 수 있고 스케일 아웃한 경우에도 각 서버들이 데이터의 일관성을 유지하기 위한 별도 처리가 필요하기 때문에  

다른 방법을 사용합니다.

  • 요청이 몰리는 경우 대상 테이블들에 대해 하나하나 락을 걸어버린다면 성능 저하와 데드락이 많이 발생할 수 있습니다.

( 락을 잡은 스레드가 다른 락을 기다리는 동안 다른 스레드가 락을 잡고 있을 경우 데드락이 발생할 수 있습니다. )

락 방식에서는 보통 락을 위한 별도 테이블을 둬서 트랜잭션 동시성을 제어합니다.?

별도 테이블을 통해 락을 획득하여 트랜잭션을 시작하는 방식입니다. ?

  • MSA 구조여서 각 모듈이 독립적인 데이터베이스를 사용하고 있는 경우 서비스 간의 결합도가 올라갈 수 있고 비효율적인 자원 사용을 하게 될 수 있습니다.
  • 또 하나의 서버가 스케일아웃 하여 여러 서버로 분리되어 있는 경우에도 각 서버가 디비 락 자원을 공유해야 합니다. 이 때, 여러 서버 간에 일관성을 유지하기 위한 처리가 필요하고 서버가 죽거나 재시작 되는 경우에도 일관성을 유지해야 하기 때문에 코드의 복잡도가 상승할 수 있고 구현이 어렵습니다.

Redis 기반 분산락 방식이 있습니다.

공유 자원인 수량을 Redis 에 올려놓고 Redis 기반 분산락을 사용하게 되면 모듈의 데이터베이스에 대한 의존을 없애고 서비스간의 결합도를 낮출 수 있습니다.

또 Redis는 메모리 기반 저장방식이기 때문에 RDBMS에서 락을 처리하는 방식보다 높은 처리량을 제공합니다.

Redis 방식을 사용하는 경우에는 분산락에 대한 적절한 타임아웃을 설정하여 데드락을 피해야 합니다.

추가적으로 분산락 타임아웃이 지나 락 자체는 해제가 되었으나 트랜잭션이 아직 끝나지 않은 경우 다른 트랜잭션에서 자원을 사용하게 돼서 갱신 유실이 발생할 수 있습니다. 

갱신 유실을 방지 방법으로는 원자적 연산 사용, 명시적 잠금, 갱신 손실 자동 감지, Compare-and-set 연산이 있습니다.

 

명시적 잠금 : 여러 테이블을 갱신하는 트랜잭션에 대해 성능 저하가 발생할 수 있습니다.

원자적 연산 사용, 갱신 손실 자동 감지 : DBMS에 의존적이기 때문에 ORM 방식에 적절하지 않습니다.

Compare-and-set 연산 : JPA에서는 @OptimisticLocking 어노테이션을 통해 간단하게 CAS 연산 구현이 가능합니다.

OptimisticLocking을 해석하면 낙관적 락인데, 조회 데이터에 대한 version을 통해 갱신 유실을 방지합니다.

 update를 시도 할 때 조회한 데이터에 대한 버전이 변경됐다면 update가 실패합니다.

 

그렇다면 분산락 없이 @OptimisticLocking 만으로 동시성 제어를 할 수 있을까요?

동시에 발생하는 트랜잭션들은 대기 없이 실패하게 되거나 별도의 재시도 구현이 필요합니다. 트랜잭션 재시도 구현은 재시도 자체의 실패 등  여러 케이스를 고려해야 해서 코드의 복잡성을 상승시킬 수 있기 때문에 적합하지 않습니다.

 

결론적으로는 분산락을 통해 처리량을 높이면서 공유자원에 대한 동시성을 보장하고 @OptimisticLocking 어노테이션을 추가로 적용해서 좀 더 안전하게 동시성 처리를 할 수 있습니다.

 

또한 주요테이블의 경우 hivernate envers를 이용해 변경 히스토리를 저장하여 데이터 흐름을 파악할 수 있습니다.

 

 

Redis 분산 락을 이용하여 동시성 처리 구현

Redisson을 이용한 재고 관리 :: 임민석의 블로그 (tistory.com)

GitHub - Junhan0037/spring-concurrency: 동시성이슈 해결방법

GitHub - Hyune-s-lab/manage-stock-concurrency: 재고시스템으로 알아보는 동시성이슈 해결방법

선물하기 시스템의 상품 재고는 어떻게 관리되어질까? | 우아한형제들 기술블로그 (woowahan.com)

[재고시스템으로 알아보는 동시성이슈 해결방법] 3. Redis Distributed Lock (tistory.com)

쿠폰 재고의 설계 및 개발 | 후덥의 기술블로그 (pkgonan.github.io)

스프링으로 알아보는 동시성 이슈 (velog.io)

Redisson 분산락을 이용한 동시성 제어 (velog.io)

Redis를 활용하여 동시성 문제 해결하기 (velog.io)

선착순 자원을 사용하기 위한 방법! (동시성, Lock, Isolation, ...) (tistory.com)

Springboot 좋아요 기능을 통해 동시성을 알아보자! (velog.io)

동시성을 고려한 조회수 증가 기능 (velog.io)

 

 

1. docker를 window에 설치, redis 를 docker에 설치, redis 실행

https://velog.io/@yoojkim/Redis-local%EC%97%90%EC%84%9C-Redis-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-Docker-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-Redis-server-%EC%A0%91%EC%86%8D

C:\Users\jig>docker pull redis
Using default tag: latest
latest: Pulling from library/redis
9e3ea8720c6d: Pull complete
7bb0a593ef8e: Pull complete
1563ab48b627: Pull complete
e71aa54519a0: Pull complete
e43bffb29bfd: Pull complete
a8fb6ae3ba1b: Pull complete
Digest: sha256:93a5ed8cdfa909a12cb41e5605c147756b804a9fe38376f8afed133f427ea828
Status: Downloaded newer image for redis:latest
docker.io/library/redis:latest

C:\Users\jig>docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
redis        latest    116cad43b6af   4 hours ago   117MB

C:\Users\jig>docker network create redis-net
b10e4f0ab3ffff565b7c3c4047f3367150182571dd60a0d335cdcc052acda28b

C:\Users\jig>docker network ls
NETWORK ID     NAME        DRIVER    SCOPE
f9ca78e896d3   bridge      bridge    local
ba94731fdcc5   host        host      local
a112507468fb   none        null      local
b10e4f0ab3ff   redis-net   bridge    local

C:\Users\jig>docker run --name redis-study -p 6379:6379 --network redis-net -v d:/dev/docker/db/redis -d redis redis-server --appendonly yes
7b375b942efa2fb0f4ff276b5bc7067f4c3be12d08afe7acad7ceb5db661c474

C:\Users\jig>docker run -it --network redis-net --rm redis redis-cli -h redis-study
redis-study:6379>

2.

 

 

 

 


동시성 처리를 위한 여러가지 방법

자바에서 동시성을 해결하는 다양한 방법과 Redis의 분산락 (tistory.com)

synchronyzed

- 자바에서 제공하는 API

- lock을 걸어서 다른 쓰레드에서 접근할 수 없게 하여 멀티 스레드 환경에서 공유 객체를 동기화 하고,-thread-safe하게 구현할 때 사용됩니다.

- 하지만 단점으로 하나의 프로세스에서만 보장되는 방법이라서 분산 시스템에서 공유하는 자원에 대해서는 사용 불가능합니다. 또한 lock  걸어놓은 쓰레드의 상태를 실행 상태로 변경하는데(context switch) 자원을 사용해야 하기 때문에 성능이 안 좋 습니다.

- Atomic type을 사용한다면 non-blocking 방법이기 때문에 성능 저하가 일어나지 않는다.

- 주의사항으로 @Transactional 설정을 해놓으면 프록시 방식으로 aop 동작을 해서 synchronized가 적용되지 않기 때문에 synchronized를 사용할 때는 @Trasactional 어노테이션을 제거해줘야 합니다.

[Java] synchronized 메소드와 일반 메소드로 같은 자원에 접근하면 thread-safe할까? (tistory.com)

자바 동기화 | Synchronized(Monitor), Atomic Type (tistory.com)

 

DB에 LOCK 걸어서 처리

1. 비관적 락 (Pessimistic Lock)

 - 데이터 자체에 락을 거는 방식입니다.

 - 데이터 충돌이 자주 일어날 것으로 예상되는 경우 낙관적 락을 사용할 경우 롤벡을 자주 해줘야 해서 성능이 안 좋을 수 있으므로 비관적 락이 성능이 더 좋을 수 있습니다.

- 서로 자원이 필요한 경우 Dead Lock (교착 상태)에 빠질 가능성이 있습니다?

 

2. 낙관적 락(Optimistic Lock)

- 자원에 미리 락을 걸지 않고 동시성 문제가 발생하면 그 때 가서 처리하는 방식입니다.

- 자원을 조회하고 update 할 때 조회한 데이터의 버전이 변경이 됐는지 확인해서 맞는 경우에만 업데이트를 합니다. 만약 버전이 다른 경우에는 다시 조회 하고 update 할 때 확인하는 과정을 반복합니다.

- 데이터 충돌이 자주 일어나지 않는 경우에 데이터 자체에 락이 걸려있으면 계속해서 context switch를 해줘야 해서 성능이 안 좋아질 수 있기 때문에 낙관적 락을 사용하면 성능을 높일 수 있습니다.

[구현 방법]

- DB 조회시 @Lock(value=LockModeType.OPTIMISTIC)을 적용

public interface StockRepository extends JpaRepository<Stock, Long> {

    @Lock(value = LockModeType.OPTIMISTIC)
    Optional<Stock> findById(Long id);
    
}

- 지속적으로 db 변경을 재 시도 하는 로직을 구현해야 함.

아래 블로그에서 OptimisticLockStockFacade 참고

자바에서 동시성을 해결하는 다양한 방법과 Redis의 분산락 (tistory.com) 

 

3. 네임드 락 (Named Lock)

.....

 

 

Redis

1. Lettuce

Lettuce를 사용하는 경우 별도로 스핀락으로 구현을 해서 사용합니다.

 ( 반복해서 락을 획득하려고 시도하는 방식 )

근데 락이 해제 되는 유효기간을 설정할 수 없고 레드스에 계속해서 요청을 보내기 때문에 레디스에 부하가 많이 갈 수 있는 방식입니다.

 

 - 락이 해제 되는 유효 기간을 설정할 수 없기 때문에 락에서 에러가 발생하는 경우 해제 로직을 수행하지 않아서 다른 어플리케이션이나 쓰레드가 락이 해제되는 것만을 무한정 기다리는 상태가 될 수 있습니다.

 -  일정 시간마다 레디스에게 락의 획득을 시도하기 때문에 레디스에 부하를 줄 수 있습니다.

만약 레디스에 부하를 덜 주기 위해 획득 시도 주기를 늘려서 실제 쓰레드 작업 수행시간보다 길어지는 경우 작업이 다 끝났음에도 획득 시도가 늦어져서 비효율적인 상황이 됩니다.

 

2.Redisson

Redisson 라이브러리를 사용해서 Redis를 사용하는 경우

tryLock() 메서드로 lock을 얻어게 되는데 이 때  lock 대기시간과 유효시간을 설정할 수 있습니다.

waitTime 동안만 기다려서 waitTime이 지나면 lock 획득을 실패했다고 보고 false를 반환합니다.

leaseTime 이 지나면 lock이 해제 돼서 다음 어플리케이션이나 쓰레드가 lock을 획득할 수 있습니다.

publish-subscribe 기능이 지원돼서 별도의 스핀락 로직을 구현하지 않아도 되고 레디스의 부하도 줄일 수 있습니다.

publish-subscribe

 

 

 

 

 

DataBase의 격리수준

[Database] MVCC(다중 버전 동시성 제어)란? - MangKyu's Diary (tistory.com)

[MySQL] MySQL 동시성 처리(2) - 트랜잭션의 고립성 보장을 위한 격리 수준과 MVCC — CHAN-GPT (tistory.com)

[MySQL] 트랜잭션과 격리수준 (tistory.com)

DB의 트랜잭션 격리 수준(isolation level) (velog.io)

[MySQL] - 트랜잭션의 격리 수준(Isolation level) (tistory.com)

DB 동시성 제어 (tistory.com)

 [JPA] @Transactional 과 동시성 (velog.io)

SQLP - 동시성 제어 (tistory.com)

DB 동시성 제어 (tistory.com)

 

 

 

 

 

 

 


참고

 

분산락

- 중앙 락 관리자가 분산시스템에서 각 프로세스의 공유 자원에 대해 락을 관리하는 방식

- 분산시스템에서 각 프로세스의 공유 자원에 대한 동지 접근을 제어하기 위해 사용합니다.

 

스핀락

 - 반복해서 락을 획득하려고 시도하는 방식

 ( 락을 걸려고 할 때 락이 걸려있는지 확인 해서 락이 걸려있지 않다면 락을 걸고 락이 이미 걸려있다면 다시 확인하는 작업을 반복하는 방식 )

 - 단일 시스템에서 멀티쓰레드의 공유 자원에 대한 동시 접근을 제어할 때 사용합니다.

락이란? 분산락, 스핀락의 개념 (tistory.com)

 

 

 

 

///