본문 바로가기
  • 1+1=3
독서/개발관련

[데이터베이스 첫걸음] 7장: 트랜잭션과 동시성 제어

by 여스 2022. 1. 3.
반응형

트랜잭션이란

테이블 갱신은 단순히 INSERT/DELETE/UPDATE만 사용하지 않고, 대부분 복수 쿼리를 연속적으로 수행한다. 또한, 갱신 전의 데이터로 SELECT를 사용할 때 이를 포함해 복수 쿼리를 일관된 형태의 한덩어리로 다뤄야 한다. 트랜잭션이란 이런 복수쿼리를 한단위로 묶은 것.

 

트랜잭션의 특징은 ACID로 정의된다.

- Atomicity(원자성)

- Consistency(일관성)

- Isolation(고립성 또는 격리성)

- Durability(지속성)

 

원자성(Atomicity)

데이터 변경(INSERT/DELETE/UPDATE)을 수반하는 일련의 데이터 조작이 전부 성공할지 전부 실패할지를 보증하는 구조. 

예) 기차표 예매하는데 결제하기 전에 보유한 돈이 부족해서 결제 실패하는 경우(또는 서버통신이 끊기거나 서버 다운되는 경우), 결제하기 까지의 전 과정이 모두 rollback되어야 한다. 

즉, 전부 성공하거 전부 실패하거나이다.

 

일관성(Consistency)

예) 사용자정보를 디비에 저장할 때 식별하기 위한 일련번호를 발급해서 저장함. 이때 유니크 제약을 설정해서 중복된 번호를 갖지 않게 함. 

 

고립성(Isolation)

일련의 데이터조작을 복수의 사용자가 동시에 실행해도 '각각의 처리가 모순없이 실행되는 것을 보증한다'는 것.

 

예) 호텔 예약상황에서, 사용자 두명이 동시에 방을 예약했어. 숙박예약 프로그래밍 로직은 다음과 같음

1. 현재 빈 룸의 수를 확인한다(select)

2. 빈 싱글룸의 수에서 1을 빼고 결과를 빈 싱글룸 수로 되돌려쓴다(update)

 

두명이 예약하면 방이 원래 10개라면 8개가 되어야 한다. 근데 순서가

a1->a2->b1->b2로 차근차근 이뤄지지 않고, a와 b가 거의 동시에 예약을 해서

a1->b1->a2->b2로 이뤄지면 실제론 두명이 예약한것임에도 불구하고 한명만 예약한것처럼 되어 남은 방이 9개로 처리가 된다.

 

위와 같은 상황을 막기 위해선 테이블에 대해 잠금(lock)을 걸어서 후속처리를 블록(block)하면 된다. 잠금단위는 테이블 전체, 블록, 행 등이 있는데, mysql에서는 트랜잭션 처리를 할 때에는 주로 행단위의 잠금기능을 이용함.

예를 들어, 앞의 1,2번 로직중 1번의 select(현재 빈 룸 수 확인)을 처리할 때 실행한 행에 잠금이 걸림. 그럼 해당 잠금이 commit 또는 rollback되어 해제될때까지 후속처리는 대기하게 된다.

 

  • 고립성이란 '각각의 처리가 모순없이 실행되는 것을 보증한다'는 것인데 어떤 상태가 모순없다는 것일까?

답은 복수의 트랜잭션이 순서대로 실행되는 경우와 같은 결과를 얻을 수 있는 상태이다. 이것을DBMS에서 격리수준으로 구현하고 제공하는 것이 '직렬화 가능'이라는 사양임. 그러나 직렬화 가능의 고립성은 항상 동시에 동작하는 트랜잭션이 1개가 되기에 성능면에서 실용적이지 않다. 따라서 직렬화 가능으로부터 격리수준을 완화해서 자신이 아닌 다른 트랜잭션이 영향받는 것을 허용하는 4단계를 ANSI에서 정의함.

 

- 격리수준(1에서 4로 갈수록 점점 직렬화 기능이 엄격해짐)

1. 커밋되지 않은 읽기(Read Uncommited)

2. 커밋된 읽기(Read Commited)

3. 반복 읽기(Repeatable Read)
4. 직렬화 기능(Serializable)

 

그러나 완화되면서 직렬화 기능에서 발생하지 않았던 문제들이 발생: 

-더티읽기(Dirty Read)

어떤 트랜잭션이 커밋되기 전에 다른 트랜잭션에서 데이터를 읽는 현상. 

예) 사용자a가 값을 변경하고 커밋되기 전에 사용자b가 변경한 후의 값을 읽는 것임. 사용자a가 빈 방의 수가 10인 레코드를 9로 변경한 경우 커밋전이라도(ㄹㅇ극단적으로 롤백되어도) 사용자 b가 select한 결과는 9가 된다. 

즉 확정전의 더럽혀진 데이터를 읽는 것!

 

- 애매한 읽기(Fuzzy/NonRepeatable Read)

어떤 트랜잭션이 이전에 읽어들인 데이터를 다시 읽어들일 때 2회 이후의 결과가 1회때와 다른 현상.

예) a가 빈방 수 10을 읽고 그 후 사용자b가 9로 변경해서 커밋했다. 계속해서 a가 select를 다시 실행하면 최초에 select했던 10이 아닌 변경 후의 9를 읽어들이게 된다. 

최초에 읽은 값 10이 2회째에서 보증되지 못하고 애매하게 되는 것임. 

 

- 팬텀읽기(Phantom Read)

어떤 트랜잭션을 읽을 때 선택할 수 있는 데이터가 나타나거나 사라지는 현상.

