DB, 트랜잭션과 트랜잭션 간 경쟁, 격리상태

infoqoch·2021년 6월 27일
0

Mysql, MariaDB

목록 보기
2/5

트랜잭션이란?

  • 트랜잭션이란 분리할 수 없는(atomic) 하나의 행위를 의미한다. 양치라는 과정을 생각하면, 칫솔을 치약에 묻히고, 칫솔로 이를 닦은 후, 물로 입과 칫솔을 행군다. 만약 이 과정 중 치약을 칫솔에 묻히는 과정을 생략하면 문제가 심각할 것임은 자명하다. 그럼 우리는 양치란 과정을 멈추고, 칫솔과 입을 행궈, 당시 수행했던 양치의 과정을 없던 것으로 할 것이다. 그리고 다시 칫솔에 치약을 묻히는 양치의 과정을 새로 시작할 것이다.
  • DB와의 통신과정도 위와 유사하다. DB와의 통신과정에서 하나라도 빠지면 데이타의 완전성에 훼손이 되는 하나의 작업의 묶음이 존재할 것이다. 이런 분해할 수 없는 하나의 작업을 트랜잭션이라 한다.
  • 트랜잭션의 작업 중 어떤 것을 실패할 경우, 그 과정이란 atomic하기 때문에, 해당 트랜잭션에 묶여있는 그 모든 작업을 취소하고 처음으로 돌아간다. 이를 rollback이라 한다. DB는 이 과정을 자동으로 수행한다. 예를 들면 우리가 구조가 복잡한 자전거를 분해했다가, 다시 조립을 시도하려고 했을 때, 분해의 과정을 까먹어서 되돌릴 수 없다고 상상해보자. 아, 끔찍하다. DB는 롤백을 통해 과거로 돌리는 것을 보장한다.
  • rollback할 필요가 없이 하나의 트랜잭션이 완료가 되면 commit을 한다. 이때 commit을 할 경우 결코 과거로 돌아갈 수 없이 영구적으로 durability 데이타베이스에 저장된다.

트랜잭션 간 경쟁과 격리수준이란?

  • 오늘 블로그에 정리할 내용은 트랜잭션의 다양한 특징 중 하나인 격리수준 lsolation 이다.
  • 앞서의 rollback, commit, 트랜잭션의 과정은 개발 과정에서 체화하기 때문에 이해하는데 어려움이 없었다.
  • 하지만 두 개 이상의 트랜잭션이 경쟁하는 것을 이해하기란 쉽지 않다. 초보 개발자의 입장에서 백엔드에 트랜잭션을 구성하는 것이 쉽지 않고, 트랜잭션 간 경쟁 상태가 놓이는 조건을 만드는 것도 난망하기 때문이다.
  • 그러므로 트랜잭션 간 경쟁과 격리수준에 대해 정리코자 한다.

트랜잭션 간 격리수준을 경험하기

  • 트랜잭션의 격리수준은 아래의 링크를 참고하자. 예제가 매우 잘 되어 있기 때문에 굳이 현 블로그에서 정리하지 않겠다. 개인적으로 트랜잭션에 대한 블로그의 글을 읽는 것보다, 한 번 아래의 코드를 따라하는 것이 더 큰 도움이 되었다.
  • https://jupiny.com/2018/11/30/mysql-transaction-isolation-levels/
  • 참고로, 트랜잭션을 분리하는 방법은 dbeaver 등 툴에서, sql 페이지를 두 개 생성하고 각각의 페이지에서 트랜잭션을 시작하면 된다. 이를 통해 트랜잭션을 분리할 수 있다.

트랜잭션의 격리수준

  • 앞서의 공유한 블로그의 예제를 따라하면, 트랜잭션의 격리수준이 어떤 차이를 가지는지 알 수 있다.
  • 격리수준의 단계를 나타내는 단어는 DB 간 공유하지만, 그 단계가 의미하는 실제 내용은 DB 마다 차이를 가진다. 현재 블로그는 mysql을 기준으로 하겠다.

Read Commited

  • dirty read : commit 되지 않은 데이터는 읽지 않는다. (commit 되지 않은 데이타는 신뢰하지 않는다)

