안녕하세요

트랜잭션 격리 수준이 무엇인지 알고 계신가요?

트랜잭션 격리는 트랜잭션의 특징 ACID 중 I(Isolation)에 해당합니다.

모든 트랜잭션끼리는 격리되어야 한다는 것입니다.

모든 데이터베이스가 ACID를 모두 완벽하게 만족시키면 좋지만 트랜잭션이 완전히 격리가 되면 그만큼 동시처리 능력도 떨어져 성능이 저하됩니다.

따라서 DBMS는 4가지 트랜잭션 격리 수준을 제공하여 상황에 맞게 선택할 수 있도록 하고 있습니다.

트랜잭션 격리 수준은 4가지가 있는데요, 레벨이 높아질수록 격리성(고립성)은 높아지고 동시 처리 능력은 떨어지게 됩니다.

그럼 이제

  • 트랜잭션 격리 수준 4가지
  • 읽기 이상 현상 3가지 - dirty read, non-repeatable read, phantom read
  • non-repeatable read, phantom read의 차이점
  • REPEATABLE READ에서 있던 row가 사라질 수 있나? (대부분의 블로그에 잘못 써있는 사실)
  • SERIALIZABLE 격리 수준과 SELECT ... FOR UPDATE문으로 lock을 거는 것과의 차이는?
  • 그래서 트랜잭션 격리 수준을 어떻게 지정해줄 수 있는데?

등에 대해 알아보겠습니다.

🥑 READ UNCOMMITTED (레벨 0)

READ UNCOMMITTED는 다른 트랜잭션의 커밋되지 않은 데이터에 접근할 수 있는 격리 수준입니다.

이 격리 수준은 심각한 문제가 있기 때문에 일반적인 데이터베이스에서는 거의 사용되지 않습니다.

어떤 문제가 있을지 예시를 통해 알아봅시다.

 

김땡땡의 계좌에는 13만원이 있었습니다.

오늘은 김땡땡의 통신비 5만원이 자동이체 되는 날입니다.

1. 통신사의 출금 시스템에서 김땡땡의 통신비를 출금하는 트랜잭션이 시작되었습니다.

2. UPDATE account SET balance=80000 WHERE user_name="김땡땡";

3. 그리고 김땡땡이 온라인 뱅킹 앱으로 통장 잔고를 확인합니다. READ UNCOMMITTED에서는 커밋되지 않은 데이터를 확인할 수 있기 때문에 김땡땡은 계좌에 8만원이 있는 것을 확인합니다.

4. 그 후 자동이체 트랜잭션이 어떠한 이유로 트랜잭션이 롤백되었습니다.

5. 조금 뒤 김땡땡은 다시 잔고를 확인했는데 계좌의 잔고는 13만원입니다.

DB에서 김땡땡의 잔고가 8만원이 된 적도 없는데 19시 25분 3초에 온라인 뱅킹 잔액 조회 트랜잭션은 커밋되지 않은 데이터인 8만원을 읽어갔습니다. 실제 잔액은 13만원인데 말이죠.

이것이 바로 dirty read입니다.

 

읽기만 하면 그나마 다행인데 만약 이렇게 읽은 8만원을 가지고 UPDATE를 했다면 아주 큰 문제가 발생할 것입니다.

김땡땡의 잔고는 15만원이어야 하는데 10만원이 되는 문제가 발생했습니다.

이처럼 READ UNCOMMITTED 격리수준은 큰 문제를 가지고 있어서 거의 사용되지 않습니다.

🥑 READ COMMITED (레벨 1)

READ COMMITTED는 커밋되지 않은 데이터도 읽어왔던 READ UNCOMMITTED와 달리, 커밋된 데이터만 읽어옵니다.

이 격리 수준은 Orable, PostgreSQL 등 많은 RDBMS의 default 격리 수준입니다.

READ UNCOMMITTED 격리 수준에서 발생했던 문제인 dirty read 문제가 발생하지 않습니다.

트랜잭션이 시작되기 전에 COMMIT된 데이터만 조회할 수 있습니다.

