2024. 4. 29. 03:09ㆍ데이터베이스
- 환경: PostgreSQL
- 목표: 트랜잭션 격리수준과 락 종류에 대해서 알아보고, 예제로 포인트 사용 기능에 대한 데이터베이스 설계와 플로우를 구상 해 보기.
- 이번 포스트: 트랜잭션 격리수준과 락 종류에 대해서 알아보고
- 다음 포스트: 예제로 포인트 사용 기능에 대한 데이터베이스 설계와 플로우를 구상 해 보기
들어가며
PostgreSQL 환경에서 포인트 거래에 대한 동시성 문제를 해결하기위해 트랜잭션 격리수준에 대해 공부하고 예시 설계를 만들어 보고자 합니다.
이를통해 실제상황에서 더 안정된 데이터베이스 설계 노하우와 버그를 더 잘 발견할 수 있길 기대합니다.
트랜잭션 격리수준
PostgreSQL 는 다음 4가지 격리수준을 지원합니다.
- Read Uncommitted: 다른 트랜잭션에서 커밋되지 않은 변경사항을 볼 수 있습니다.
- Read Committed: 커밋된 데이터만 읽을 수 있습니다. PostgreSQL 의 기본 격리수준
- Repeatable Read: 트랜잭션이 시작할 때의 데이터 스냅샷을 사용하여 트랜잭션 동안 일관된 데이터 보기를 제공. 트랜잭션 내의 모든 쿼리가 처음 트랜잭션이 시작될 때의 데이터 상태를 볼 수 있음을 의미. 다른 트랜잭션에 의해 데이터가 변경되어도, 현재 트랜잭션에서는 변경 사항이 반영되지 않음.
- 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; |
트랜잭션 1과 2가 거의 동시에 실행되었다고 가정하고 1이 먼저 실행된다고 치면
- Read Committed 격리수준인 상태에서 트랜잭션 1이 실행된다.
- 트랜잭션 1에서 update 문에서 트랜잭션은 name 이 Alice 인 로우에 대한 락을 걸고, 이 락은 트랜잭션이 완료될 때 까지 유지된다.
- name = 'Alice' 인 로우는 Exclusive lock 이 걸림
- 이렇게 로우에 걸리게 된 락은 다른 트랜잭션이 해당 로우를 읽거나 수정하려고할때, 락이 해제될 때 까지 대기해야한다.
- 트랜잭션 2에서 accounts 의 name = `Alice` 로우를 수정하려고 할 때, PostgreSQL 은 먼저 해당 로우가 잠겨있는지 확인한다.
- 트랜잭션 1이 아직 커밋되지 않았다면 조회하고자 하는 로우에 Exclusive lock 이 걸려있어 트랜잭션 2는 해당 로우를 조회하지 못하고 대기
- 트랜잭션 2는 트랜잭션 1이 커밋/롤백 될 때까지 대기하고, 트랜잭션 1이 완료되면 데이터베이스에 반영되고 락이 해제된다.
- 이후 트랜잭션 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; |
- update 명령은 name 이 Alice 인 로우에 대한 락을 걸고, 이 락은 트랜잭션이 완료될 때 까지 유지.
- 위와같이 name='Alice' 인 로우에 대해선 Exclusive lock 이 걸리게 됨.
- 트랜잭션 2에서 select 요청 시 트랜잭션이 시작될때의 데이터 스냅샷을 보게됨. 따라서 트랜잭션 1의 변경 이후에도 트랜잭션 2는 그 변경을 볼 수 없게되고, balance - 100 하기 전 값이 조회될 수 있음.
의문점과 해답
- 만약 트랜잭션 1에서 잔액을 -100 하고, 트랜잭션 2에서 -200 할때 두 트랜잭션이 동시에, 하지만 1이 조금 더 빠르게 실행될 경우 기대하는값으론 -300 이 되어야하지만 트랜잭션 2이 스냅샷을 기준으로 -200하면 최종적으로 -200 만 되지 않을까..?
- 해답: 트랜잭션 2에서도 update 를 하려면 exclusive 락을 기다리고, 락이 풀려 트랜잭션 1 이 데이터베이스에 반영되었을땐 트랜잭션 2도 변경사항이 반영된채로 연산을 수행. 따라서 -300이 됨.
- 하지만 애플리케이션에서 현재 잔액을 조회하고 "잔액 - 200" 을 직접 구해 넣는다면 최종적으로 -200만 되는 문제가 생길 수 있음.
- 해답: 트랜잭션 2에서도 update 를 하려면 exclusive 락을 기다리고, 락이 풀려 트랜잭션 1 이 데이터베이스에 반영되었을땐 트랜잭션 2도 변경사항이 반영된채로 연산을 수행. 따라서 -300이 됨.
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; |
- 트랜잭션 1은 name 이 Alice 인 로우를 조회
- 트랜잭션 1의 update 문으로 인해 해당 로우에 exclusive lock 을 걸고 잔액을 감소시킴.
- 동시간에 트랜잭션 2가 Alice 의 잔액을 업데이트하려고 시도.
- 트랜잭션2는 exclusive lock 으로 인해 롤백 혹은 락이 풀릴때까지 대기.
- 트랜잭션 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 가 이름에 안들어가면 테이블단위 락이라고 보면 됨
락의 종류는 다음과 같다.

락종류별로 어떻게 동작하는지 자세하게 쓸 자신이 없어 레퍼런스를 던져두어야겠다..
[DB] Postgresql Lock 파헤치기
Read Lock, Write Lock, Race condition 그리고 명시적 락에 대해서 어떻게 동작을 하는지 자세히 알아보겠습니다.
chrisjune-13837.medium.com
후기
평소에 단순하게 사용했던 트랜잭션에 대해 더 자세히 알게되었다.
나중에 티케팅같은 빠른 리스폰스가 필요하거나 동시성이 중요한것을 개발할때 트랜잭션 격리수준과 lock 을 활용할 수 있을 듯 하다.
'데이터베이스' 카테고리의 다른 글
[학습노트] MySQL 학습노트 (0) | 2024.07.09 |
---|