Repeatable read

  • non-repeatable read : 하나의 트랜잭션에서 읽은 값은 언제나 동일하다.
  • phantom read : 하나의 트랜잭션이 실행됬고 그 트랜잭션에 어떤 테이블에 대해 select을 하였다. 그리고 다른 트랜잭션이 해당 테이블에 insert 혹은 update를 하였다 하였다. 그 이후 처음의 트랜잭션에 해당 테이블을 select를 했다. 그러나 그 레코드의 갯수는 변화가 없고, 다른 트랜잭션이 추가한 레코드는 나타나지 않는다. (mysql의 특징) (설명이 매우 복잡하지만 앞서의 예제를 따라하면 즉각적으로 이해할 수 있다)
  • 다만, 다른 트랜잭션에서 commit한 내용에 대해, 현재의 트랜잭션이 update를 할 경우, 해당 레코드를 수정할 수 있으며, 이후 select를 할 경우 해당 레코드가 추가된다.

Read Commited 과 Repeatable read의 차이점

  • 전자는, 해당 데이터의 commit을 기준으로 최신의 snapshot을 계속 갱신한다.
  • 후자는, 트랜잭션 자신이 시작한 시간을 기점으로, 그 당시의 가장 최신의 snapshot을 읽는다.

SERIALIZABLE

  • 선점한 트랜잭션만 insert, update와 delete를 할 수 있다. 차후 발생한 다른 트랜잭션은 읽기만 가능하다.
  • 차후 발생한 트랜잭션이 update 혹은 insert를 할 경우, 선점한 트랜잭션이 종료하기 전까지 대기 상태로 유지가 되며, time out 이 발생할 수 있다.

SERIALIZABLE와 이전 레벨과의 차이점

  • 다른 레벨에서는 선점한 트랜잭션 이외의 트랜잭션이 해당 db에 update와 delete를 할 수 있다. 하지만 serializable의 경우 그것이 불가능하다.

트랜잭션의 데이타 무결성을 위한 기타 다른 팁

  • 격리수준의 설정 이외에 트랜잭션에 대한 데이타의 무결성을 보장하는 방법이 존재한다.
  • 구체적인 내용은 최범균 개발자님의 유튜브를 참고하자. 정리가 잘 되어있다.
  • https://www.youtube.com/watch?v=urpF7jwVNWs

외부연동의 문제

public void testTxRQ(TestVo vo){
	mapper.select(vo.getNo());
	mapper.insert(vo);
	TestVo vo2 = externalApi.insert(vo);
	mapper.update(vo2); 
}
  • 트랜잭션 과정에서 외부 api가 있을 경우, 외부 api에 대한 롤백이 불가능하다. 그러므로 외부 api의 사용에 있어서는 주의를 해야 한다.
public TestVO testTxRQ1(TestVo vo){
	mapper.select(vo.getNo());
	mapper.insert(vo);
	TestVo vo2 = externalApi.insert(vo);
	return vo2
}

public void testTxRQ2(TestVo vo2){
	mapper.update(vo2); 
}
  • 보통은 외부 api 연동은 트랜잭션 마지막에 둔다. 현재 서버의 트랜잭션 과정이 완료되면 마지막에 외부 api와 연동하여, 외부 api에 전달한 데이타가 무결함을 보장한다.

글로벌 트랜잭션

  • 글로벌 트랜잭션은 두 개 이상의 자원을 활용하는 것을 의미한다. (DB 2개, DB+메시징큐 등)
  • 성능상 문제로 잘 사용하지 않는다.
  • 보통 이벤트, 비동기 메시징 처리를 한다.

명시적 잠금

  • update를 위한 select일 경우.... select .... for update 라는 형태로 쿼리를 사용할 경우, 해당 레코드는 수정할 수 없다.

원자적 연산 사용

select hit from test;
-> hit : 1

update TEST 
set hit = 2

update TEST
set hit += 1 
  • update를 할 때 원자적 연산을 사용하는 것이 무결성에 더 낫다.

CAS ( compare and set)

트랜잭션 1
select * from test;

no name ver
1  IU   1

트랜잭션 2
select * from test;

no name ver
1  IU   1

트랜잭션 1
update test
set ver = 2, name = 'IUU' 
where no = '1' and ver='1'
-> 성공 (ver 2가 된다)

트랜잭션 2 
update test
set ver = 2, name = 'IUU'
where no = '1' and ver='1'
-> 실패 (ver 1은 더 이상 존재하지 않는다)
  • 레코드에 버전을 부여하고 갱신할 때 해당 버전을 항상 조건으로 한다.
profile
JAVA web developer

0개의 댓글