λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°

MySQL

MySQL 은 읽기/μ“°κΈ° μΏΌλ¦¬μ—μ„œ μ–΄λ–€ 락을 μ‚¬μš©ν• κΉŒ?

이 κΈ€μ—μ„œλŠ” MySQL 8.0 μ—μ„œ λ‚΄λΆ€μ μœΌλ‘œ μ–Έμ œ 락을 μ‚¬μš©ν•˜λŠ”μ§€μ— λŒ€ν•΄ μ•Œμ•„κ°€λŠ” κ³Όμ •μœΌλ‘œ MySQL μ—”μ§„ 레벨의 락은 아직 ν•™μŠ΅μ΄ λΆ€μ‘±ν•˜μ—¬ ν•΄λ‹Ή κΈ€μ—μ„œ μ œμ™Έν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 

 

μΆ”κ°€λ‘œ Real MySQL 을 읽고, 곡유 락과 배타 락에 λ°°κ²½ 지식이 μžˆλŠ” 뢄듀이 μ‰½κ²Œ 이해 될 것 κ°™μœΌλ©°, 결둠은 '마치며' 에 μ •λ¦¬ν•΄λ‘μ—ˆμŠ΅λ‹ˆλ‹€.

 

λͺ©μ°¨λŠ” λ‹€μŒ μˆœμ„œλ‘œ μ§„ν–‰λ©λ‹ˆλ‹€.

  • ν•œ νŠΈλžœμž­μ…˜μ—μ„œ 곡유 락/배타 락 νšλ“ μ‹œ, λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œ κΈ°λ‹€λ¦Ό 없이 μ‹€ν–‰ν•  수 μžˆλŠ” 쿼리 μ†Œκ°œ
  • 곡유 락/배타 락 μ‚¬μš© μ‹œ, λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œ μ“°κΈ° μž‘μ—…μ΄ λŒ€κΈ°ν•˜λŠ” 이유
  • UPDATE / DELETE 쿼리 μ‹œ WHERE 절의 인덱슀 μ—¬λΆ€μ˜ μ€‘μš”μ„±
  • MySQL8.0 Repetable Read μ—μ„œ Phantom Read κ°€ λ°œμƒν•˜λŠ” 경우
  • UPDATE / DELETE 쿼리λ₯Ό μ‹€ν–‰ ν•  λ•Œ, MySQL 이 λ‚΄λΆ€μ μœΌλ‘œ 배타 락을 μ‚¬μš©ν•˜μ§€λ§Œ select...for update(JPA의 비관적 락)을 μ‚¬μš©ν•˜λŠ” 이유
  • 마치며

 

1. ν•œ νŠΈλžœμž­μ…˜μ—μ„œ 곡유 락/배타 락 νšλ“ μ‹œ, λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œ κ°€λŠ₯ν•œ 쿼리 μ†Œκ°œ

μ‹€ν—˜ ν™˜κ²½

  • DBMS: Mysql 8.0 InnoDB
  • 격리 μˆ˜μ€€: Repeatable Read 

 

μ‹€ν—˜μ— 쓰일 ν…Œμ΄λΈ”κ³Ό λ°μ΄ν„°μž…λ‹ˆλ‹€.

CREATE TABLE example (
	id BININT,
    name VARCHAR(255)
);

INSERT INTO example (id, name) VALUES (1, "a");   // 1번 row
INSERT INTO example (id, name) VALUES (2, "b");   // 2번 row

μ‹€ν—˜ 1 : 배타락 & 곡유락

 

  • μ™Όμͺ½ νŠΈλžœμž­μ…˜: 1번 행에 λŒ€ν•΄ 배타락(Exclusive Lock)을 νšλ“ν•©λ‹ˆλ‹€.
    • select ... for update
  • 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜: 1번 행에 λŒ€ν•΄ 곡유락(Shared Lock)을 μ‹œλ„ν•©λ‹ˆλ‹€.
    • LOCK IN SHARE MODE (Mysql 5.7버전 μ΄ν•˜)
    • select ... for share ( Mysql 8.0버전 이상)

 

κ²°κ³Ό

  • 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜μ΄ κ³΅μœ λ½μ„ νšλ“ν•˜μ§€ λͺ»ν•˜κ³  λŒ€κΈ° μƒνƒœμ— λ“€μ–΄κ°”μŠ΅λ‹ˆλ‹€.
  • 배타락이 κ±Έλ¦° μƒνƒœμ—μ„œλŠ” κ³΅μœ λ½λ„ νšλ“ν•  수 μ—†μœΌλ―€λ‘œ, 배타락 μ—­μ‹œ λ‹Ήμ—°νžˆ νšλ“ν•  수 μ—†μŠ΅λ‹ˆλ‹€.

μ‹€ν—˜ 1

 

 

μ‹€ν—˜ 2 : 배타락 & κΈ°λ³Έ SELECT

  • μ™Όμͺ½ νŠΈλžœμž­μ…˜: 1번 행에 λŒ€ν•΄ 배타락을 νšλ“ν•©λ‹ˆλ‹€.
  • 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜: 1번 행에 λŒ€ν•΄ SELECT 쿼리λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.

