데이터베이스에서 실행되는 연산이 사용자 관점에서는 하나로 보이는 경우가 있다.
하지만, 실제 내부적으로는 여러 연산으로 구성되어 있다.
계좌이체의 경우 특정 계좌에서 출금, 다른 계좌에 입금과 같이 두개 이상의 연산으로 구성된다.
하지만 사용자 입장에선 이 모든 연산이 하나의 트랜잭션 처럼 보이게 된다.
여기서 중요한 점은 여러 연산이 동시에 수행되거나, 도중에 오류가 발생하면 어떠한 연산도 실행되지 않은 것처럼 데이터 베이스에서 이전의 상태로 되돌려져야 한다.
이처럼 하나의 논리적 작업 단위를 이루는 연산의 집합을 트랜잭션이라 부른다.
모두 실행되거나 그렇지 않으면 하나도 실행되지 않음을 보장해야 한다.
트랜잭션의 개념
트랜잭션(transaction)은 다양한 데이터 항목에 접근하고 갱신하는 프로그램 수행 단위다.
트랜잭션은 다른 트랜잭션과 begin transaction 과 end transaction 형태의 명령문으로 구분된다. 즉 하나의 트랜잭션은 begin transaction 과 end transaction 사이에 실행된 연산으로 구성되어 있다.
원자성(Atomicity)
연산 집합은 사용자에게 하나의 나눌 수 없는 단위로 보여야 한다.
나눌 수 없는 단위이기 때문에 전부 실행되거나 전부 실행되지 않아야 한다.
연산 집합은 사용자에게 하나의 나눌 수 없는 단위로 보여야 한다.
나눌 수 없는 단위이기 때문에 전부 실행되거나 전부 실행되지 않아야 한다.
실패하면 데이터베이스에 행한 모든 변경은 되돌려져야 한다.
이러한 규칙은 트랜잭션 자체의 실패, 운영체제 오류, 컴퓨터의 전원 off와 같은 어떠한 상황에서도 반드시 지켜져야 한다.
하지만, 이 조건은 상당히 어려운 것이다.
일부는 메모리에 존재하고 일부는 디스크에 저장되어 있을 수 있기 때문에 “전부 아니면 전무(all-or-none)” 조건을 지켜야 한다. 이것이 원자성 이다.
고립성(isolation)
트랜잭션은 하나의 단위이다. 따라서 여러 데이터베이스 연산으로 구성된 것처럼 보여선 안된다.
이 역시 어려운 요구사항인데 이유는 하나의 SQL이 실제로는 하나의 데이터베이스에 여러 번 접근하거나 하나의 트랜잭션 역시 여러개 SQL문장으로 구성되기 때문이다.
따라서 데이터베이스 시스템은 다른 데티어베이스 명령어의 영향을 받지 않고, 트랜잭션이 올바르게 수행될 수 있는 조치가 필요하다.
이 그림에서 게시글의 counter는 현재 42이다.
서로 다른 유저 1, 2가 접근하여 실제 44가 되길 희망하지만, 공교롭게 42+1 이 두번 실행되었기 때문에 43으로 결과가 남는다.
고립성은 이 각각이 격리되어 올바르게 44가 되도록 하는 것이다.
지속성(durability)
시스템이 올바르게 트랜잭션을 수행하는 것을 보장하였다 하더라도 장애가 발생하면 변경 사항(저장, 수정, 삭제)을 잃어버릴 수 있다.
트랜잭션의 결과는 시스템의 장애가 발생한다 하더라도 영구적으로 반영되어야 한다.
이러한 속성을 지속성이라 한다.
일관성(consistency)
만약 하나의 트랜잭션이 일관된 상태의 데이터베이스에서 원자적이고 고립된 상태로 실행되었다면 데이터베이스는 트랜잭션이 종료된 후에도 일관된 상태여야 한다.
필자는 일관성을 primary key 제약 조건, 참조 키 제약조건, check 제약조건 이라 생각했다.
하지만 이 뿐 아니라 SQL을 사용해 기술할 수 없는 응용 프로그램 자체의 일관성 조건을 보장해야 한다.
이러한 제약 조건을 지키는 것은 트랜잭션을 사용하는 프로그래머의 역할과 책임이다.
어찌보면 필자가 그간 개발해온 내용들의 대부분은 consistency에 해당하는 부분이 아닐까 싶다.
원자성(Atomicity)
트랜잭션의 모든 연산이 정상적으로 수행 완료되거나 아니면 어떠한 연산도 수행되지 않는 원래 상태가 되도록 해야한다.
일관성(Consistency)
고립(동시에 수행되는 트랜잭션이 없는)상태에서 트랜잭션이 데이터베이스의 일관성을 보존해야 한다.
고립성(Isolation)
여러 트랜잭션이 동시에 수행된다 하더라도, 임의의 트랜잭션 와 쌍이 있을때
시스템은 에게 가 시작되기 전에 수행을 마쳤거나 가 종료된 후에 가 수행을 시작하는 것과 같이 되도록 보장해야 한다.
즉, 각각의 트랜잭션은 다른 트랜잭션이 동시에 수행되고 있는지 알지 못하는 것과 동일하게 해야한다.
지속성(Durability)
트랜잭션이 성공적으로 수행되면 시스템에 오류(정전, 네트워크 이슈 등등)가 발생한다 하더라도 영구적으로 반영되어야 한다.
이러한 트랜잭션의 특성의 앞 글자를 따서 ACID라 부른다.
ACID를 지키기 않는 트랜잭션 모델을 BASE라 하는데 이는 매우 애매하다.
또한 여기서 설명한 ACID는 전반적인 database의 ACID를 설명하기 때문에 특정 제품을 선택한다면 그곳에서 정의하는 ACID는 무엇인지, 그래서 무엇을 개발자가 고려해야 하는지 알아둘 필요가 있다.
트랜잭션 모델
SQL 언어는 데이터가 디스크에서 메모리로 이동하는지, 혹은 메모리에서 디스크로 이동하는지에 초점을 맞추는 언어이다.
이 글은 이해를 명확히 하기위해 SQL의 insert, delete는 고려하지 않는다.
•
read(x): 데이터 x를 읽어 read 연산을 수행하는 메인 메모리 버퍼에 x라는 변수에 저장한다.
•
write(x): 메인 메모리 버퍼에 있는 변수 x의 값을 데이터베이스의 항목 x에 저장한다.
원자성에서 언급한것 처럼 메인 메모리에만 반영되었는지 아니면 디스크에도 기록된 것인지 판단하는게 중요하다. 데이터베이스는 write 연산의 갱신 결과를 즉시 디스크에 저장하지 않고, 다른 곳에 임시로 저장하고 나중에 디스크에 저장할 수도 있다.
ACID 특성을 계좌이체 트랜잭션을 예시로 들어 더 살펴보자.
트랜잭션 는 계좌 A에서 계좌 B로 $50을 이체하는 트랜잭션이라 가정하자.
일관성
•
위 예제에서 계좌 A, B의 합은 트랜잭션 전과 후 모두 동일하다.
•
이 일관성 조건이 없다면 트랜잭션 수행 이후 A + B의 총 합이 증가하거나 감소할 수 있다.
원자성
•
트랜잭션 의 와 사이에서 에러가 일어났다고 가정하자.
•
트랜잭션 실행 전 두 계좌의 합이 3000$인데 에러로 인해 2950$으로 남았다.(에러로 인해 50$을 잃었다)
◦
이러한 상태를 비일관성 상태(inconsistent)라 한다.
•
원자성이 제공되면 트랜잭션의 모든 연산이 데이터베이스에 반영되거나 아니면 하나도 반영되지 않아야 한다.
이러한 원자성은 어떻게 지켜지는 것일까?
시스템은 트랜잭션이 write 연산을 하는 대상의 예전 값을 추적한다.
이러한 정보를 log라 한다.
에러로 인해 일부만 반영되었다면 log 중 undo log를 사용하여 트랜잭션 시작 전의 값으로 원복하여 마치 트랜잭션이 실행되지 않은 것 처럼 유지한다.
지속성
•
트랜잭션이 성공적으로 동작하여 50$이 이체 되어 가 성공적으로 종료되면, 시스템에 장애가 발생해도 지속적으로 유지되어야 한다.
•
만약 메모리에서 디스크로 아직 이동되지 않았고, 이 상태에서 장애가 발생해도 최종적으로 디스크에 남아 지속성을 유지해야 한다.
지속성 유지를 위해 다음 둘 중 하나를 선택해야 한다.
1. 트랜잭션이 일으킨 갱신을 트랜잭션이 완료 되기 전에 disk에 저장한다.
2. 데이터베이스 시스템이 오류 이후 재시작할 때 복구할 수 있을만큼 충분한 정보를 디스크에 기록한다.
고립성
•
트랜잭션의 원자성, 일관성이 보장 되어도 무수히 많은 n개의 트랜잭션이 동시에 실행된다면 예기치 않은 순서로 연산이 배치되어 비일관성 상태가 발생할 수 있다.
•
위 사례에서 와 사이에서 임의의 트랜잭션 가 를 실행하면 비일관성 상태가 된다.
◦
만약 트랜잭션 에서 다른 곳에 쓰기 연산을 수행하면 트랜잭션 가 올바르게 종료되어도 비일관성 상태로 남을 수 있다.
동시에 실행하여 발생하는 문제를 해결하는 가장 간단한 방법은 모든 트랜잭션을 순차적으로 실행하는 것이다.
하지만, 트랜잭션의 동시 실행은 성능에 매우 상당한 이점을 주기 때문에 분명한 케이스를 제외하곤 포기할 수 없다.
고립성은 트랜잭션 집합 이 동시에 실행된다 하더라도 순차적으로 실행되는 것과 같은 결과를 보장하는 것이다.
MySQL의 ACID
MySQL의 ACID는 InnoDB와 연관이 있다. 필자는 MySQL로 접근하기 보다 InnoDB로 접근하는 것이 더 알맞은 상황이므로 InnoDB로 접근하겠다.
원자성
•
◦
auto commit을 활성화 하면 즉시 반영된다.
◦
활성화 하지 않았다면 commit, rollback 을 명시적으로 입력해야 한다.
◦
MySQL은 default로 enable 상태이다.
•
◦
START TRANSACTION 혹은 BEGIN 으로 새로운 트랜잭션을 실행한다.
◦
현재 세션에 대해 SET autocommit 을 활성화 혹은 비활성화 할 수 있다.
•
◦
현재 트랜잭션의 변경을 취소한다.
By default, MySQL runs with autocommit mode enabled. This means that, when not otherwise inside a transaction, each statement is atomic, as if it were surrounded by START TRANSACTION and COMMIT. You cannot use ROLLBACK to undo the effect; however, if an error occurs during statement execution, the statement is rolled back.
MySQL은 autocommit 모드가 기본으로 켜져있다. 이 뜻은 transaction 으로 감싸지 않아도 각각의 SQL 쿼리가 원자성을 갖는다듣 뜻이다.
사용자는 반영을 ROLLBACK 으로 트리거할 수 없으나 쿼리에 에러가 있다면 자동으로 ROLLBACK 이 실행되어 취소된다.
Consistency
일관성은 MySQL의 기능과 관련하여 InnoDB에서 일관성을 포함한다.
•
◦
Doublewrite 버퍼는 InnoDB에 있는 storage 공간이다.
▪
Page를 적절한 InnoDB file에 쓰기 전에 buffer pool에서 플러시된 페이지를 저장하는 공간이다.
◦
mysqld 프로세스가 에러로 인해 예상치 못하게 종료되었을 때 InnoDB는 doublewrite buffer에서 페이지의 복사본을 찾아온다.
◦
두 번 기록되지만, I/O overhead를 두 배로 요구하거나 두배의 I/O 작업이 필요하진 않다.
◦
데이터는 순차적 chunk로 doublewrite buffer에 기록되고 fsync() 호출을 통해 OS에 전송된다.
•
◦
Crash recovery는 다음의 단계로 진행된다.
1.
Tablespace 검색: InnoDB는 복구 중에 redo log 적용이 필요한 테이블스페이스를 찾는다.
2.
Redo log application: DB 초기화 시 connection을 맺기 전에 redo log를 적용하기 시작한다. 종료 혹은 크래시 시점에 모든 변경 사항이 tablespace로 flush 되었다면 redo log 적용은 생략한다. (InnoDB는 redo log 파일이 없을 때에도 redo log 적용을 생략한다.)
3.
4.
변경 버퍼 병합: tablespace의 change buffer에서 secondary index의 leaf page로 변경 사항을 적용한다.
5.
Purge: active 트랜잭션에서 더 이상 보이지 않는 삭제 태깅 된 레코드를 삭제한다.
Isolation
•
◦
요약 내용은 Atomicity 참조
•
◦
Isolation level은 따로 다룰 예정이다. 주의할 점은 통상적인 격리 수준과 MySQL에서 격리 수준이 다소 차이가 있다.
◦
또한 isolation level에 따른 lock은 관계 대수에 근거하여 잠금을 건다. → next key lock
•
◦
후에 다룰 Isolation level과 함께 새로운 포스트로 정리할 예정이다.
Durability
지속성은 MySQL S/W의 기능으로 H/W 구성과 상호작용 한다.
상호작용을 하는 이유는 CPU, network, storage devices 등과 관련하여 다양한 가능성이 존재하기 때문이고 이들 각각은 구체적인 guideline(이 가이드에서 종종 새로운 hardware를 구매하라고 권장하기도)을 제공한다.
•
InnoDB의 doublewrite buffer
◦
Atomicity 요약 참조
•
◦
이 변수의 기본값은 1이다.
◦
1: 각각의 transaction이 commit 될 때 로그가 disk로 flush된다. ACID
◦
0: 로그가 1초에 한번 log가 작성되고, disk로 flush된다. crash 상황에서 특정 로그(not written)가 유실될 수 있다.
◦
2: transaction이 commit되면 바로 로그가 쓰여지지만, flush는 1초에 한번 발생한다. 각 트랜잭션의 로그가 flush되지 않았다면 crash 상황에서 유실될 수 있다.
•
◦
얼마나 자주 binlog를 디스크와 동기화 시킬 것인가
sync_binlog=0 | MySQL 서버에서 binlog가 동기화 되는 것을 비활성화 한다. 대신 MySQL 서버는 다른 파일과 같이 OS가 binlog를 주기적으로 disk 에 flush 하도록한다.
최고의 성능을 제공하는 옵션이지만, 전원, OS의 crash가 발생하면 동기화되지 않은 commit transaction이 발생한다. |
sync_binlog | 트랜잭션이 commit되기 전에 동기화 되도록 한다.
이 방법은 가장 높은 안정성을 보장하지만 disk에 write의 횟수를 증가하기 때문에 성능에는 부정적이다. 전원이나 OS 등의 crash가 발생하면 binlog에 누락된 트랜잭셔은 ready 상태로 남아있으며, automatic recovery routine에서 트랜잭션을 롤백하여 binlog에서 트랜잭션이 손실되지 않도록 보장한다. |
sync_binlog=N
N은 0과 1 제외 | N개의 binlog commit group이 수집된 이후 동기화를 실행한다.
binlog에 플러시 되지 않은 commit 트랜잭션을 가질 수 있어 손실이 발생할 수 있다.
높은 값을 가질수록 성능은 향상 시키지만, 데이터 손실의 가능성 역시 비례한다. |
•
◦
활성화가 되면 테이블을 하나의 file-per-table로 tablespace에 생성하고, 비활성화 하면 시스템 tablespace에 저장한다.
◦
Tablespace에 대한 별도의 학습이 필요하다.
•
Writer buffer는 disk, SSD, RAID 안에 있다.
정리
•
Transaction의 설명을 위한 ACID 특징 설명
•
예시 케이스를 통한 ACID 설명
•
MySQL에서 설명한 ACID 기능 설명
◦
이건 리스팅만 했는데 따로 포스팅을 기획하는게 좋을것 같다.