RDB의 특징 중 하나는 트랜잭션이다. 흔히 트랜잭션이 ACID 특성을 가진다고 하는데, ACID는 Atomic(원자성), Consistency(일관성), Isolation(고립성) 그리고 Durability(지속성)을 일컫는다. 즉, 여러 SQL문이 로직에 의해 조합되어 실행되는 경우 하나의 단위처럼 취급해야 함을 뜻한다고 해석해도 될 듯 싶다,
ACID에 대한 자세한 설명은 위키를 참조하자.
https://en.wikipedia.org/wiki/ACID
최근 NoSQL중 하나인 Mongo DB도 4.x 버전으로 올라가면서 트랜잭션을 지원하기 시작했고, 트랜잭션이 RDB만의 특징이라고 언급할 수는 없을 것 같다. 시간이 될 때 MongoDB의 트랜잭션은 어떤 특징이 있는지 정리하는 것도 좋을 것 같다.
https://docs.mongodb.com/master/core/transactions/
(나는) 백엔드 로직을 작성할 때 RDB의 Transaction(트랜잭션) 기능을 맹신해 실수하는 경우가 많고, 부끄럽지만 아직 Locking(로킹)을 완전히 이해하지 못 하고 있다. 이번 포스팅을 계기로 트랜잭션과 로킹을 한 단계 더 이해하는 계기가 됐으면 하는 바람이다.
내가 자주 사용하는 MySQL InnoDB 위주로 정리했지만 RDB라면 맥락이 거의 비슷할 것이고, lock에는 여러 종류가 있지만, transaction을 사용할 때 exclusive lock과 shared lock을 어떻게 사용해야 하는지에 대해서만 간단히 정리했다. 하지만 그 외의 lock도 반드시 정리할 필요가 있다.
잘못된 Transaction(트랜잭션)
다음과 같이 account(계좌) money(현금)을 withdraw(인출)하는 로직을 살펴보자.
로직을 간단히 설명하면 다음과 같다.
accountMoney(계좌 금액) < withdrawithdrawalAmount(인출 금액) 인 경우 Transaction(이하 tx)를 rollback하고,
그렇지 않은 경우 account의 money를 withdrawal 한 만 큼 빼고, commit 한다.
function withdraw(accountId, withdrawalAmount) {
tx = startTransaction();
accountMoney = tx.query('SELECT money FROM account WHERE id = { accountId }');
if (accountMoney < withdrawalAmount) {
tx.rollback();
throw Error('잔고가 부족합니다!');
}
tx.query('UPDATE account SET money = money - { withdrawalAmount } WHERE id = { accountId }');
tx.commit();
}
겉보기에는 `SELECT` 문과, `UPDATE` 문이 transaction에 묶여 있어서 이상이 없어 보이지만, 다음과 같이 TX1과 TX2가 동시에 처리되는 경우를 생각해보자.
현재 account의 money에는 1000원이 있고, TX1은 1000원을 인출하는 경우, TX2는 500원을 인출하는 경우이다.
TX1 |
TX2 |
START TRANSACTION; SELECT money FROM account WHERE id = 1; (1) |
|
|
START TRANSACTION; SELECT money FROM account WHERE id = 1; (2) UPDATE account SET money= money - 500 WHERE id = 1; COMMIT; (3) |
UPDATE account SET money = money-1000 WHERE id = 1; (4) COMMIT; (5) |
|
(1) TX1이 money를 조회했을 때 1000원이 남아 있다.
(2) TX2도 1000원이 남아 있음을 확인했다.
(3) Money를 1000-500 = 500 을 수행하고 commit 했다. 따라서 이 시점에서 Money는 500이다.
(4) 1000 - 1000 = 0 을 수행했다 (?)
(5) 따라서 이 시점에서 Money는 0 이다 (?)
(1)~(3) 은 맞지만 (4), (5)는 틀리다. (4)의 시점에서 이미 money는 500이 되었고, (1)에서 확인한 값과 다른 money-1000을 수행해서, (5)에서는 -500이 남는다!
이런식으로 트랜잭션을 맹신해서는 아니된다!
해결책
위와 같이 경우(`SELECT` 후 `UPDATE` 하는 경우) 로직이 일관성이 유지되게 하기 위해서는 `exclusive lock`이 필요하다. MySQL에서 `exclusive lock`은 `SELECT ~~~ FOR UPDATE`를 사용한다. 그러면 T1이 수행되는 동안 T2가 시작되는 것을 막을 수 있다.
위 로직상 T1이 수행된 후 잔고가 0원이므로, T2에서 `SELECT` 한 후 잔고가 부족하다는 에러를 던지면서 `rollback` 할 것이다.
T1 |
T2 |
START TRANSACTION; SELECT money FROM account WHERE id = 1 FOR UPDATE; |
|
|
START TRANSACTION; SELECT money FROM account WHERE id = 1 FOR UPDATE; UPDATE account SET money= money - 500 WHERE id = 1; COMMIT; |
UPDATE account SET money = money-1000 WHERE id = 1' COMMIT; |
|
잘못된 해결책
`exclusive lock`대신 `shared lock`을 사용하면 어떻게 될까?
(1) s-lock을 얻는다. 이 lock은 T1이 commit 될 때 해제된다.
(2) x-lock을 얻기 위해 (1) 에서 얻어진 s-lock 이 끝나기를 기다린다.
(3) 마찬가지로 x-lock을 얻기위해 기다려야 하는데, (2)와 (3)이 동시에 id=1 에 대해 x-lock을 얻기 위해 기다리게 되므로 dead lock이 발생한다.
T1 |
T2 |
START TRANSACTION; SELECT money FROM account WHERE id = 1 LOCK IN SHARE MODE; (1) |
|
|
START TRANSACTION SELECT money FROM account WHERE id = 1 LOCK IN SHARE MODE; UPDATE account SET money= money - 500 WHERE id = 1; (2) COMMIT; |
UPDATE account SET money = money-1000 WHERE id = 1 (3) COMMIT; |
|
이와 같이 transaction과 lock를 같이 사용할 때 lock의 특징을 제대로 이해해야 한다.
Lock
MySQL InnoDB에는 앞으로 소개할 `s-lock(Shared Lock)`, `x-lock(eXclusive Lock)` 외에 더 많은 종류가 있지만 기본적으로 이 둘을 우선 이해하는 것이 필요해서 이번에는 이 둘만 정리하고 다음 포스팅 때 Lock 종류만 자세히 따로 정리할 예정이다.
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
s-lock(Shared Lock)
read 작업을 동시에 할 때 사용한다. write 작업은 할 수 없다. 다시 말해, s-lock에는 여러 s-lock 이 걸릴 수 있지만 s-lock이 걸린 상태에서는 x-lock이 걸릴 수 없다.
x-lock(eXclusive Lock)
x-lock이 걸리면 다른 lock을 모두 배척한다. 즉, x-lock은 한 write 작업만 가능하고, 다른 트랜잭션이 read를 결코 할 수 없다.
s-lock, x-lock에 대해 어떤 stackoverflow 에서 쉽게 설명하고 있어서 한글로 간단히 아래에 정리했다.
교실에 칠판, 선생님, 학생들이 있을 때, 칠판은 lock 될 수 있는 대상(객체), 선생님은 writer, 학생들은 다수의 readers 라고 볼 수 있다.
1. exclusive lock 상황
- 선생님이 칠판에 무언가를 쓸 때, 여러 학생들은 선생님 등에 가려서 볼 수 없다.
→ exclusive lock이 걸린 경우 shared lock이 걸릴 수 없다.
- 다른 선생님이 와서 동시에 같은 칠판에 글을 쓸 수 없을 뿐더러 쓰게 되면 칠판의 내용이 혼란스러워진다.
→ exclusive lock이 걸린 경우 exclusive lock을 동시에 걸 수 없다.
2. shared lock 상황
- 선생님이 판서를 마치고 옆으로 비켜있는 때 여러 학생들은 동시에 칠판을 읽고 노트에 필기할 수 있다.
→ shared lock은 동시에 여러 개 얻을 수 있다.
- 선생님이 다음 내용을 판서하기 위해 학생들이 모두 필기하기를 기다린다.
→ shared lock이 한 개 이상 존재하면, exclusive lock을 걸 수 없다.
결론
트랜잭션만으로 쿼리의 조합이 결코 일관성을 가져다 줄 것이라 맹신해서는 안되며, 일관성을 갖기 위해서는 적절히 lock을 사용해야 한다. 특히 `SELECT`, `UPDATE` 문에서 말이다. 하지만, lock을 사용하게 되면 논리적인 일관성은 얻게되나, 잘 못 사용하게 되면 나쁜 성능을 내게 된다. 따라서 lock의 종류와 성질을 정확히 이해하고 사용할 필요가 있다.
기초가 되는 x-lock, s-lock에 대해 정말 간략히 알아봤고, lock에 대해서는 더 정리가 필요하다.
언젠가는 mongo db의 트랜잭션 & 로킹과 mysql의 그것들과 비교해는 글을 포스팅해보는 것도 좋을 것 같다.
'MySQL' 카테고리의 다른 글
MySQL innodb_flush_log_at_trx_commit 옵션 (0) | 2023.03.03 |
---|---|
Clustered Index와 Secondary Index (0) | 2021.01.28 |