κ²°κ³Ό

  • 기본적인 SELECT μΏΌλ¦¬λŠ” μ„±κ³΅μ μœΌλ‘œ μ‹€ν–‰λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
  • 즉, 배타락이 κ±Έλ¦° μƒνƒœμ—μ„œλ„ 읽기 μž‘μ—…(SELECT)은 ν—ˆμš©λ©λ‹ˆλ‹€.
  • λ°°νƒ€λ½μ—μ„œ 읽기가 ν—ˆμš© μ•ˆλœλ‹€λŠ” 말은 Locking Read κ°€ μ•ˆλ˜λŠ” 것이지 Non-Locking Read λŠ” κ°€λŠ₯ν•©λ‹ˆλ‹€. Locking Read κ°€ 곡유락, 배타락같이 락을 μ‚¬μš©ν•΄μ„œ λ ˆμ½”λ“œλ₯Ό 직접 μ½λŠ” 것이고, Non-Locking Read λŠ” 일반적인 select 문을 μ˜λ―Έν•˜λ©° μŠ€λƒ…μƒ·μ— μ˜ν•œ 언두 μ˜μ—­μ„ μ½λŠ” κ²ƒμž…λ‹ˆλ‹€. 

μ‹€ν—˜ 2

 

μ‹€ν—˜ 3 : 배타락 & UPDATE/DELETE

  • μ™Όμͺ½ νŠΈλžœμž­μ…˜: 1번 행에 λŒ€ν•΄ 배타락을 νšλ“ν•©λ‹ˆλ‹€.
  • 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜: 1번 행에 λŒ€ν•΄ UPDATE λ˜λŠ” DELETE 쿼리λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.

κ²°κ³Ό

  • 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜μ€ λŒ€κΈ° μƒνƒœμ— λ“€μ–΄κ°”μŠ΅λ‹ˆλ‹€.
  • 배타락이 κ±Έλ¦° μƒνƒœμ—μ„œλŠ” μ“°κΈ° μž‘μ—…(UPDATE, DELETE)이 μ°¨λ‹¨λ©λ‹ˆλ‹€.

μ‹€ν—˜ 3

 

μ‹€ν—˜ 4 : 곡유락 & SELECT/UPDATE/DELETE

  • μ™Όμͺ½ νŠΈλžœμž­μ…˜: 1번 행에 λŒ€ν•΄ 곡유락(Shared Lock)을 νšλ“ν•©λ‹ˆλ‹€.
  • 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜: 1번 행에 λŒ€ν•΄ SELECT, UPDATE, λ˜λŠ” DELETE 쿼리λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.

κ²°κ³Ό

  • SELECT μΏΌλ¦¬λŠ” 정상 μ‹€ν–‰λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
  • ν•˜μ§€λ§Œ UPDATE와 DELETE μΏΌλ¦¬λŠ” λŒ€κΈ° μƒνƒœμ— λ“€μ–΄κ°”μŠ΅λ‹ˆλ‹€.

select λŠ” 정상 μ‹€ν–‰, update λŠ” 차단

 

μ‹€ν—˜5 : 곡유락 & INSERT

 

INSERT μ—μ„œλŠ” μ‹€ν—˜μ΄ μ•½κ°„ λ‹€λ¦…λ‹ˆλ‹€.

μ‹€ν—˜μ— μ‚¬μš©λ  λ ˆμ½”λ“œμž…λ‹ˆλ‹€.

// example ν…Œμ΄λΈ”μ— 3개의 λ ˆμ½”λ“œ 쑴재, id λŠ” PK
mysql> select * from example;
+-----+------+
| id  | name |
+-----+------+
|   1 | r    |
|   2 | b    |
| 500 | a    |
+-----+------+

 

μ‹€ν—˜ 5-1. INSERT κ°€ λŒ€κΈ°ν•˜μ§€ μ•Šμ„ λ•Œ μž…λ‹ˆλ‹€.

1번 νŠΈλžœμž­μ…˜μ—μ„œ idκ°€ 1λΆ€ν„° 2κΉŒμ§€μΈ id 에 κ³΅μœ λ½μ„ νšλ“ν•˜λ‹ˆ, id κ°€ 1κ³Ό 2인 λ ˆμ½”λ“œμ— λ ˆμ½”λ“œ 락이 κ±Έλ €μžˆμŠ΅λ‹ˆλ‹€.

// νŠΈλžœμž­μ…˜ 1μ—μ„œ idκ°€ 1λΆ€ν„° 2κΉŒμ§€ 곡유락 νšλ“
mysql> select * from example where id between 1 and 2 for share;
+----+------+
| id | name |
+----+------+
|  1 | r    |
|  2 | b    |
+----+------+

// REC_NOT_GAP은 λ ˆμ½”λ“œ 자체λ₯Ό μž κ·Έμ§€λ§Œ, κ°­(Gap)은 μž κ·Έμ§€ μ•ŠλŠ” μž κΈˆμ„ 의미둜 ν•΄λ‹Ή λ ˆμ½”λ“œλ§Œ λ³΄ν˜Έν•˜κ³ , μƒˆλ‘œμš΄ λ ˆμ½”λ“œ μ‚½μž…μ€ ν—ˆμš©ν•©λ‹ˆλ‹€.
// id κ°€ 1κ³Ό 2인 λ ˆμ½”λ“œκ°€ 곡유락인 λ ˆμ½”λ“œ 락을 νšλ“ν–ˆμŠ΅λ‹ˆλ‹€.
mysql> select * from performance_schema.data_locks;
+--------+------------------------------------------------------------------------+
| ENGINE | ... | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
+--------+------------------------------------------------------------------------+
| INNODB | ... | NULL       | TABLE     | IS            | GRANTED     | NULL      |
| INNODB | ... | PRIMARY    | RECORD    | S             | GRANTED     | 2         |
| INNODB | ... | PRIMARY    | RECORD    | S,REC_NOT_GAP | GRANTED     | 1         |
+--------+------------------------------------------------------------------------+

 

