[DB] Optimistic Locking vs Pessimistic Locking
안녕하세요
오늘은 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