반응형

📝 DB 격리 수준

동시에 여러 트랜잭션이 같은 데이터를 읽거나 수정하면 동시성 문제가 발생할 수 있습니다.

 

READ UNCOMMITTED

  • 트랜잭션에서 커밋되지 않은 데이터(Dirty Data)도 읽을 수 있어서 SELECT로 다른 트랜잭션의 미완료 변경 내용을 볼 수 있습니다.
  • 가장 빠르지만 데이터 일관성이 없다.
  • 거의 사용하지 않는 형태
  • Dirty Read (더티 리드) 발생 가능

 

READ COMMITTED

  • 커밋된 데이터만 읽기 가능해 SELECT를 실행할 때마다 가장 최근에 커밋된 값을 읽습니다. (Oracle, SQL Server의 기본 격리 수준)
  • Non-Repeatable Read 발생 가능

 

REPEATABLE READ

  • 트랜잭션 내에서 같은 데이터를 여러 번 조회해도 항상 같은 값 반환합니다. (MySQL의 기본 격리 수준)
  • Phantom Read(팬텀 리드) 발생 가능

 

SERIALIZABLE (직렬화)

  • SELECT도 공유 락을 걸어서 다른 트랜잭션의 INSERT/UPDATE/DELETE 차단 (직렬화 된 것처럼 작동)
  • 락으로 인해 엄청난 대기 시간 발생

 

 
격리 수준 Dirty Read Non-Repeatable ReadPhantom Read 성능
READ UNCOMMITTED 발생 O 발생 O 발생 O 가장 빠름
READ COMMITTED 발생 X 발생 O 발생 O 빠름
REPEATABLE READ 발생 X 발생 X 발생 △ 보통
SERIALIZABLE 발생 X 발생 X 발생 X 가장 느림

 

 

📝DB 동시성 문제

여러 트랜잭션이 동시에 같은 데이터에 접근하여 읽기/수정/삭제 작업을 수행할 때 발생할 수 있는 데이터 불일치나 무결성 문제를 말합니다. 가장 간단한 예를 들자면 상품의 총 개수가 3개인데 5개의 주문이 동시에 들어왔을 때 3개는 성공하고 2개는 실패해야하지만 5개가 다 성공하는 상황이 벌어지게 됩니다.

 

Dirty Read (더티 리드)

T1: UPDATE account SET balance=500 WHERE id=1; (아직 COMMIT 안함)
T2: SELECT balance FROM account WHERE id=1; → 결과 500 읽음
T1: ROLLBACK; (실제 데이터는 1000으로 복구됨)
T2가 읽은 500은 존재하지 않는 값이 됨 → Dirty Read 발생

다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽는 것으로 읽은 데이터가 롤백 될 수 있어 신뢰할 수 없는 값을 알게 됩니다.

READ UNCOMMITTED 격리 수준에서 발생

 

예시) A가 돈을 출금하지만 아직 확정(커밋)하지 않음 → B가 900원을 읽음 (커밋되지 않은 데이터) → A가 ROLLBACK 되어 1000원으로 복원 되지만 B는 900원으로 읽어버림

 

 

Non-Repeatable Read (반복 불가능 읽기)

T1: SELECT balance FROM account WHERE id=1; → 1000
T2: UPDATE account SET balance=500 WHERE id=1; COMMIT
T1: SELECT balance FROM account WHERE id=1; → 500 (값이 바뀜)

같은 트랜잭션 안에서 같은 데이터를 두 번 읽는데 값이 바뀌는 현상으로 다른 트랜잭션이 데이터를 중간에 변경하고 커밋했기 때문이다.

READ COMMITTED 격리 수준에서 발생할 수 있음

 

 

예시) A가 1000원을 읽음 → B가 업데이트 해서 1200원이 됨 → A가 한번 더 조회했더니 1000원이 됨

 

Phantom Read (팬텀 리드)

T1: SELECT * FROM orders WHERE amount > 100; → 결과 10건
T2: INSERT INTO orders (amount=200); COMMIT
T1: SELECT * FROM orders WHERE amount > 100; → 결과 11건

같은 조건으로 두 번 조회했는데 중간에 새로운 row가 생겨 결과 row 수가 달라지는 현상으로 다른 트랜잭션이 데이터 INSERT/DELETE했기 때문이다. (조회 조건에 해당하는 부분 락을 걸어야 해결 됨)