2번 νŠΈλžœμž­μ…˜μ—μ„œ idκ°€ 1κ³Ό 2λ₯Ό λ²—μ–΄λ‚˜λŠ” μ˜μ—­μ— idκ°€ 10κ³Ό 501인 λ ˆμ½”λ“œλ₯Ό INSERTν–ˆκ³ , μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€.

mysql> insert into example (id, name) values (10, "c");
Query OK, 1 row affected (0.00 sec)

mysql> insert into example (id, name) values (501, "c");
Query OK, 1 row affected (0.00 sec)

 

μ‹€ν—˜ 5-2. INSERT κ°€ λŒ€κΈ°ν•  λ•Œ μž…λ‹ˆλ‹€.

νŠΈλžœμž­μ…˜ 1μ—μ„œ id κ°€ 2 λΆ€ν„° 500 사이인 λ ˆμ½”λ“œλ₯Ό μ‘°νšŒν•˜λ©° κ³΅μœ λ½μ„ νšλ“ν•©λ‹ˆλ‹€.

// νŠΈλžœμž­μ…˜ 1μ—μ„œ idκ°€ 2λΆ€ν„° 500κΉŒμ§€ 곡유락 νšλ“
mysql> select * from example where id between 2 and 500 for share;
+----+------+
| id | name |
+----+------+
|   2 |  b  |
| 500 |  a  |
+----+------+

// LOCK이 κ±Έλ¦° 데이터에 supremum pseudo-record κ°€ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
mysql> select * from performance_schema.data_locks;
+--------+-------------------------------------------------------------------------------------+
| ENGINE | ... | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA              |
+--------+-------------------------------------------------------------------------------------+
| INNODB | ... | NULL       | TABLE     | IS            | GRANTED     | NULL                   |
| INNODB | ... | PRIMARY    | RECORD    | S             | GRANTED     | supremum pseudo-record |
| INNODB | ... | PRIMARY    | RECORD    | S             | GRANTED     | 500                    |
| INNODB | ... | PRIMARY    | RECORD    | S,REC_NOT_GAP | GRANTED     | 2                      |
+--------+-------------------------------------------------------------------------------------+

 

νŠΈλžœμž­μ…˜ 2μ—μ„œ μ‹€ν—˜ 5-1κ³Ό 달리 INSERT κ°€ μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μ‹€ν—˜ 5-11처럼 λ ˆμ½”λ“œ 락이 λ²—μ–΄λ‚œ μ˜μ—­μΈ id=501 인 μ˜μ—­μ— INSERT λ₯Ό μ‹œλ„ν–ˆμ§€λ§Œ, 락 νƒ€μž„μ•„μ›ƒμ΄ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

μ‹€ν—˜5-1처럼 곡유 락 μ˜μ—­ 밖에 INSERT λ₯Ό μ‹œλ„ν–ˆμ§€λ§Œ, μ‹€ν—˜ 5-2 만 μ‹€νŒ¨ν•œ μ΄μœ κ°€ λ¬΄μ—‡μΌκΉŒμš”?

mysql> insert into example (id, name) values (100, "z");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into example (id, name) values (501, "z");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

 

LOCK_DATE 에 supremum pseudo-record κ°€ 있기 λ•Œλ¬Έμž…λ‹ˆλ‹€. supremum pseudo-record λž€ 인덱슀 νŽ˜μ΄μ§€μ˜ 끝을 λ‚˜νƒ€λ‚΄λŠ” κ°€μƒμ˜ λ ˆμ½”λ“œλ‘œ 사싀상 "κ°­ 잠금" 역할을 ν–ˆκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

 

κ²°κ³Ό

  • supremum pseudo-record 값은 사싀상 ∞ 와 κ°™λ‹€ν•©λ‹ˆλ‹€. κ·Έλž˜μ„œ, supremum 여뢀에 따라 INSERT 문이 락에 μ˜ν•΄ λŒ€κΈ°ν•  μˆ˜λ„, 아닐 μˆ˜λ„ 있게 λ©λ‹ˆλ‹€.

μ‹€ν—˜ κ²°λ‘ 

κ²°κ³Ό 곡유 락 배타 락 κΈ°λ³Έ select update / delete insert
곡유 락 O X O X λ ˆμ½”λ“œ 락일 경우 O
κ°­ 락이 κ±Έλ €μžˆμ„ 경우 X
배타 락 X X O X

 

락 μ‚¬μš© μ‹œ, λ‹€μ–‘ν•œ 상황에 μžˆμ–΄μ„œ μ–΄λ–€ μž‘μ—…μ΄ ν—ˆμš©λ˜κ³ , λŒ€κΈ°ν•˜λŠ”μ§€ μ•Œμ•„λ΄€μŠ΅λ‹ˆλ‹€.

이λ₯Ό 톡해 update / delete 도 락을 μ‚¬μš©ν•  것 κ°™λ‹€λŠ” 예감이 λ“€λ©°, 이에 λŒ€ν•΄ μ°Ύμ•„λ΄€μŠ΅λ‹ˆλ‹€.


* μ°Έκ³  - 낙관적 락은 JPAμ—μ„œ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯으둜 DB의 곡유 락과 λ¬΄κ΄€ν•©λ‹ˆλ‹€

 

2. 곡유 락/배타 락 μ‚¬μš© μ‹œ, λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œ μ“°κΈ° μž‘μ—…μ΄ λŒ€κΈ°ν•˜λŠ” 이유

