트랜잭션이란?

논리적인 작업의 가장 작은 단위로, 작업의 일관성을 보장해주기 위한 단위입니다. 이러한 특성에 의해 작업이 부분적으로 실패하거나 성공하지 않고, 완전히 성공 또는 실패하게 됩니다.

트랜잭션의 특성

트랜잭션에는 아래 네가지 특성이 존재하며 각 특성의 앞글자를 따서 ACID로 불립니다.

  1. 원자성 (Atomicity)
    • 트랜잭션이 하나의 수행단위이며, 완전히 수행(Commit) 되거나 전혀 수행되지 않아야 합니다(Rollback)
  2. 일관성 (Consistency)
    • 트랜잭션이 끝난 전후로 데이터베이스는 항상 일관된 상태(제약조건 만족)를 유지해야 합니다
  3. 고립성 (Isolation)
    • 서로 다른 트랜잭션은 독립적이며 간섭할 수 없는 상태입니다.
  4. 지속성 (Durability)
    • 트랜잭션이 Data를 변경시킨 뒤 Commit 되면 이후 데이터는 손실되지 않고 유지됩니다.

트랜잭션 특성에 따른 결과 차이

  • MySQL에서 대부분 사용되는 엔진은 InnoDB입니다. InnoDB는 트랜잭션을 지원하는 스토리지 엔진이며, Default 스토리지 엔진입니다.
  • MySQL에서 사용가능 한 MyISAM 엔진은 트랜잭션을 지원하지 않으며, 그로 인해 데이터 정합성 보장이 되지 않습니다.
  • 트랜잭션 지원 여부에 따라 달라지는 결과에 대해 아래 테스트를 이용해 확인해봤습니다.
    • MyISAM 테이블의 경우, 트랜잭션이 실패했지만 결과가 부분적으로 성공 (=Partial Update)