하지만 READ COMMITTED도 문제가 있습니다. non-repeatable read 문제가 발생합니다.

non-repeatable read 문제란 무엇일까요?

 

잔액 조회 트랜잭션에서

SELECT balance FROM account WHERE user_name='김땡땡';

이라는 똑같은 쿼리를 두 번 실행했을 때 첫 번째는 130000을, 두 번째는 80000을 받게 됩니다.

이렇게 같은 트랜잭션 내에서 같은 쿼리를 여러 번 실행했을 때 같은 row의 값이 서로 다른 것을 non-repeatable read라고 합니다.

🥑 REPEATABLE READ (레벨 2)

REPEATABLE READ는 한 트랜잭션 내에서 한 번 읽은 데이터는 바뀌지 않는다는 것을 보장하는 격리 수준입니다.

따라서 READ COMMITTED에서 발생했던 non-repeatable read 문제가 발생하지 않습니다.

REPEATABLE READ는 MySQL(MariaDB)의 default 격리 수준입니다.

별도로 격리 수준을 변경하지 않은 MariaDB에서 격리 수준을 조회해봤습니다. REPEATABLE READ입니다.

REPEATABLE READ 격리 수준에서는 여러 번 읽어도 같은 결과가 나오는 것을 보장하는 것은 아니지만 읽은 row들은 변하지 않는 것을 보장합니다.

그럼 어떤 경우에 같은 결과임을 보장하지 못하나요?

새로운 row가 추가될 때입니다.

읽었던 row들은 변하지 않지만 새로운 row가 추가되면 처음 읽었을 때는 없었던 새로운 row도 읽어옵니다.

따라서 처음 읽었을 때는 row 수가 10개였는데 다시 읽었을 때는 row 수가 11이 되는 현상이 발생할 수 있습니다.

이렇게 유령처럼 없던 row가 생기는 것을 phantom read라고 합니다.

그럼 있던 row가 사라지는 경우도 발생하나요? 🤔

아니요!!!!!

없던 row는 새로 생길 수 있지만 있던 row는 사라지지도, 값이 변경되지도 않습니다.

있던 row가 사라지거나 없던 row가 생긴다고 적은 글들이 많은데 이는 잘못된 정보입니다.

 

대부분의 RDBMS는 REPEATABLE READ 수준에서 phantom read 현상이 발생하지만 MySQL(MariaDB)의 InnoDB 엔진에서는 SNAPSHOT을 이용하기 때문에 phantom read 현상이 발생하지 않는다고 합니다. 

 

🥑 SERIALIZABLE (레벨 3)

SERIALIZABLE 격리 수준은 모든 트랜잭션 작업이 순차적으로 진행되는 격리수준입니다.

따라서 REPEATABLE READ 수준에서 발생했던 Phantom Read 문제가 발생하지 않습니다.

그럼 SELECT ... FOR UPDATE문으로 lock을 거는 것과는 뭐가 다른걸까요?

SELECT ... FOR UPDATE문은 특정 row들에만 lock을 거는 반면 SERIALIZABLE에서는 전체에 lock을 건다는 점입니다.

(물론 RDBMS마다 차이가 있어서 모든 RDBMS가 SERIALIZABLE 수준일 때 전체 lock을 거는 것은 아닙니다.)

 

그럼 트랜잭션 격리 수준을 변경하려면 어떻게 해야 할까요?

MySQL 기준

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

이렇게 하면 트랜잭션 격리 수준을 변경할 수 있습니다.

 

만약 현재 세션에서만 격리 수준을 변경하고 싶다면

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

이렇게 해주면 됩니다.

 

 

참고

https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/transaction-isolation-levels?view=sql-server-ver16 

https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html

https://www.sqlshack.com/dirty-reads-and-the-read-uncommitted-isolation-level

https://www.postgresql.kr/docs/13/transaction-iso.html

https://dotnettutorials.net/lesson/non-repeatable-read-concurrency-problem/

https://nesoy.github.io/articles/2019-05/Database-Transaction-isolation