1. FOREIGN KEY μ œμ•½ 쑰건이 ν…Œμ΄λΈ”μ— μ •μ˜λ˜μ–΄ μžˆλŠ” 경우, μ™Έλž˜ν‚€ μžˆλŠ” μ»¬λŸΌμ— INSERT / UPDATE / DELETE μž‘μ—…μ„ μˆ˜ν–‰ν•΄λ„ λ˜λŠ”μ§€ λΆ€λͺ¨ ν…Œμ΄λΈ”(PK) 에 λ ˆμ½”λ“œ μˆ˜μ€€μ˜ 곡유 락(shared record-level locks)을 μ„€μ •ν•©λ‹ˆλ‹€. 

  • If a FOREIGN KEY constraint is defined on a table, any insert, update, or delete that requires the constraint condition to be checked sets shared record-level locks on the records that it looks at to check the constraint. InnoDB also sets these locks in the case where the constraint fails.

2. μœ λ‹ˆν¬ ν‚€λŠ” INSERT μ‹œ μ€‘λ³΅λœ 값을 μ²΄ν¬ν•˜κΈ° μœ„ν•΄ 읽기 μž κΈˆμ„, λ ˆμ½”λ“œλ₯Ό μΆ”κ°€ν•  λ•ŒλŠ” μ“°κΈ° μž κΈˆμ„ μ‚¬μš©ν•˜λ©° 이 κ³Όμ •μ—μ„œ λ°λ“œλ½μ΄ μ•„μ£Ό 빈번히 λ°œμƒν•©λ‹ˆλ‹€.(Real MySQL 8.0 좜처)

 

3. UPDATE 와 DELETE μƒνƒœμ—μ„œλ„ λ‚΄λΆ€μ μœΌλ‘œ 락을 μ‚¬μš©ν•©λ‹ˆλ‹€. 이 λ•Œ, μ‚¬μš©λ˜λŠ” 락의 μ’…λ₯˜λŠ” 쿼리λ₯Ό μ‹€ν–‰ν•˜κΈ° μœ„ν•΄ μ‚¬μš©λ˜λŠ” μΈλ±μŠ€μ™€ WHERE 쑰건에 따라 λ ˆμ½”λ“œ 락인지, κ°­ 락인지 λ„₯슀트 ν‚€ 락인지 λ‹€λ₯΄κ² μ§€λ§Œ 이듀은 κ²°κ΅­ 배타 락을 μ‚¬μš©ν•©λ‹ˆλ‹€. μΆ”κ°€λ‘œ, UPDATE / DELETE μ‹œ WHERE μ‘°κ±΄μ ˆμ— μΈλ±μŠ€κ°€ ν¬ν•¨λœ 컬럼이라면 λ ˆμ½”λ“œ 락을, μΈλ±μŠ€κ°€ μ—†λŠ” μ»¬λŸΌμ„ μ§€μ •ν•  경우 ν•΄λ‹Ή 컬럼이 WHERE 쑰건을 λ§Œμ‘±ν•  λ•ŒκΉŒμ§€ 읽은 λͺ¨λ“  λ ˆμ½”λ“œμ— (InnoDBλŠ” λ ˆμ½”λ“œμ˜ μΈλ±μŠ€μ—) 락을 κ±Έκ²Œλ©λ‹ˆλ‹€.

    • For locking reads (SELECT with FOR UPDATE or FOR SHARE), UPDATE, and DELETE statements, the locks that are taken depend on whether the statement uses a unique index with a unique search condition or a range-type search condition.
    • UPDATE ... WHERE ... sets an exclusive next-key lock on every record the search encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.
    • μ•„λž˜ 사진은 νŠΈλžœμž­μ…˜μ„ μ—΄κ³ , update 쿼리λ₯Ό μ‹€ν–‰ ν›„ 락이 κ±Έλ €μžˆλŠ” 데이터λ₯Ό λ³΄λŠ” λͺ…λ Ήμ–΄κΉŒμ§€ μ‹€ν–‰ν•œ μ‚¬μ§„μž…λ‹ˆλ‹€. LOCK_STATUS κ°€ GRATED λŠ” 락을 νšλ“ν•œ μƒνƒœμž„μ„ μ˜λ―Έν•˜λ©°, LOCK_DATE λŠ” λ ˆμ½”λ“œμ˜ ν•΄μ‹œκ°’μ΄λ©°, LOCK_MODE λŠ” X 둜 배타락을 μ˜λ―Έν•©λ‹ˆλ‹€. 그리고, LOCK_TYPE 을 톡해 락 μœ ν˜•μ€ λ ˆμ½”λ“œ λ½μž„μ„ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. 즉, update μΏΌλ¦¬λŠ” 배타 락이 κ±Έλ¦½λ‹ˆλ‹€.

 

4. MySQL 8.0 의 InnoDB μŠ€ν† λ¦¬μ§€ 엔진은 λ ˆμ½”λ“œκ°€ μžμ²΄κ°€ μ•„λ‹ˆλΌ 인덱슀의 λ ˆμ½”λ“œλ₯Ό 잠그게 λ©λ‹ˆλ‹€. μΈλ±μŠ€κ°€ ν•˜λ‚˜λ„ μ—†λŠ” ν…Œμ΄λΈ”μ΄λ”λΌλ„ λ‚΄λΆ€μ μœΌλ‘œ μžλ™ μƒμ„±λœ ν΄λŸ¬μŠ€ν„° 인덱슀λ₯Ό μ΄μš©ν•΄ λ ˆμ½”λ“œλ₯Ό μž κΈ‰λ‹ˆλ‹€. 

 

좜처

https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html

https://dev.mysql.com/doc/refman/8.0/en/create-table-foreign-keys.html#foreign-key-locking

- Real MySQL 1ꢌ의 νŠΈλžœμž­μ…˜κ³Ό 잠금

 

