트랜잭션 격리수준과 락 종류

2024. 4. 29. 03:09데이터베이스

  • 환경: PostgreSQL
  • 목표: 트랜잭션 격리수준과 락 종류에 대해서 알아보고, 예제로 포인트 사용 기능에 대한 데이터베이스 설계와 플로우를 구상 해 보기.
    • 이번 포스트: 트랜잭션 격리수준과 락 종류에 대해서 알아보고
    • 다음 포스트: 예제로 포인트 사용 기능에 대한 데이터베이스 설계와 플로우를 구상 해 보기

 

들어가며

PostgreSQL 환경에서 포인트 거래에 대한 동시성 문제를 해결하기위해 트랜잭션 격리수준에 대해 공부하고 예시 설계를 만들어 보고자 합니다.

이를통해 실제상황에서 더 안정된 데이터베이스 설계 노하우와 버그를 더 잘 발견할 수 있길 기대합니다.

 

트랜잭션 격리수준

PostgreSQL 는 다음 4가지 격리수준을 지원합니다.

  1. Read Uncommitted: 다른 트랜잭션에서 커밋되지 않은 변경사항을 볼 수 있습니다.
  2. Read Committed: 커밋된 데이터만 읽을 수 있습니다. PostgreSQL 의 기본 격리수준
  3. Repeatable Read:  트랜잭션이 시작할 때의 데이터 스냅샷을 사용하여 트랜잭션 동안 일관된 데이터 보기를 제공. 트랜잭션 내의 모든 쿼리가 처음 트랜잭션이 시작될 때의 데이터 상태를 볼 수 있음을 의미. 다른 트랜잭션에 의해 데이터가 변경되어도, 현재 트랜잭션에서는 변경 사항이 반영되지 않음.
  4. Serializable: 격리 수준은 데이터베이스에서 가장 엄격한 트랜잭션 격리 수준. 이 수준에서는 트랜잭션들이 순차적으로 실행되는 것처럼 동작하도록 보장. 즉, 동시에 실행되는 트랜잭션 간에 서로 영향을 주지 않음.

 

Read Uncommitted

Read Uncommited 격리수준은 다른 트랜잭션에서 커밋되지 않은 데이터도 조회할 수 있습니다. 하지만 PostgreSQL 에선 Read Uncommited 를 Committed 로 자동으로 조절
즉 PostgreSQL 에선 Read Unccommitted 의 격리수준을 사용할 수 없음

https://www.postgresql.org/docs/current/transaction-iso.html

 

Read Committed

 

 

-- 트랜잭션 1
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE accounts SET balance = balance - 100 WHERE name = 'Alice';
COMMIT;
-- 트랜잭션 2
BEGIN;
SELECT balance FROM accounts WHERE name = 'Alice';
COMMIT;
view raw index.sql hosted with ❤ by GitHub

 

트랜잭션 1과 2가 거의 동시에 실행되었다고 가정하고 1이 먼저 실행된다고 치면

 

  1. Read Committed 격리수준인 상태에서 트랜잭션 1이 실행된다.
  2. 트랜잭션 1에서 update 문에서 트랜잭션은 name 이 Alice 인 로우에 대한 을 걸고, 이 은 트랜잭션이 완료될 때 까지 유지된다.
    1. name = 'Alice' 인 로우는 Exclusive lock 이 걸림
    2. 이렇게 로우에 걸리게 된 락은 다른 트랜잭션이 해당 로우를 읽거나 수정하려고할때, 이 해제될 때 까지 대기해야한다.
  3. 트랜잭션 2에서 accounts 의 name = `Alice` 로우를 수정하려고 할 때, PostgreSQL 은 먼저 해당 로우가 잠겨있는지 확인한다.
  4. 트랜잭션 1이 아직 커밋되지 않았다면 조회하고자 하는 로우에 Exclusive lock 이 걸려있어 트랜잭션 2는 해당 로우를 조회하지 못하고 대기
  5. 트랜잭션 2는 트랜잭션 1이 커밋/롤백 될 때까지 대기하고, 트랜잭션 1이 완료되면 데이터베이스에 반영되고 이 해제된다.
  6. 이후 트랜잭션 2가 Alice 에 대한 업데이트된 데이터를 볼 수 있게된다.

 

Repeatable Read 

 

