과정을 즐기자

Lock을 이용한 트랜잭션 격리와 MVCC가 나온 이유 본문

Database

Lock을 이용한 트랜잭션 격리와 MVCC가 나온 이유

320Hwany 2023. 10. 2. 16:31

여러 트랜잭션이 동시에 실행된다면 데이터의 무결성에 문제가 생길 수 있으며 여러 트랜잭션을 순차적으로 실행한다면

성능이 좋지 않을 수 있습니다. 트랜잭션의 특징인 ACID 중에서 I인 Isolation level은 여러 트랜잭션을 실행할 때 

트랜잭션끼리 얼마나 격리되어 있는 지를 나타냅니다.

 

트랜잭션의 격리 레벨에는 크게 4가지가 있습니다.

격리 레벨이 높은 순서대로 Serializable, Repeatable Read, Read Commited, Read Uncommited가 있습니다.

Serializable 수준은 동시 처리 성능이 상당히 떨어지기 때문에 거의 사용되지 않으며

Read Uncommited 수준은 데이터의 무결성에 문제가 생길 수 있기 때문에 거의 사용되지 않습니다.

대부분의 RDBMS에서는 Repeatable Read, Read Commited 수준을 기본으로 채택하고 있으며

MySQL, InnoDB 엔진을 사용한다면 Repeatable Read 수준을 기본으로 사용합니다.

Lock을 사용한 격리

여러 트랜잭션을 동시에 실행하더라도 동시성 문제가 발생하지 않도록 Lock을 사용합니다.

트랜잭션 A가 특정 레코드에 Lock을 걸면 트랜잭션 B는 Lock이 해제될 때까지 해당 레코드에 접근하지 못하는 것입니다.

Lock에는 Read Lock(Shared Lock), Write Lock(Exclusive Lock)이 있습니다.

Read Lock은 조회를 할 때 필요한 Lock이고 Write Lock은 레코드를 변경할 때 필요한 Lock입니다.

위 그림과 같이 트랜잭션 A가 Read Lock을 획득했다 하더라도 트랜잭션 B도 Read Lock을 획득할 수 있습니다.

하지만 Read Lock - Write Lock, Write Lock - Write Lock은 하나를 획득하면 다른 하나는 획득할 수 없습니다.

Lock을 사용한다고 해서 항상 데이터의 무결성이 보장되는 것은 아닙니다.

 

만약 트랜잭션 A가 x, y, z 레코드의 값을 읽고 변경해야 할 때 x에 대한 Lock을 획득하고 해제하고 y에 대한 Lock을 획득하고

해제하고 z에 대한 Lock을 획득하고 해제하는 방식으로 진행한다면 다른 트랜잭션 B가 그 사이에 Lock을 획득하지 않은 x, y, z에 

값을 변경한다면 데이터의 무결성을 보장하지 못하는 문제가 있습니다.

 

그래서 2PL(2-Phase-Locking) 개념이 나오는데 트랜잭션의 모든 Locking 작업은 최초의 unlock 작업보다 먼저 수행되도록 

하는 것입니다. 초기의 RDBMS에서는 이러한 방식으로 데이터의 무결성을 보장하였습니다.

하지만 Lock을 사용하는 격리는 동시 처리 성능이 떨어진다는 단점이 존재합니다.

MVCC (Multi-Version Concurrency Control)

MVCC는 Lock을 사용하는 격리의 문제점인 동시 처리 성능 저하를 해결하기 위해 나왔습니다.

MVCC는 여러 트랜잭션이 Write Lock - Write Lock을 동시에 가질 수 없지만 Read 작업의 경우에는 Lock을 획득하지 않고

진행하기 때문에 동시 처리 성능을 높일 수 있는 것입니다.

그렇다면 어떻게 Read 작업에 대해 Lock을 걸지 않고도 진행할 수 있는 것일까요?

Multi-Version 이라는 것이 한 레코드의 값이 여러 버전으로 저장될 수 있다는 것입니다.

특정 트랜잭션이 특정 값을 100에서 200으로 변경했는데 아직 커밋을 하지 않았다면 언두 로그에는 100이라는 값이 저장되어 있고

다른 트랜잭션이 이 시점에 동시에 해당 값을 100으로 읽는 것입니다.

이 과정에서 Lock을 사용하지 않았기 때문에 동시 처리 성능을 높일 수 있는 것입니다.

MVCC의 문제점?

지금까지 MVCC를 사용해서 동시 처리 성능을 높일 수 있다고 이야기 했습니다. 

하지만 MVCC 역시 동시성 문제가 발생할 수 있습니다.

 

예를 들어 10개의 트랜잭션이 동시에 특정 레코드의 값(0)을 읽어서 1 증가 시킨다고 가정해봅시다.

Read Lock은 걸지 않기 때문에 10개의 트랜잭션이 동시에 값 0을 읽고 값 1을 write 하려고 합니다.

그래서 최종 값이 10이 아니라 1이 나오게 됩니다.

원래 최종 값은 10이 되어야 하는데 제대로 된 업데이트가 이뤄지지 않은 것입니다.

 

이 경우에는 Read 하는 시점에 Lock을 사용해야 하며 이를 Locking Read 라고 합니다.

이 부분은 RDBMS에서 지원하는 것이 아니라 개발자가 따로 챙겨줘야 하는 부분입니다.

SELECT ... FOR UPDATE; - Write Lock
SELECT ... FOR SHARE; - Read Lock

위 예시를 다시 적용해보면 10개의 트랜잭션이 동시에 값 0을 읽지 못하고 가장 먼저 0을 읽은 트랜잭션이 1로 write 하고

Write Lock을 해제한 후 다른 트랜잭션이 다시 Write Lock을 획득할 수 있는 것입니다.

정리

Lock을 사용한 격리는 동시 처리 성능에 문제가 있습니다.

동시 처리 성능을 높이기 위해 언두 로그를 이용한 MVCC 방식을 이용하였습니다.

하지만 이 방식은 동시 처리 성능을 높일 수 있지만 동시성 문제가 발생할 수 있기 때문에

개발자가 명시적으로 Lock을 걸어주는 Locking Read 방식을 적절하게 사용해야 합니다.

 

참고한 자료

 

[Database] MVCC(다중 버전 동시성 제어)란?

오늘은 단일 쿼리로는 해결할 수 없는 로직을 처리할 때 필요한 개념인 트랜잭션에 대해 알아보고, Spring에서 어떻게 활용하는지 확인해보도록 하겠습니다. 1. 동시성 제어(Concurrency Control) [ 동시

mangkyu.tistory.com