즉, MySQL 8.0μ—μ„œλŠ” μ™Έλž˜ ν‚€ μ œμ•½ 쑰건과 UPDATE / DELETE μ‹œμ—λ„ 락듀이 λ‚΄λΆ€μ μœΌλ‘œ μ‚¬μš©λ©λ‹ˆλ‹€. 이둜 인해 μœ„ μ‹€ν—˜μ—μ„œ ν•œ νŠΈλžœμž­μ…˜μ΄ 배타 락/곡유 락을 νšλ“ν–ˆμ„ λ•Œ, λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œ λ‹¨μˆœ INSERT / UPDATE / DELETE μž‘μ—…μ„ μˆ˜ν–‰ν•˜λ”λΌλ„ MySQL λ‚΄λΆ€μ μœΌλ‘œ μžλ™μœΌλ‘œ μƒμ„±λœ ν΄λŸ¬μŠ€ν„° 인덱슀λ₯Ό μ΄μš©ν•΄ λ ˆμ½”λ“œ 락을 νšλ“ν•˜κ³ μž ν–ˆκ³ , λ ˆμ½”λ“œ 락은 배타 락이기 λ•Œλ¬Έμ— λŒ€κΈ° μƒνƒœμ— λ“€μ–΄κ°”λ˜ κ²ƒμž…λ‹ˆλ‹€.

 

즉, MySQL 8.0 μ—μ„œ MVCCλŠ” κΈ°λ³Έ select 쿼리와 같이 잠금 μ—†λŠ” 읽기λ₯Ό μ§€μ›ν•˜κΈ° μœ„ν•¨μ΄κ³ , 곡유/배타 락은 μ“°κΈ°λ₯Ό μœ„ν•¨μœΌλ‘œ 정리할 수 μžˆμŠ΅λ‹ˆλ‹€.

 

* INSERT / UPDATE / DELETE μž‘μ—…μ„ ν•˜λ €λŠ” λ ˆμ½”λ“œμ˜ λ²”μœ„κ°€ 각각의 λ ˆμ½”λ“œ μˆ˜μ€€μΈμ§€, νŠΉμ • λ²”μœ„μΈμ§€, ν…Œμ΄λΈ” μˆ˜μ€€μΈμ§€μ— 따라 μ‚¬μš©λ˜λŠ” MySQL λ‚΄λΆ€μ—μ„œ μ‚¬μš©λ˜λŠ” 락의 μ’…λ₯˜λŠ” λ‹€λ¦…λ‹ˆλ‹€.

* μ–Έμ œ μ–΄λ–€ 락이 μ“°μ΄λŠ”μ§€ μžμ„Ένžˆ μ•Œκ³  μ‹Άλ‹€λ©΄ 좜처의 Locking λΆ€λΆ„μ΄λ‚˜ Real MySQL의 Lock 파트λ₯Ό μ°Έκ³ ν•˜μ‹œκΈΈ μΆ”μ²œλ“œλ¦½λ‹ˆλ‹€.

 

3. UPDATE / DELETE 쿼리 μ‹œ WHERE 절의 인덱슀 μ—¬λΆ€μ˜ μ€‘μš”μ„±

λ˜ν•œ, MySQL은 UPDATEλ‚˜ DELETE μž‘μ—… μ‹œ λ‚΄λΆ€μ μœΌλ‘œ 배타 락을 μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— WHERE 쑰건이 μ€‘μš”ν•©λ‹ˆλ‹€.

WHERE 쑰건에 μ‚¬μš©λ˜λŠ” μ»¬λŸΌμ— μ μ ˆν•œ μΈλ±μŠ€κ°€ μ—†λŠ” 경우, μ“°κΈ° μž‘μ—…μ„ ν•  λ ˆμ½”λ“œλΏλ§Œ μ•„λ‹ˆλΌ λ‹€λ₯Έ λ ˆμ½”λ“œμ—λ„ 락이 걸릴 수 있고, μ΅œμ•…μ˜ 경우 ν…Œμ΄λΈ” λ‹¨μœ„λ‘œ 락이 걸릴 수 μžˆμŠ΅λ‹ˆλ‹€. 즉, UPDATE / DELETE 쿼리λ₯Ό 처리 ν•  λ•Œ, WHERE 쑰건에 PK와 같은 μΈλ±μŠ€κ°€ μžˆλŠ” μ»¬λŸΌμ„ μ‚¬μš©ν•΄μ•Ό ν•œλ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€.

 

WHERE 쑰건에 μΈλ±μŠ€κ°€ μ—†λŠ” UPDATE / DELETE 쿼리가 μ‹€ν–‰λ˜λ©΄ ν…Œμ΄λΈ”μ˜ λͺ¨λ“  행을 μŠ€μΊ”ν•˜κ³ , λΆˆν•„μš”ν•œ λ ˆμ½”λ“œμ—λ„ 락이 섀정될 수 μžˆμŠ΅λ‹ˆλ‹€.

 

예λ₯Ό λ“€μ–΄, UPDATE / DELETE ... WHERE last_name = "μ’…ν˜" and first_name = "μ•ˆ" μ΄λž€ 쿼리가 μžˆμ„ λ•Œ, last_name 이 "μ’…ν˜"인 λ ˆμ½”λ“œκ°€ 1000건이라고 κ°€μ •ν•˜κ³ , κ·Έ μ€‘μ—μ„œ first_name 이 "μ•ˆ" 을 λ§Œμ‘±ν•˜λŠ” λ ˆμ½”λ“œκ°€ 단 1개라고 κ°€μ •ν•˜κ² μŠ΅λ‹ˆλ‹€. 이 λ•Œ, last_name μ—λ§Œ μΈλ±μŠ€κ°€ μ„€μ •λ˜μ—ˆλ‹€λ©΄ last_name 이 "μ’…ν˜"을 λ§Œμ‘±ν•˜λŠ” 1000건의 λ ˆμ½”λ“œμ— ν•΄λ‹Ήν•˜λŠ” μΈλ±μŠ€μ— 락이 걸리게 λ©λ‹ˆλ‹€.(* MySQL 8.0μ—μ„œ 락은 λ ˆμ½”λ“œκ°€ μ•„λ‹Œ μΈλ±μŠ€μ— κ²λ‹ˆλ‹€.)

 