-- myisam 테이블 생성 
mysql> CREATE TABLE tab_myisam ( fdpk INT NOT NULL, PRIMARY KEY (fdpk) ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO tab_myisam (fdpk) VALUES (3);
Query OK, 1 row affected (0.01 sec)

-- innodb 테이블 생성 
mysql> CREATE TABLE tab_innodb ( fdpk INT NOT NULL, PRIMARY KEY (fdpk) ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO tab_innodb (fdpk) VALUES (3);
Query OK, 1 row affected (0.01 sec)

-- PK 중복 에러가 발생하도록, (1,2,3) Insert
mysql> INSERT INTO tab_myisam (fdpk) VALUES (1), (2), (3);
ERROR 1062 (23000): Duplicate entry '3' for key 'tab_myisam.PRIMARY'
mysql> INSERT INTO tab_innodb (fdpk) VALUES (1), (2), (3);
ERROR 1062 (23000): Duplicate entry '3' for key 'tab_innodb.PRIMARY'

-- Insert 작업 실패 후, 테이블 데이터 확인
mysql> SELECT * FROM tab_myisam;
+------+
| fdpk |
+------+
|    1 |
|    2 |
|    3 |
+------+
3 rows in set (0.00 sec)

mysql> SELECT * FROM tab_innodb;
+------+
| fdpk |
+------+
|    3 |
+------+
1 row in set (0.01 sec)

잠금(Lock)

잠금이란, 다중 트랜잭션 환경에서 데이터 일관성을 유지할 수 있게 마련된 장치입니다. 잠금 동작은 트랜잭션 Isolation Level에 따라 영향을 받습니다. 각 Isolation level에 따른 특징은 아래 링크를 참조하시면 됩니다. MySQL 기본 Isolation Level은 Reapeatable Read입니다.

MySQL에서 잠금은 크게 아래 두 가지 레벨로 분류됩니다.

  1. 스토리지 엔진 레벨
    모든 스토리지 엔진에 영향을 주는 잠금 (ex) 테이블 Lock, 메타데이터 Lock, 네임드 Lock
  2. MySQL 엔진 레벨
    스토리지 엔진 상호 간 영향을 주지 않는 잠금 (ex) 스토리지 엔진 잠금 이외에 모든 Lock

잠금의 종류

글로벌 락

  • 글로벌 락은 FLUSH TABLES WITH READ LOCK 명령어를 통해 획득할 수 있으며, 현재 MySQL에서 제공하는 Lock 중 가장 큽니다.
  • 한 세션에서 글로벌 락을 소유할 경우, 조회를 제외한 모든 DDL, DML 세션이 대기하게 됩니다.
  • 동일 오브젝트가 아닌 다른 오브젝트에 대한 모든 접근이 영향을 받습니다.
  • 일관된 데이터를 mysqldump툴을 이용한 백업을 받을 때 사용되는 Lock입니다. (옵션에 따라 조절 가능)
-- Session 1
mysql> FLUSH TABLES WITH READ LOCK;
Query OK, 0 rows affected (0.02 sec)

-- Session 2 (SELECT, DML, DDL 시도)
mysql> SELECT * FROM tab_innodb;
+------+
| fdpk |
+------+
|    3 |
+------+
1 row in set (0.00 sec)

mysql> INSERT INTO tab_innodb VALUES (1), (2), (4);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql> ALTER TABLE tab_innodb ADD add_col varchar(10), ALGORITHM = INPLACE, LOCK = NONE;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction


-- 글로벌 Lock으로 인해 조회 쿼리를 제외한 모든 쿼리가 실패한다.

백업 락

  • InnoDB의 경우, 트랜잭션을 지원하며 일관된 상태를 보장하므로 모든 데이터 변경 작업을 멈추는 것이 비효율적입니다.
  • 글로벌 락에 비해 좀 더 가벼운 락의 필요성이 생기게 되어 새로 도입된 Lock입니다.
  • 백업 락이 걸려있을 경우, 데이터 조회, 변경은 가능하며 아래 명령어들은 잠금에 의해 영향을 받습니다.
    • 스키마 관련 작업 (DDL)
    • REPAIR TABLE / OPTIMIZE TABLE
    • 사용자 관리 / 비밀번호 변경
  • Replica에서 Xtrabackup 툴을 이용한 백업 도중 기존에는 DDL 실행 시, 백업이 실패했지만 백업 락을 이용해 DDL이 실행되더라도 백업은 성공하고, 복제는 멈추게 됩니다.
-- Session 1
mysql> LOCK INSTANCE FOR BACKUP;
Query OK, 0 rows affected (0.00 sec)

-- Session 2 (SELECT, DML, DDL 시도)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO tab_innodb VALUES (1), (2), (4);
Query OK, 3 rows affected (0.01 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)

mysql> ALTER TABLE tab_innodb ADD add_col varchar(10), ALGORITHM = INPLACE, LOCK = NONE;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

-- 백업 Lock으로 인해 DDL은 실패

테이블 락

  • 개별 테이블 단위의 잠금입니다.
  • LOCK TABLES [테이블명] [READ | WRITE] 명령어를 이용해 명시적으로 잠금을 획득합니다.

    [실제 운영에서 사용되는 경우는 거의 없습니다]

  • InnoDB 엔진 테이블의 경우, 레코드 기반 잠금을 지원하여 DML 작업에서 테이블 Lock은 발생하지 않습니다.

    특정 유형의 DDL 실행 시, 잠금이 발생합니다. ALGORITHM=INPLACE, LOCK=NONE 옵션을 이용해 작업 유형별 Online DDL이 가능한 경우합 있습니다.

네임드 락

  • 네임드락은 GET_LOCK() 함수를 이용하여 특정 문자열에 대한 잠금 획득이 가능합니다.
  • 데이터베이스 관련 오브젝트가 아닌, 단순 사용자 지정 문자열에 대한 잠금입니다.

메타데이터 락

  • 메타데이터 락은 객체 이름이나 구조를 변경 (ex. DDL, RENAME)할 때 잠금을 획득합니다.
  • 해당 Lock이 걸린 객체에 대해, DML이나 DDL은 불가합니다.
  • performance_schema.metadata_locks 테이블을 통해 확인 가능합니다.
    • 8.0 버전으로 업그레이드 되면서 Default로 활성화되도록 변경됐습니다.
-- 테스트 테이블 생성 & 1000만 건 데이터 생성
mysql> CREATE TABLE `lock_table` (
        `id` int NOT NULL AUTO_INCREMENT,
        `calendar` date DEFAULT NULL,
        `num` int DEFAULT NULL,
        `num_2` int DEFAULT NULL,
        PRIMARY KEY (`id`),
        KEY `idx_calendar` (`calendar`),
        KEY `idx_num` (`num`)
        ) ENGINE=InnoDB AUTO_INCREMENT=10026856 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

mysql> INSERT lock_table(calendar, num, num_2)
       SELECT (CURRENT_DATE - INTERVAL FLOOR(RAND() * 14) DAY),
              cast(rand() * 100 as unsigned),
              cast(rand() * 100 as unsigned)
       FROM information_schema.TABLES a
             CROSS JOIN information_schema.TABLES B
             CROSS JOIN information_schema.TABLES C
       LIMIT 10000000;
Query OK, 10000000 rows affected (2 min 28.57 sec)
Records: 10000000  Duplicates: 0  Warnings: 0

-- Session 1 (INDEX 생성 시도)
mysql> ALTER TABLE lock_table ADD INDEX lock_idx3 (calendar, num_2);

-- Session 2 (metadata Lock 정보 확인)
mysql> SELECT OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, SOURCE, OWNER_THREAD_ID FROM performance_schema.metadata_locks WHERE OBJECT_SCHEMA <> 'performance_schema';
+-------------+---------------+--------------+---------------------+--------------------+-----------------+
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME  | LOCK_TYPE           | SOURCE             | OWNER_THREAD_ID |
+-------------+---------------+--------------+---------------------+--------------------+-----------------+
| SCHEMA      | test          | NULL         | INTENTION_EXCLUSIVE | sql_base.cc:5399   |              92 |
| TABLE       | test          | lock_table   | SHARED_UPGRADABLE   | sql_parse.cc:6162  |              92 |
| TABLE       | test          | #sql-28c7_30 | EXCLUSIVE           | sql_table.cc:16434 |              92 |
+-------------+---------------+--------------+---------------------+--------------------+-----------------+
3 rows in set (0.02 sec)

레코드 락

  • 테이블 인덱스 레코드별로 잠금을 획득합니다.
  • 인덱스가 없는 레코드의 경우, InnoDB에서는 내부적으로 auto_inc PK를 생성해 레코드 잠금을 획득합니다.
  • 레코드 잠금은 Primary Key, Unique Key을 사용하는 작업에서 할당됩니다.
  • 레코드 락의 종류에는 세 가지가 존재합니다.
    • S-lock : 다른 트랜잭션이 S-lock을 소유한 오브젝트에 대해 S-lock만 획득 가능한 Lock입니다.
    • X-lock : 다른 트랜잭션이 X-lock을 소유한 오브젝트에 대해 S-lock, X-lock을 획득할 수 없도록 제한합니다.
    • Intention-lock : Row-level / Table-level 락이 함께 사용되는 다중 잠금 계층을 지원합니다.
      1. Intention Shared Lock (IS) : 해당 테이블 각 행에, S-lock 설정
      2. Intention Exclusive Lock (IX) : 해당 테이블 각 행에 X-lock 설정
        • Intention Lock (의도락)을 통해 어떤 Lock을 획득할 것인지 미리 알려, 트랜잭션의 접근을 효과적으로 제어하는 방법입니다. 그러므로 Intention Lock을 Table-Level로 건 후, Row-Level Lock을 할당합니다.

서로 다른 두 트랜잭션이 호환성으로 인해 충돌할 경우, 선행 트랜잭션이 잠금해제할 때까지 대기합니다. 각 트랜잭션이 서로 소유한 Lock에 대해 상호 대기할 경우, Deadlock이 발생합니다.

갭(GAP) 락

  • 레코드 사이의 일정 구간에 부여하는 Lock을 의미합니다.
    • (ex) SELECT * FROM table WHERE a BETWEEN 5 and 10 FOR UPDATE
    • 위 예제와 같은 경우, 5~10 범위에 대해 Insert가 불가합니다. (기본 Isolation-level인 READ-REPEATABLE의 경우)
  • READ-COMMITTED isolation-level에서는 갭락이 비활성화 됩니다.
  • 해당 잠금이 할당된 구간에 Insert 되는 것을 방지합니다.
  • 넥스트키 락의 일부로 자주 사용됩니다.

넥스트 키 락

  • 레코드 락 + 갭 락 형태의 잠금을 의미합니다.
  • 해당 락은 binlog 기반으로 Replica 장비에서 실행될 때 데이터 일관성을 보장하기 위해 고안된 Lock입니다.
  • 넥스트 키 락과 갭 락으로 인해, 데드락이 발생하거나 대기가 발생하는 경우가 있어, 최대한 줄이는 것이 좋습니다.
-- 테스트 테이블 생성 * 데이터 Insert
mysql> CREATE TABLE next_key (A int NOT NULL AUTO_INCREMENT PRIMARY KEY, b int DEFAULT NULL, c char(1) DEFAULT 'Y', INDEX ix_b(b));
Query OK, 0 rows affected (0.03 sec)

mysql> INSERT INTO next_key (b, c) VALUES (1,'N'), (4,'S'), (10, NULL),(11, 'Y'), (14,'Y'), (15, 'N');
Query OK, 6 rows affected (0.02 sec)
Records: 6  Duplicates: 0  Warnings: 0

-- 데이터 확인
mysql> SELECT * FROM next_key;
+---+------+------+
| A | b    | c    |
+---+------+------+
| 1 |    1 | N    |
| 2 |    4 | S    |
| 3 |   10 | NULL |
| 4 |   11 | Y    |
| 5 |   14 | Y    |
| 6 |   15 | N    |
+---+------+------+
6 rows in set (0.01 sec)


-- Session 1
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT * FROM next_key WHERE b BETWEEN 5 AND 13 FOR UPDATE;
+---+------+------+
| A | b    | c    |
+---+------+------+
| 3 |   10 | NULL |
| 4 |   11 | Y    |
+---+------+------+
2 rows in set (0.03 sec)


-- Session 2 (b 데이터 범위별 Insert 실행)
mysql> INSERT INTO next_key (b,c) VALUES (3, 'S');
Query OK, 1 row affected (0.01 sec)


-- 4 <= b < 14 Lock으로 인해 wait_timeout 발생
mysql> INSERT INTO next_key (b,c) VALUES (4, 'A');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql> INSERT INTO next_key (b,c) VALUES (10, 'A');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql> INSERT INTO next_key (b,c) VALUES (13, 'A');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

-- b < 4 / b >=14 Lock의 영향을 받지 않고 Insert 성공
mysql> INSERT INTO next_key (b,c) VALUES (14, 'A');
Query OK, 1 row affected (0.02 sec)

자동 증가 락

  • AUTO_INCREMENT 속성을 가진 컬럼에 대해 채번을 위해 사용하는 락입니다.
  • 해당 Lock은 테이블 수준의 잠금이 사용됩니다.
  • Insert, Replace와 같이 새로운 값을 저장하는 쿼리에서만 사용됩니다.

  • 관련 옵션
    • innodb_autoinc_lock_mode=0
      • 모든 Insert문장에 자동 증가 락을 사용
    • innodb_autoinc_lock_mode=1 [5.7이하 기본값]
      • 단순 Insert문 (INSERT INTO ~ VALUES)처럼 몇건이 Insert될지 예측이 되는 경우, 훨씬 가볍고 빠른 래치 (=mutex)를 이용해 처리합니다.
      • insert ~ select문과 같이 몇 건인지 알 수 없을 경우 자동 증가 락을 할당합니다.
      • 대량 Insert가 발생할 때, 한번에 여러 개의 채번값을 할당하여 사용합니다.
      • 사용되지 못할 경우 값을 버리고 다음 번호를 할당하는데 이 경우 채번값이 번호가 연속되지 않는 케이스가 발생할 수 있습니다.
    • innodb_autoinc_lock_mode=2 [8.0이상 기본값]
      • 자동 증가 락을 사용하지 않고, 항상 뮤텍스를 이용해 Insert 작업을 진행합니다.
      • 대량 Insert문 진행 중에 다른 Insert가 가능합니다.
      • 복제 구성된 환경에서는 결과 값이 달라질 수 있으므로 주의해야 됩니다.
-- MySQL 8.0 기본값
mysql> show global variables like '%autoinc_lock_mode%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 2     |
+--------------------------+-------+
1 row in set (0.01 sec)

격리 수준(Isolation-Level)

  • 여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션의 변경/조회 작업을 제어하는 방법입니다.
  • 총 4개의 단계로 이루어져 있으며 단계가 높아질수록 동시성이 떨어집니다.
    1. READ UNCOMMITTED
    2. READ COMMITTED
    3. REPEATABLE READ
    4. SERIALIZABLE
  • Isolation Level에 따라 부정합 현상이 발생할 수 있으며, 아래 표와 같습니다.
    • dirty Read: Commit 되지 않은 데이터를 다른 트랜잭션이 조회 가능
    • Non Repeatable Read: 동일 트랜잭션 내에서 Select문의 결과가 다름
    • Phantom Read: 동일 트랜잭션에서 조회 시 없던 결과가 생김.

Isolation Level에 따른 특징

  • READ UNCOMMITTED
    • Commit 여부에 상관없이 다른 트랜잭션 결과를 그대로 조회합니다.
    • 위에 설명한 세가지 부정합 현상이 모두 발생합니다.
-- Session 1
mysql> SET SESSION transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT * FROM next_key;
+---+------+------+
| A | b    | c    |
+---+------+------+
| 1 |    1 | N    |
| 2 |    4 | S    |
| 3 |   10 | NULL |
| 4 |   11 | Y    |
| 5 |   14 | Y    |
+---+------+------+
5 rows in set (0.01 sec)

-- Session 2
START TRANSACTION;
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.01 sec)

mysql> DELETE FROM next_key WHERE A=5;
Query OK, 1 row affected (0.03 sec)

mysql> INSERT next_key (b) VALUES (100);
Query OK, 1 row affected (0.02 sec)

-- Session 1 (Commit 되지 않은 결과 확인) = Dirty Read
mysql> SELECT * FROM next_key;
+----+------+------+
| A  | b    | c    |
+----+------+------+
|  1 |    1 | N    |
|  2 |    4 | S    |
|  3 |   10 | NULL |
|  4 |   11 | Y    |
| 15 |  100 | Y    |
+----+------+------+
5 rows in set (0.03 sec)

-- Session 2 
mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)