REPEATABLE READ 격리 수준에서 발생할 수 있음 (Serializable에서만 완전 방지)

 

예시) A가 5건 조회 결과를 받음 → B가 새로운 주문 추가 → A가 같은 트랜잭션에서 또 조회를 했을 때 6건이 되는 유령이 생김

 

Lost Update (갱신 손실)

T1: SELECT balance FROM account WHERE id=1; → 1000
T2: SELECT balance FROM account WHERE id=1; → 1000

T1: UPDATE account SET balance=balance-100; (결과 900)
T2: UPDATE account SET balance=balance-50; (결과 950)
둘 다 COMMIT → 최종값 950 (T1의 업데이트는 무시됨)

동시에 두 트랜잭션이 같은 데이터를 갱신하는데, 한 쪽의 결과가 덮어씌워져서 손실되는 현상으로 두 트랜잭션이 충돌을 감지하지 못하고 둘 다 COMMIT했기 때문이다.

낙관적 락/비관적 락을 사용하지 않으면 발생할 수 있음

 

예시) A가 좋아요 100개를 봄 → B도 100개를 봄 → A가 업데이트후 커밋 → B가 업데이트 후 커밋 → 실제 102이여야하지만 B가 A 결과를 덮어써버려서 101개가 되어버림

 

📝비관적 락 vs 낙관적 락 (MVCC)

비관적 락과 낙관적 락의 경우는 동시성 문제를 해결하기 위해서 나오는 기술입니다.

 

  • 비관적락
    • 잠금을 먼저하고 다른 것들이 해당 행을 들어오지 못하게 한다.
  • 낙관적락
    • 잠금을 하지 않고 처리하고 추후에 검증을 해서 처리한다.
  • MVCC
    • 데이터베이스에서 여러 트랜잭션이 동시에 같은 데이터를 읽고 쓸 때, 서로 간섭을 최소화(동시성 처리)하기 위해 데이터의 여러 버전을 관리하는 기술
    • 두개의 트랜잭션이 충돌이 나는 경우 기본적으로 마지막 커밋이 덮어쓰는 방식으로 동작 (마지막 커밋이란 version정보가 올바르게 올라간 것) [version1에서 2개가 읽으면 version2만 제대로 커밋하고 version3는 실패]
    • 대부분 UPDATE에서 발생되는 문제 입니다.
  • 비관적 락 (DB)
    • 데이터를 읽거나 수정하기 전에 잠금을 걸어서 다른 트랜잭션 접근을 막음
    • FOR UPDATE라는 명령어를 통해 직접 SQL에 락을 거는 명령을 줍니다.
      • SELECT * FROM account WHERE id=1 FOR UPDATE
  • 비관적 락 (어플리케이션)
    • JPA를 예를 들면 아래와 같은 코드를 통해 명시해주면 됩니다.
      • Account acc = entityManager.find(Account.class, accountId, LockModeType.PESSIMISTIC_WRITE)
  • 낙관적 락 (DB)
    • 충돌이 거의 없을 것이라 가정하고 데이터에 락을 걸지 않으며 업데이트 시점 버전과 타임스탬프를 비교하여 변경했는지 확인해 롤백 처리합니다.
    • 대부분 DB에서 기본적으로 MVCC기반으로 낙관적 동시성 제어를 합니다.
  • 낙관적 락 (어플리케이션)
    • 기본적으로 DB에 들어가있지만 DB에만 의존하는 경우 마지막 커밋만 덮어씌워지는 규칙이 적용됩니다. JPA의 경우 @Version이라는 어노테이션을 사용해 마지막 커밋만 덮어씌워지는 규칙을 못하게끔 둘다 업데이트를 못하게끔 할 수 있습니다.

 

📝DBMS 격리 수준 및 MVCC

DBMS  격리 수준 MVCC 사용 여부
MySQL REPEATABLE READ ✅ 사용
Oracle READ COMMITTED ✅ 사용
PostgreSQL READ COMMITTED ✅ 사용
SQL Server READ COMMITTED 옵션 (SNAPSHOT 모드 사용 시 MVCC)

 

반응형