이 λ•Œ, λ ˆμ½”λ“œ 락이 κ±Έλ¦° 1000건의 λ ˆμ½”λ“œ 쀑, first_name = "μ•ˆ" 을 λ§Œμ‘±ν•˜λŠ” 1건의 데이터λ₯Ό μ œμ™Έν•œ 999건은 λΆˆν•„μš”ν•˜κ²Œ 락이 걸리게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ§Œμ•½, last_name 에도 μΈλ±μŠ€κ°€ μ—†λ‹€λ©΄ last_name="μ’…ν˜" 을 μ°ΎκΈ° μœ„ν•΄ MySQL 은 ν…Œμ΄λΈ” ν’€ μŠ€μΊ”μ„ ν•˜κ²Œ 될 것이고, 이 κ³Όμ •μ—μ„œ ν…Œμ΄λΈ”μ˜ λͺ¨λ“  λ ˆμ½”λ“œμ—λ„ 락을 걸게 λ©λ‹ˆλ‹€.

 

μ΄λŸ¬ν•œ 이유둜 UPDATE / DELETE μ‹œ μΈλ±μŠ€κ°€ μžˆλŠ” μ»¬λŸΌμ„ WHERE μ‘°κ±΄μ ˆμ— 쀌으둜써 λΆˆν•„μš”ν•œ λ ˆμ½”λ“œμ— 락이 κ±Έλ¦¬λŠ” 것을 λ°©μ§€ν•˜λŠ” 것이 μ€‘μš”ν•©λ‹ˆλ‹€.

 

λ§Œμ•½, μ˜ˆμ‹œμ²˜λŸΌ μΈλ±μŠ€κ°€ μ—†λŠ” μ»¬λŸΌμ„ WHERE μ‘°κ±΄μ ˆμ— μ€˜μ•Όν•œλ‹€λ©΄ (last_name , first_name) 두 컬럼의 λ©€ν‹° 컬럼 인덱슀λ₯Ό 생성해주며, 더 λ‚˜μ•„κ°€ first_name 의 카디널리티가 더 λ†’κΈ° λ•Œλ¬Έμ— (first_name , last_name) 으둜 인덱슀λ₯Ό μƒμ„±ν•˜λ©° 락을 λ°©μ§€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

* 좜처 : Real MySQL 5.3.2 μΈλ±μŠ€μ™€ 잠금

 

4. MySQL 8.0 Repetable Read μ—μ„œ 비관적 락 μ‚¬μš© μ‹œ, Phantom Read κ°€ λ°œμƒν•˜λŠ” 경우

μΆ”κ°€λ‘œ, Real MySQL 8.0 μ—μ„œ select + select ... for update μ‘°ν•©μœΌλ‘œ 쿼리 μ‹œ, Phantom Read κ°€ λ°œμƒ ν•  μˆ˜λ„ μžˆλ‹€λŠ” 사싀 μ•Œκ³  κ³„μ…¨λ‚˜μš”? 곡식 λ¬Έμ„œμ—μ„œλŠ” λ„₯슀트 ν‚€ 락으둜 Phantom Read 문제λ₯Ό 막을 수 μžˆλ‹€κ³  λ‚˜μ™€ μžˆμŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ, select ν›„ 비관적 락인 select ... for update 쿼리λ₯Ό μ§„ν–‰ν•˜λ©΄, Phantom Read κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

https://dev.mysql.com/doc/refman/8.4/en/innodb-next-key-locking.html

 

쿼리가 [1]μ™Όμͺ½ νŠΈλžœμž­μ…˜μ—μ„œ select ... for update → [2]였λ₯Έμͺ½ νŠΈλžœμž­μ…˜ insert μˆœμ„œλ‘œ μ§„ν–‰ 될 경우

λ¨Όμ €, μ™Όμͺ½ νŠΈλžœμž­μ…˜μ—μ„œ select ... for update 후에 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜μ—μ„œ insert λ₯Ό ν•˜λ©΄ 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜μ€ λŒ€κΈ° μƒνƒœκ°€ 되며 κ³΅μ‹λ¬Έμ„œμ—μ„œ λ‚˜μ˜¨ κ²ƒμ²˜λŸΌ λ„₯슀트 ν‚€ 락을 μ‚¬μš©ν•΄μ„œ λ ˆμ½”λ“œ μ‚½μž…μ— μ˜ν•΄ λ°œμƒν•˜λŠ” phatom read λŠ” λ°œμƒν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 

next-key 락에 μ˜ν•΄ phatom read λ₯Ό λ§‰λŠ” 경우

 

쿼리가 [1]μ™Όμͺ½ select [2]였λ₯Έμͺ½ insert  [3]였λ₯Έμͺ½ commit  [4]μ™Όμͺ½ select ... for update μˆœμ„œμΌ 경우