https://www.zghurskyi.com/lost-update/

https://en.wikipedia.org/wiki/Isolation_(database_systems) 

https://stackoverflow.com/questions/11043712/what-is-the-difference-between-non-repeatable-read-and-phantom-read

https://stackoverflow.com/questions/4034976/difference-between-read-commited-and-repeatable-read-in-sql-server

https://stackoverflow.com/questions/16957638/serializable-transactions-vs-select-for-update

'데이터베이스' 카테고리의 다른 글

[DB] Optimistic Locking vs Pessimistic Locking  (0) 2023.03.13

안녕하세요

오늘은 Optimistic Locking과 Pessimistic Locking에 대해 알아보겠습니다.

 

일단 상황을 가정해보겠습니다.

 

김땡땡이라는 사용자는 1000 포인트가 있었습니다.

김땡땡은 물건을 구매하면서 1000 포인트를 씁니다.

그리고 김땡땡은 며칠 전에 응모했던 이벤트에 당첨이 되어 500포인트를 지급받게 됩니다.

원래 1000포인트가 있었고, 1000 포인트를 쓰고 500 포인트를 받았으니 최종적으로 500포인트가 있어야 합니다.

하지만 김땡땡의 포인트는 1500포인트였습니다.

🤔 왜 이런 일이 생긴걸까요?

Transaction 1과 Transaction 2가 있습니다. (앞으로는 편의상 T1과 T2로 부르겠습니다.)

T1에서는 1000 포인트를 차감하는 일이 수행됩니다.

T2에서는 500 포인트를 증가하는 일이 수행됩니다.

🕐 11:30:05.051

T1이 id가 1인 사용자를 조회했습니다. 이 사용자의 포인트는 1000포인트입니다.

🕑 11:30:05.085

T2가 id가 1인 사용자를 조회했습니다. 이 사용자의 포인트는 1000포인트입니다.

🕒 11:30:05.138

T1이 해당 사용자의 포인트를 1000 포인트 차감합니다. 1000 - 1000 = 0이므로 해당 사용자의 포인트를 0으로 UPDATE 하고 커밋합니다.

🕓11:30:05.376

T2가 해당 사용자의 포인트를 500 포인트 증가시킵니다. 1000 + 500 = 1500이므로 해당 사용자의 포인트를 1500으로 UPDATE 하고 커밋합니다.

 

김땡땡의 포인트는 500 포인트가 있어야 하는데 1500포인트가 된 것입니다.

 

이것이 바로 동시성 문제입니다.

데이터에 Lock을 거는 것은 이러한 동시성 문제를 해결하기 위한 방법 중 하나입니다.

🔓 Optimistic Locking

Optimisitc Locking은 말그대로 낙관적으로 보는 방법입니다.

DB 트랜잭션을 사용하지 않고 Application 단에서 처리를 합니다.

일단 동시에 해당 데이터에 접근하는 경우가 없을 것이라고 생각하고(낙관적이죠?) lock을 걸지 않고 일단 해당 데이터에 접근하여 조회를 하고 UPDATE를 시도합니다. 그 사이에 데이터가 변경되었다면 UPDATE에 실패합니다.

🤔 그렇다면 데이터가 변경되었는지는 어떻게 알 수 있을까요?

테이블에 데이터 변경 확인을 위한 컬럼을 하나 추가하는 것입니다.

수정 시각 timestamp를 남길 수도 있고

수정을 할 때마다 version을 증가시킬 수도 있고

checksum이나 hash를 사용할 수도 있습니다.

만약 이것을 확인했을 때, 조회했을 때와 다르다면 UPDATE에 실패를 하는 것입니다.

🙂 아까 상황에 Optimistic Locking을 적용해봅시다.

저희는 버전을 이용해서 변경 체크를 하도록 하겠습니다.

Optimistic Locking에는 트랜잭션이 없기 때문에 T1과 T2의 T가 Application의 서로 다른 Thread라고 생각하면 될 것 같습니다.

초기 상태는 이렇습니다.

1. T1이 조회를 합니다. version은 0, 포인트는 1000입니다.