-- Session 1 (동일 트랜잭션 내 변경된 결과: Non-Repeatable Read / 이전에 없던 b=100 생성: Phantom-Read)
mysql> SELECT * FROM next_key;
+---+------+------+
| A | b    | c    |
+---+------+------+
| 1 |    1 | N    |
| 2 |    4 | S    |
| 3 |   10 | NULL |
| 4 |   11 | Y    |
| 5 |   14 | Y    |
+---+------+------+
5 rows in set (0.01 sec)
  • READ COMMITTED
    • 각 명령문마다 Commit 트랜잭션 ID값을 사용해 레코드 버전 제어
    • Non Repeatable Read 현상이 발생 (InnoDB에선 Phantom Read 미발생)
    • 각 명령문마다 자기만의 스냅샷을 소유하여, 정합성을 유지할 수 있음.
    • Lock이 발생하지 않음
-- Session 1
mysql> SET SESSION TRANSACTION isolation level READ COMMITTED;
Query OK, 0 rows affected (0.01 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT * FROM next_key;
+---+------+------+
| A | b    | c    |
+---+------+------+
| 1 |    1 | N    |
| 2 |    4 | S    |
| 3 |   10 | NULL |
| 4 |   11 | Y    |
| 5 |   14 | Y    |
+---+------+------+
5 rows in set (0.03 sec)


-- Session 2
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM next_key WHERE A=5;
Query OK, 1 row affected (0.02 sec)

mysql> INSERT next_key(B) VALUES (100);
Query OK, 1 row affected (0.02 sec)

-- Session 1 (동일한 결과 / Dirty Read 방지)
mysql> SELECT * FROM next_key;
+---+------+------+
| A | b    | c    |
+---+------+------+
| 1 |    1 | N    |
| 2 |    4 | S    |
| 3 |   10 | NULL |
| 4 |   11 | Y    |
| 5 |   14 | Y    |
+---+------+------+
5 rows in set (0.02 sec)

-- Session 2
mysql> COMMIT;

-- Session 1 (동일 트랜잭션 내 변경된 결과: Non-Repeatable Read / 이전에 없던 b=100 생성: Phantom-Read)
mysql> SELECT * FROM next_key;
+----+------+------+
| A  | b    | c    |
+----+------+------+
|  1 |    1 | N    |
|  2 |    4 | S    |
|  3 |   10 | NULL |
|  4 |   11 | Y    |
| 16 |  100 | Y    |
+----+------+------+
5 rows in set (0.02 sec)
  • REPEATABLE READ
    • MySQL의 기본 isolation level입니다.
    • 트랜잭션이 시작될 때 스냅샷을 생성하고, 언제나 동일한 결과셋을 읽도록 제어합니다.
    • 동일 트랜잭션에서 내에서 같은 결과를 읽어와 Phantom READ를 방지하지만, 발생하는 예외 케이스가 존재합니다.
      • (ex) SELECT ~ FOR UPDATE와 같은 조회에 잠금 할당이 필요한 명령문
  • SERIALIZABLE
    • SELECT가 하나의 트랜잭션이며, 조회 시 S-Lock이 할당됩니다.
    • select문에 영향받는 object에 대해 DML, DDL 불가합니다.
    • 가장 높은 격리 수준이며, 동시성이 제일 떨어집니다.