[Database] 트랜잭션, deadlock

rul9office·2021년 6월 19일
4

2020.12.1 ghost에서 작성한 글로부터 옮김

Transaction

트랜잭션은 작업의 완전성을 보장해주는 것이다. 여러 개의 작업을 하나의 논리적 단위로 묶어 반영과 원상복귀를 조정할 수 있기 위해 사용된다.
논리적인 작업 셋을 모두 완벽하게 처리하거나 또는 처리하지 못할 경우에는 원 상태로 복구하여 작업의 일부만 적용되는 현상이 발생하지 않게 만들어주는 기능이다.
사용자의 입장에서는 작업의 논리적 단위로 이해할 수 있고, 시스템의 입장에서는 데이터들을 접근 또는 변경하는 프로그램의 단위가 된다.

Commit & Rollback

하나의 transaction의 처리 과정이다.
트랜잭션 처리가 이상없이 완료되면 영구적인 반영을 위해 commit 작업을 진행한다. 여기서 오류가 발생하면 aborted(취소) 상태로 트랜잭션의 시작 전 상태로 rollback 작업을 한다. partial committed 상태는 commit 요청이 들어온 상태이고, 이후 commit을 문제없이 수행할 수 있으면 committed 상태로 전이되고 오류가 발생하면 failed 상태가 된다.

트랜잭션의 특성(ACID)

트랜잭션은 다음의 4가지 특성을 만족해야 한다.

원자성(Atomicity)
ALL OR NOTHING을 보장한다. 부분적으로 실행되는것 없이 모두 성공적으로 실행되거나, 전혀 실행되지 않은 채로 남아 있게 된다.

일관성(Consistency)
성공적으로 Transaction이 완료되면 일관적인 DB 상태를 유지한다.
즉, SELECT 하는 시점에서 DB에 변경이 확정된 데이터만 읽어 항상 일관적인 데이터를 조회한다. 일관성에 대한 내용은 읽기 일관성과 관련되어 있다.

격리성(ISOLATION)
Transaction 수행시 다른 Transaction의 작업이 끼어들지 못하도록 보장하는 특징. 트랜잭션이 실행되는 도중에 다른 트랜잭션의 영향을 받아 잘못된 결과를 만들어서는 안된다.

지속성(DURABILITY)
성공적으로 수행된 Transaction은 Database에 영구적으로 반영된다.

트랜잭션을 사용할 때 주의할 점

트랜잭션은 범위를 최소화하여 꼭 필요한 코드에만 적용하는 것이 좋다.
일반적으로 데이터베이스 커넥션은 개수가 제한적인데 각 단위 프로그램이 커넥션을 소유하는 시간이 길어진다면 사용 가능한 여유 커넥션의 개수가 줄어들게 된다.
그러다 어느 순간에는 각 단위 프로그램에서 커넥션을 가져가기 위해 기다려야하는 상황이 발생할 수도 있다.

Transaction update

트랜잭션에서 UPDATE 를 할 경우 읽기 일관성을 유지하기 위해 행레벨에서 Lock을 걸어버리는데, 다른 트랜잭션에서 UPDATE 작업을 할 수 없도록 막는 작업이다.
그 후 다른 트랜잭션에서 SELECT를 할 경우 아직 COMMIT을 하지 않았기 때문에 이전 정보를 UNDO BLOCK에서 조회할 수 있도록 정보를 기록해주고 데이터 UPDATE 작업을 진행한다.
트랜잭션이 최종적으로 끝나면 행 레벨 락을 풀어준다.

Lock이란?

데이터의 일관성을 보장하기 위한 방법이다. Lock과 트랜잭션은 비슷한 개념같지만 Lock은 동시성을 제어하기 위한 기능이고, 트랜잭션은 데이터의 정합성을 보장하기 위한 기능이다.
잠금은 여러 커넥션에서 동시에 동일한 자원을 요청할 경우, 순서대로 한 지점에는 하나의 커넥션만 변경할 수 있게 해주는 역할을 한다.
여기서 자원은 레코드나 테이블을 말한다. 이와는 조금 다르게 트랜잭션은 꼭 여러 개의 변경 작업을 수행하는 쿼리가 조합되었을 때만 의미있는 개념은 아니다.