2. T2가 조회를 합니다. version은 0, 포인트는 1000입니다.

3. T1이 김땡땡의 포인트를 0으로, version은 1로 UPDATE를 합니다.

UPDATE user SET point=0, version=1 WHERE id=1 AND version = 0

version이 아까 조회했을 때와 동일하게 0이기 때문에 정상적으로 처리가 됩니다.

이제 DB의 상태는 위와 같아졌습니다.

4. T2가 김땡땡의 포인트를 0으로 UPDATE를 합니다.

UPDATE user SET point=1500, version=1 WHERE id=1 AND version = 0

내 버전은 0인데 현재 버전은 1로, 버전이 다릅니다.

id=1이고 version=0인 row가 없기 때문에 UPDATE에 실패를 합니다.

 

아까와 같은 문제가 발생하지 않았습니다.

하지만 이대로라면 500 포인트를 추가하는 로직이 실행되지 않았기 때문에 재시도를 해야 합니다.

 

🔃 재시도

1. T2가 다시 조회를 합니다. version은 1, 포인트는 0입니다.

2. T2가 포인트를 500으로 UPDATE합니다.

UPDATE user SET point=500, version=1 WHERE id=1 AND version = 1

조회했을 때의 version은 1, 지금의 버전도 1로 같으므로 정상적으로 처리가 됩니다.

 

재시도는 마법처럼 자동으로 되는 것이 아니기 때문에 Application에서 실패시 재시도를 하는 로직을 직접 구현을 해주어야 합니다.

재시도시에도 또 충돌이 발생할 수 있기 때문에 그것을 고려하여 로직을 작성해야 합니다. (ex. 몇 번 이상 시도 후 그래도 성공하지 못하면 에러를 응답하는 등)

 

Optimistic Lock은 트랜잭션을 이용하지 않고 Lock을 걸지도 않습니다.

 

🔒 Pessimistic Locking

Pessimistic Locking은 충돌이 발생할 것이라고 비관적으로 생각하고 처리하는 방법입니다. 

Pessimistic Locking이 바로 우리가 흔히 알고 있는 Lock으로, 실제로 데이터에 Exclusive Lock 또는 Shared Lock을 거는 방법입니다.

트랜잭션이 시작할 때 Lock을 걸고 시작합니다.

Lock을 획득하지 못하면 UPDATE를 할 수 없습니다.

다른 트랜잭션이 끝나고 Lock이 해제되면 그 때, Lock을 얻은 후 정상 처리가 됩니다.

🙂 아까 상황에 Pessimistic Locking을 적용해봅시다.

저희는 조회한 포인트를 기반으로 포인트를 수정하기 때문에 Exclusive Lock을 걸어야 합니다.

1. T1이 Lock을 획득하고 조회를 합니다.

SELECT point FROM user where id=1 FOR UPDATE
2. T2가 Lock 획득을 시도하지만 실패합니다.

3. T1이 UPDATE를 하고 COMMIT을 합니다.

4. T2가 Lock 획득을 성공하고 조회 후 UPDATE, COMMIT을 합니다.

 

여러 테이블에 Pessimistic Lock을 거는 경우 데드락이 발생할 수 있습니다.

불필요한 경우에도 Lock을 걸기 때문에 성능이 저하될 수 있습니다.

✅ 정리

충돌이 빈번하게 발생하지 않는 경우 Optimistic Locking을 사용하는 것이 좋습니다.

만약 티켓팅, 수강신청과 같은 상황에서 Optimistic Locking을 사용한다면 재시도 로직이 엄청나게 많이 발생할 것입니다.

Pessimistic Lock은 성능 저하 및 데드락이 발생할 수 있습니다.

 

참고

https://stackoverflow.com/questions/129329/optimistic-vs-pessimistic-locking

https://en.wikipedia.org/wiki/Optimistic_concurrency_control

'데이터베이스' 카테고리의 다른 글

[DB] 트랜잭션 격리 수준에 대해 알아보자  (0) 2023.03.27

+ Recent posts