μ•„λž˜ μ‚¬μ§„μ²˜λŸΌ 1λ²ˆλΆ€ν„° 4λ²ˆκΉŒμ§€ μ°¨λ‘€λ‘œ μˆœμ„œλŒ€λ‘œ μž‘μ„±ν•˜λ‹ˆ μ™Όμͺ½ νŠΈλžœμž­μ…˜μ΄ 같은 νŠΈλžœμž­μ…˜μž„μ—λ„ λΆˆκ΅¬ν•˜κ³ , phantom read κ°€ λ°œμƒν•œ 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” select ... for update κ°€ 언두 μ˜μ—­μ„ 읽지 μ•Šκ³ , κΈ°λ³Έ select 와 달리 ν…Œμ΄λΈ” λ ˆμ½”λ“œλ₯Ό λ°”λ‘œ 읽기 λ•Œλ¬ΈμΈλ°μš”. MVCC기법에 따라 κΈ°λ³Έ select 쿼리인 1λ²ˆμ—μ„œλŠ” μ–Έλ‘μ˜μ—­μ„ 읽고, 비관적 락이 κ±Έλ¦° 4λ²ˆμ—μ„œλŠ” 언두 μ˜μ—­μ΄ μ•„λ‹Œ ν…Œμ΄λΈ” λ ˆμ½”λ“œλ₯Ό 읽게 λ©λ‹ˆλ‹€.

 

이런 λ³΅μž‘ν•œ 상황이 같은 ν…Œμ΄λΈ”(μ—”ν‹°ν‹°)μ—μ„œ λ°œμƒν•  수 μžˆλ‚˜ 싢기도 ν•˜μ§€λ§Œ, κ·Έλ ‡λ‹€κ³  또 100%λΌλŠ” 보μž₯은 μ—†μœΌλ‹ˆκΉŒ μ–΄λ–»κ²Œ ν•΄μ•Όν• μ§€ μ°Έ μ–΄λ ΅μŠ΅λ‹ˆλ‹€.

++ μΆ”κ°€. μ΄λŸ¬ν•œ κ²½μš°λŠ” 잘 λ°œμƒν•˜μ§€ μ•ŠλŠ”λ‹€κ³  ν•©λ‹ˆλ‹€.

 

μ •λ¦¬ν•˜μžλ©΄, MySQL 8.0 InnoDBλŠ” MVCC λ₯Ό ν™œμš©ν•΄ REPEATABLE READ λ₯Ό κ°€λŠ₯ν•˜κ²Œ ν•˜κ³ , select...for update 같이 λ ˆμ½”λ“œμ— 락을 κ±Έμ–΄μ•Ό ν•  λ•ŒλŠ”(MVCCλ₯Ό ν™œμš©ν•˜μ§€ λͺ»ν•  λ•Œ λŠ”) λ„₯슀트 ν‚€ 락을 ν™œμš©ν•΄ Phantom Read λ₯Ό λ°©μ§€ν•œλ‹€. 단, ν•œ νŠΈλžœμž­μ…˜μ—μ„œ select ν›„, select...for update μ‹œμ—λŠ” Phantom Read κ°€ λ°œμƒν•  μˆ˜λ„ μžˆλ‹€λ‘œ 정리할 수 μžˆκ² μŠ΅λ‹ˆλ‹€.

MVCCλ₯Ό μœ„ν•œ μ–Έλ‘μ˜μ—­κ³Ό select ... for update 의 νŠΉμ§•μœΌλ‘œ 인해 phantom read κ°€ λ°œμƒν•˜λŠ” 경우

 

5. UPDATE / DELETE 쿼리λ₯Ό μ‹€ν–‰ ν•  λ•Œ, MySQL 이 λ‚΄λΆ€μ μœΌλ‘œ 배타 락을 μ‚¬μš©ν•˜μ§€λ§Œ select...for update(JPA의 비관적 락)을 μ‚¬μš©ν•΄ λ™μ‹œμ„± μ œμ–΄λ₯Ό ν•˜λŠ” 이유

ν˜„λŒ€ DBMS λŠ” MVCCλ₯Ό 톡해 잠금 μ—†λŠ” 읽기λ₯Ό μ§€μ›ν•©λ‹ˆλ‹€. MySQL 8.0은 Repeatable Read κ°€ κΈ°λ³Έ 격리 μˆ˜μ€€μ΄λ©° MVCCλ₯Ό 톡해 νŠΈλžœμž­μ…˜μ€ λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ˜ κ²°κ³Όλ₯Ό λ°˜μ˜ν•˜μ§€ μ•Šκ³  μžˆλŠ”λ°μš”.

 

λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ˜ κ²°κ³Όλ₯Ό λ°˜μ˜ν•˜μ§€ μ•Šλ‹€λ³΄λ‹ˆ 두 νŠΈλžœμž­μ…˜μ—μ„œ ν•œ λ ˆμ½”λ“œμ— UPDATE 쿼리λ₯Ό 각각 μ‹€ν–‰ν•˜λ©΄ ν•˜λ‚˜μ˜ νŠΈλžœμž­μ…˜ 결과만 λ ˆμ½”λ“œμ— λ°˜μ˜ν•˜κ²Œ λ©λ‹ˆλ‹€. 즉, ν•˜λ‚˜μ˜ UPDATE 결과만 λ ˆμ½”λ“œμ— 반영되고 λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ˜ UPDATE κ²°κ³ΌλŠ” λ ˆμ½”λ“œμ— λ°˜μ˜λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 값이 λˆ„μ λ˜μ–΄μ•Ό ν•˜λŠ”λ°, λˆ„μ λ˜μ§€ μ•ŠλŠ” 것이죠. 이λ₯Ό Lost Update 문제라 ν•©μ‹œλ‹€.

 