-- 트랜잭션 1
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
UPDATE accounts SET balance = balance - 100 WHERE name = 'Alice';
COMMIT;
-- 트랜잭션 2
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE name = 'Alice';
COMMIT;
view raw index.sql hosted with ❤ by GitHub

 

  1. update 명령은 name 이 Alice 인 로우에 대한 락을 걸고, 이 락은 트랜잭션이 완료될 때 까지 유지.
  2. 위와같이 name='Alice' 인 로우에 대해선 Exclusive lock 이 걸리게 됨.
  3. 트랜잭션 2에서 select 요청 시 트랜잭션이 시작될때의 데이터 스냅샷을 보게됨. 따라서 트랜잭션 1의 변경 이후에도 트랜잭션 2는 그 변경을 볼 수 없게되고, balance - 100 하기 전 값이 조회될 수 있음.

의문점과 해답

  • 만약 트랜잭션 1에서 잔액을 -100 하고, 트랜잭션 2에서 -200 할때 두 트랜잭션이 동시에, 하지만 1이 조금 더 빠르게 실행될 경우 기대하는값으론 -300 이 되어야하지만 트랜잭션 2이 스냅샷을 기준으로 -200하면 최종적으로 -200 만 되지 않을까..?
    • 해답: 트랜잭션 2에서도 update 를 하려면 exclusive 락을 기다리고, 락이 풀려 트랜잭션 1 이 데이터베이스에 반영되었을땐 트랜잭션 2도 변경사항이 반영된채로 연산을 수행. 따라서 -300이 됨.
      • 하지만 애플리케이션에서 현재 잔액을 조회하고 "잔액 - 200" 을 직접 구해 넣는다면 최종적으로 -200만 되는 문제가 생길 수 있음.

 

Serializable

-- 트랜잭션 1
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM accounts WHERE name = 'Alice';
UPDATE accounts SET balance = balance - 100 WHERE name = 'Alice';
COMMIT;
-- 트랜잭션 2
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance + 100 WHERE name = 'Alice';
COMMIT;
view raw index.sql hosted with ❤ by GitHub

 

  1. 트랜잭션 1은 name 이 Alice 인 로우를 조회
  2. 트랜잭션 1의 update 문으로 인해 해당 로우에 exclusive lock 을 걸고 잔액을 감소시킴.
  3. 동시간에 트랜잭션 2가 Alice 의 잔액을 업데이트하려고 시도.
  4. 트랜잭션2는 exclusive lock 으로 인해 롤백 혹은 락이 풀릴때까지 대기.
  5. 트랜잭션 1이 커밋되면 변경 사항이 데이터베이스에 반영되고 락이 해제됨. 그제서야 트랜잭션 2가 로우에 접근하여 업데이트를 수행.

의문점과 해답

  • Read committed 도 트랜잭션 2의 update 를 하기전에 트랜잭션 1을 기다리는데 뭐가 다른점일까?
    • Serializable에서는 트랜잭션 1과 트랜잭션 2가 동일한 데이터 항목을 변경하려고 할 때 데이터베이스 시스템이 두 트랜잭션 중 하나를 롤백시킬 수 있음

 

락 종류 (트랜잭션 )

https://www.postgresql.org/docs/current/explicit-locking.html

처음엔 트랜잭션 격리수준에 대해 공부만 해보려고했는데 조사하는 과정에서 의문이 생겼다.

바로 "먼저 실행된 SQL 에서 update 가 아닌 select 를 하는 경우에도 락이 걸려야할까?" 였다.

그래서 조사 해 보았다.

 

우선 락을 구별하는 방법이다.

  • SHARE
    • SHARE 가 들어가면 쓰기에 대한 lock 이다.
  • EXCLUSIVE
    • EXCLUSIVE 가 들어가면 읽기/쓰기 모두 lock 된다고 볼 수 있다.
  • ROW
    • ROW 단위에서의 락. ROW 가 이름에 안들어가면 테이블단위 락이라고 보면 됨

 

락의 종류는 다음과 같다.

 

락종류별로 어떻게 동작하는지 자세하게 쓸 자신이 없어 레퍼런스를 던져두어야겠다..

https://chrisjune-13837.medium.com/db-postgresql-lock-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-57d37ebe057

 

[DB] Postgresql Lock 파헤치기

Read Lock, Write Lock, Race condition 그리고 명시적 락에 대해서 어떻게 동작을 하는지 자세히 알아보겠습니다.

chrisjune-13837.medium.com

 

후기

평소에 단순하게 사용했던 트랜잭션에 대해 더 자세히 알게되었다.

나중에 티케팅같은 빠른 리스폰스가 필요하거나 동시성이 중요한것을 개발할때 트랜잭션 격리수준과 lock 을 활용할 수 있을 듯 하다.

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

[학습노트] MySQL 학습노트  (0) 2024.07.09