예) a가 범위검색(빈 방의 수가 10개 이상인 호텔 검색)을 수행 후 3행을 읽었다고 가정하자. 그담에 b가 막 그 범위에 들어가는 데이터 1행을 insert하고 커밋도 실행했다. 계속해서 a가 다시 같은 select문을 실행하면 최초에 select해서 얻었던 3행이 아닌 4행이 된다.

이처럼 나타나거나 사라지는 데이터가 유령과 같이 되는것임.

 

지속성(durability)

일련의 데이터 조작을 완료하고 완료 통지를 사용자가 받는 시점에서 그 조작이 영구적이 되어 그 결과를 잃지 않는 것. 이는 시스템이 정상일 때 뿐 아니라 갑자기 os가 정지되거나 시스템 장애도 견딜 수 있다는 것. 하드디스크에 로그로 기록하고 이상이 발생하면 로그를 사용해 지속성을 실현함.

 

 

커밋

mysql이나 oracle에서는 create table과 같은 ddl 실행 시 암묵적인 커밋이 발행됨.

또한 MySQL, PostgreSql 등의 DBMS에서는 오토커밋이 기본설정인데, 오토커밋이란 하나의 sql문이 하나의 트랜잭션으로 구분되는 것을 말함. 이러면 사용자가 commit 또는 rollbakc을 따로 실행하지 않아도 하나의 트랜잭션이 딘다.

 

DDL, DML, DCL

sql문은 DBMS에 줄 수 있는 명령의 종류에 따라 3가지로 구분됨.

 

- DDL (데이터 정의언어)

데이터를 저장하는 그릇인 스키마(데이터베이스) 또는 테이블 등을 작성하거나 제거한다. create, drop, alter 등이 있음.

 

- DML (데이터 조작언어)

테이블 행을 검색하거나 변경하는데 사용됨. select, insert, update, delete등이 있다. 대부분 사용하는 sql이 dml이다.

 

- DCL (데이터 제어언어)

데이터베이스에서 실행한 변경을 확정하거나 취소함. commit, rollback등이 있다. 

 

 

 


추가로 트랜잭션 관련하여 개념이 명확하게 이해가 가지 않아 유튜브를 따로 보고 정리하였다.

https://www.youtube.com/watch?v=6h-3x00fd2Y 

 

트랜잭션이란

하나의 단위로 수행되길 바라는 쿼리의 묶음(= 업무 수행단위 또는 논리적인 수행단위)

 

쿼리의 묶음이다보니까 각각의 쿼리를 수행하다가 문제가 생길 수 있다. (a,b)가 한 묶으민데 a를 실행하고 b실행전에 에러가 나버리면 수행단위가 반만 수행된거잖아. 

이처럼 하나만 오류나면 전체가 다 취소해야 하는게 상식적이니까 트랜잭션 처리를 ACID를 유지하도록 해야 함.

 


 

격리수준에 관한 내용 더 정리.

https://www.youtube.com/watch?v=poyjLx-LOEU 

커밋되지 않은 데이터 읽기

 

커밋되지 않은 데이터 덮어쓰기

 

Read Committed

위 두개의 문제를 막기 위해 Read Committed란 격리수준을 제공함.

- 커밋된 데이터만 읽기(커밋된 값과 트랜잭션 진행중인 값을 따로 보관해서 트랜잭션진행중인 값은 읽지 않음)

- 커밋된 데이터만 덮어쓰기(행단위 잠금사용. 같은 데이터를 수정하는 트랜잭션은 끝날때까지 기다리도록 함)

 

 

but...문제가 다 해결된 건 아니다.

읽는 동안 데이터변경 1

 위 문제를 해결하기 위해 Repeatable Read란 격리수준 등장

Repeatable Read

원칙은 간단. 트랜잭션 진행되는 동안에는데이터가 변경되더라도 같은 데이터를 읽게 해주는 것임. 이건 mvcc처럼 여러 버전을 만들어서 읽는 시점에 해당하는 버전의 데이터만 읽도록 함.

 

 

아니 근데 또 다른 문제가 아직 남아았어

변경 유실

변경한 내용이 유실될 수 있다. 같은 데이터를 서로 업데이트하려다 발생함.

아래 그림에서 참고로, 아래사용자가 update날릴땐 잠금에 의해서 a사용자가 커밋할때까지 지연됐다가 커밋되면 update날라감.

기대 readcnt =3이어야 함. 왜냐면 두명 추가로 읽었으니까 1+1+1 = 3. but 실제값은 2임. ㅠㅠ이런걸 lost Update라 함.

해결방법:

 

-원자적 연산, 명시적 잠금, CAS(Compare And Set)

 

- 원자적 연산

말그대로 연산을 사용하는 것임. 단 해당 디비가 연산기능 지원하는지 확인해야 함.

update article set readcnt = readcnt + 1 where id = 1

 

- 명시적 잠금

조회할 때 수정할 행을 미리 잠가버리기

- CAS

여기선 ver을 넣는데, 위사용자가 ver1을 ver2로 바꿨기 때문에 아래사용자가 한 update은 아무런 행도 수정못하게 됨.

 

읽는 동안 데이터 변경 2

이건 당직자 문제임. 당직자 두명이 있어. 당직자는 한명만 있으면 되기에 지금 당직자가 두명이면 한명은 빠져도 됨. 근데 두명이 서로 거의 동시에 확인하고 두명을 확인하니까 둘다 빠져버리는 거임.

위 당직자 문제는 serializable을 사용해서 해결할 순 있지만, 넘 비효율적이라 이케 잘 안함. 대신 인덱스 잠금이나 조건기반 잠금을 사용함.

 

 

정리

- 동시성은 초보자가 놓치기 매우 쉬운 문제이다...그러나 문제를 이해하고 겨길수준을 이해하면 문제발생을 줄일 수 있다.

- 잠금시간은 최소화하는게 좋다.(성능(처리량) 저하)

 

반응형

댓글