κ·Έλž˜μ„œ, 이럴 κ²½μš°λŠ” select ... for update λ₯Ό 톡해 UPDATE ν•˜λ €λŠ” νŠΈλžœμž­μ…˜λ“€μ„ 순차적으둜 λ§Œλ“€μ–΄μ„œ 2개의 변경사항을 λˆ„μ μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€. select ... for update λŠ” JPA의 비관적 락을 톡해 μ‰½κ²Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

Lost Update λ¬Έμ œμ— λŒ€ν•œ μ˜ˆμ‹œλ₯Ό λˆ„μ λ˜μ–΄μ•Ό ν•˜λŠ” 돈으둜 κ°€μ Έμ™”μŠ΅λ‹ˆλ‹€.

 

- update 문만 μ‚¬μš©ν•΄μ„œ Lost Update 문제 λ°œμƒν•˜λŠ” 경우

id=1 인 λ ˆμ½”λ“œμ˜ money 컬럼의 κ°’(1000)을 μ™Όμͺ½ νŠΈλžœμž­μ…˜μ—μ„œλŠ” 500을 λΉΌκ³ , 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜μ—λŠ” 100을 λΉΌκ³ , 두 νŠΈλžœμž­μ…˜μ„ μ»€λ°‹ν–ˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜, μ™Όμͺ½ νŠΈλžœμž­μ…˜μ— Query OK, 1 row affected (4.28 sec) 을 톡해 update μ‹œ 배타 락이 κ±Έλ € λŒ€κΈ°ν–ˆμ§€λ§Œ κ²°κ³ΌλŠ” id=1인 λ ˆμ½”λ“œμ˜ money 컬럼의 값은 500 으둜, 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜μ˜ κ²°κ³Όκ°€ λ ˆμ½”λ“œμ— λ°˜μ˜λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.

lost update 문제 λ°œμƒ

 

- select ... for update μ‚¬μš©ν•΄μ„œ Lost Update 문제 λ°œμƒν•˜μ§€ μ•ŠλŠ” 경우 

배타 락을 μ‚¬μš©ν•˜λ‹ˆ  id=1 인 ν–‰μ˜ money 컬럼의 κ°’(1000)을 였λ₯Έμͺ½ νŠΈλžœμž­μ…˜μ—μ„œλŠ” 100을 빼자마자, μ™Όμͺ½ νŠΈλžœμž­μ…˜μ—λŠ” id=1 인 ν–‰μ˜ money 컬럼의 값을 μ‘°νšŒν•˜λ‹ˆ 1000μ—μ„œ 100을 λΊ€ 값인 900이 λ‚˜μ˜€κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

 

 

6. 마치며

곡유 락/배타 락/MVCC λ₯Ό κ³΅λΆ€ν•˜κ³ , Real MySQL을 κ³΅λΆ€ν•˜λ©΄μ„œ 락을 μ‚¬μš©ν•˜λ©΄ μ–΄λ–€ 쿼리 μž‘μ—…κΉŒμ§€ 영ν–₯을 λ―ΈμΉ˜λŠ”μ§€ 항상 ν—·κ°ˆλ Έκ³ , 배타 락과 MySQL μ—μ„œ μ§€μ›ν•˜λŠ” λ ˆμ½”λ“œ 락 / κ°­ 락 / λ„₯슀트 ν‚€ 락은 차이가 무엇인지, update 에도 배타 락이 κ±Έλ¦¬λŠ”λ° select ... for update λŠ” μ™œ ν•„μš”ν•œμ§€? 직접 μ‹€ν—˜ν•˜κ³  μ •λ¦¬ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

 

이번 μ‹€ν—˜μ„ 톡해 UPDATE / DELETE μž‘μ—…λ„ 배타 락을 μ‚¬μš©ν•˜κ³ , μ™Έλž˜ν‚€/μœ λ‹ˆν¬ ν‚€ μ œμ•½ 쑰건듀은 μ–΄λ–»κ²Œ λ°μ΄ν„°μ˜ 무결성을 보μž₯ν–ˆλŠ”μ§€ κΆκΈˆν–ˆλŠ”λ° 이번 κΈ°νšŒμ— 정리할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 

 

  • MySQL λ‚΄λΆ€μ μœΌλ‘œ λ°μ΄ν„°μ˜ 무결성(μ™Έλž˜ν‚€, μœ λ‹ˆν¬ ν‚€)을 μ§€ν‚€κΈ° μœ„ν•΄ 락을 μ‚¬μš©ν•©λ‹ˆλ‹€.
  • MySQL InnoDBλŠ” UPDATE / DELETE 쿼리 μ‹œ μΈλ±μŠ€κ°€ μ—†λŠ” μ»¬λŸΌμ„ WHERE μ ˆμ— λ„£μœΌλ©΄ 쑰건에 λ§žλŠ” λ ˆμ½”λ“œλ₯Ό UPDATE / DELETE ν•˜κΈ° μœ„ν•΄ λ§Žμ€ λ ˆμ½”λ“œλ₯Ό 읽게 되며, 읽은 λ ˆμ½”λ“œμ—λŠ” 배타 락을 κ²λ‹ˆλ‹€.
  • 비관적 락은 배타 락을 μ‚¬μš©ν•˜λ©° 읽기 락과 배타 락과의 κ³΅μœ κ°€ λΆˆκ°€ν•©λ‹ˆλ‹€.
  • MySQL 8.0 InnoDB의 κΈ°λ³Έ 격리 μˆ˜μ€€μ—μ„œ select...for update μ‹œ, λ°œμƒν•˜λŠ” Phantom Read λ₯Ό 막기 μœ„ν•΄ 배타 락인 λ„₯슀트 ν‚€ 락(κ°­ 락 + λ ˆμ½”λ“œ 락)을 μ‚¬μš©ν•©λ‹ˆλ‹€.