Lock의 종류

  • Shared Lock (공유 Lock 또는 Read Lock)
    보통 데이터를 읽을 때 사용한다. 원하는 데이터에 lock을 걸었지만 다른 세션에서 읽을 수 있다. 공유Lock을 설정한 경우 추가로 공유Lock을 설정할 수 있지만, 배타적 Lock은 설정할 수 없다. 즉, 내가 보고 있는 데이터는 다른 사용자가 볼 수 있지만, 변경할 수는 없다.
  • Exclusive Lock (배타적 Lock 또는 Write lock)
    보통 데이터를 변경할 때 사용한다. 해당 Lock이 해제되기 전까지는, 다른 공유Lock, 배타적Lock을 설정할 수 없습니다. 즉, 읽기와 쓰기가 불가능하다는 의미이다.

Blocking

블로킹은 Lock들의 경합(Race condition이라고도 한다.)이 발생하여 특정 세션이 작업을 진행하지 못하고 멈춰 선 상태를 의미한다. 공유Lock과 배타적Lock 또는 배타적Lock과 배타적Lock끼리 블로킹이 발생할 수 있다.

이를 해결하는 방법은 Transaction commit 또는 rollback 뿐이다.

경합이 발생할 때, 먼저 Lock을 설정한 트랜젝션을 기다려야하기 때문에, 이런 현상이 반복되면 빠른 서비스를 제공할 수 없다.

교착상태 (Dead Lock)

교착상태란 두 개 이상의 트랜잭션이 특정 자원(테이블 또는 행)의 Lock을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구하면 아무리 기다려도 상황이 바뀌지 않는 상태가 되는데 이를 교착상태라고 한다.

예를 들어 TRANSACTION A는 사원 테이블의 1번 사원에 대한 UPDATE를 하고, TRANSACTION B는 2번 사원에 대한 UPDATE 작업을 하고 있다고 가정하자.
1번 사원의 row는 TRANSACTION A가, 2번 사원의 row는 TRANSACTION B가 LOCK을 하고 있는 상태이다.
두 트랜잭션 모두 끝나지 않은 상태에서 TRANSACTION A가 2번 사원을 UPDATE 시도하고 TRANSACTION B가 1번 사원을 UPDATE하려고 하면 두개의 TRANSACTION 모두 WAITING에 들어가고 Dead Lock (교착상태) 에 빠지게 된다.

그렇다면 이러한 교착 상태를 방지하기 위해서는 어떻게 해야할까?

Oracle의 경우, Dead Lock을 감지하면 한쪽 Transaction을 풀어버린다. TRANSACTION A의 마지막 UPDATE 내용에 오류가 발생되고 COMMIT을 먼저 하도록 유도한다. TRANSACTION B는 아직 WAITING 상태로 남아있고, TRANSACTION A의 COMMIT을 기다리게 된다.

교착 상태의 빈도를 낮추는 방법

  • 트랜잭션을 자주 커밋한다.
  • 정해진 순서로 테이블에 접근한다.
  • 읽기 잠금 획득(SELECT ~ FOR UPDATE)의 사용을 피한다.
  • 한 테이블의 복수 행을 복수의 연결해서 순서 없이 갱신하면 교착 상태가 발생하기 쉽다. 이 경우에는 테이블 단위의 잠금을 획득해 갱신을 직렬화하면 동시성이 떨어지지만 교착 상태를 피할 수 있다.

Issue tracking

오늘 동일한 object 에 대해 DML이 발생하면서 Deadlock이 발생한 이슈가 있었다.
EAI에서 update를 수행하는 도중, 내가 developer로 접속하여 update 프로시저를 수행하면서 해당 이슈가 발생하였다.
코로나로 인해 재택 근무가 활성화되면서 해당 테이블에 대한 사용자의 update 작업이 평소보다 4배 이상 많이 발생한 것도 원인이었다.
우선은 재정산 업무 이후 사용자의 update 작업이 일어날 수 있도록 시간을 정하는 방식으로 해결해보기로 했다.
추가적으로 commit 주기를 짧게 하고 SQL이 빠르게 처리될 수 있도록 where 조건 범위 축소, 유니크 값 추가 등 한 번 더 내부 애플리케이션 로직을 재검토할 예정이다.

참고 링크

profile
Brings a positive attitude, loves challenges, and enjoys sharing knowledge with others.

0개의 댓글