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

MySQL

λ™μ‹œμ„± μ œμ–΄ μ‹œ JPA의 비관적 락(select ... for update)에 λŒ€ν•΄ 회의적인 이유

이 κΈ€μ—μ„œλŠ” MySQL을 μ‚¬μš©ν•΄, νŠΉμ • λ ˆμ½”λ“œμ— 락이 κ±Έλ¦° μƒνƒœμ—μ„œ λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ΄ ν•΄λ‹Ή λ ˆμ½”λ“œμ— μ–΄λŠ μž‘μ—…κΉŒμ§€ ν—ˆμš©ν•˜λŠ”μ§€ μ‹€ν—˜ν•΄λ³΄κ³ , MySQL λ‚΄λΆ€μ—μ„œ μ–Έμ œ 락을 μ‚¬μš©ν•˜λŠ”μ§€ μ•Œμ•„λ³΄λ©° 비관적 락에 λŒ€ν•΄ 회의적인 이유λ₯Ό μ–˜κΈ°ν•˜λŠ” 개인적인 κΈ€μž…λ‹ˆλ‹€.

 

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

 

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

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

 

 

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

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

  • DBMS: Mysql 8.0
  • 격리 μˆ˜μ€€: 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 값은 DBλ‚΄λΆ€μ—μ„œ μ •ν•˜κ²Œ λ˜λŠ”λ° 이에 따라 INSERT 문이 락에 μ˜ν•΄ λŒ€κΈ°ν•  μˆ˜λ„, 아닐 μˆ˜λ„ 있게 λ©λ‹ˆλ‹€.

μ‹€ν—˜ κ²°λ‘ 

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

 

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

이λ₯Ό 톡해 update / delete 도 락을 μ‚¬μš©ν•  것 κ°™λ‹€λŠ” 예감이 λ“€λ©°, DBμ—μ„œ μ–Έμ œ 락을 μ‚¬μš©ν•˜λŠ”μ§€ μ°Ύμ•„λ΄€μŠ΅λ‹ˆλ‹€.


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

 

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

1. FOREIGN KEY μ œμ•½ 쑰건이 ν…Œμ΄λΈ”μ— μ •μ˜λ˜μ–΄ μžˆλŠ” 경우, INSERT / UPDATE / DELETE μž‘μ—…μ„ μˆ˜ν–‰ν•΄λ„ λ˜λŠ”μ§€ μ œμ•½ 쑰건을 ν™•μΈν•˜κΈ° μœ„ν•΄ μ‘°νšŒν•˜λŠ” λ ˆμ½”λ“œλ“€μ— λŒ€ν•΄ 곡유 λ ˆμ½”λ“œ μˆ˜μ€€ 락(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. μœ λ‹ˆν¬ 킀도 μ€‘λ³΅λœ 값을 체크할 λ•ŒλŠ” 읽기 μž κΈˆμ„, μ“°κΈ° ν•  λ•ŒλŠ” μ“°κΈ° μž κΈˆμ„ μ‚¬μš©ν•˜λ©° 이 κ³Όμ •μ—μ„œ λ°λ“œλ½μ΄ μ•„μ£Ό 빈번히 λ°œμƒν•©λ‹ˆλ‹€.(Real MySQL 8.0 좜처)

 

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

    • 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 와 락을 μ§€μ›ν•©λ‹ˆλ‹€. 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 ... for update μ‹œ, Phantom Read λ¬Έμ œκ°€ λ°œμƒ ν•  μˆ˜λ„ μžˆλ‹€λŠ” 사싀 μ•Œκ³  κ³„μ…¨λ‚˜μš”? κ³΅μ‹ λ¬Έμ„œμ—μ„œλŠ” Non-Locking Read μ‹œ, λ„₯슀트 ν‚€ 락으둜 Phantom Read λ¬Έμ œλ₯Ό 막을 수 μžˆλ‹€κ³  λ‚˜μ™€ μžˆμŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ, 비관적 락인 select ... for update 같은 Locking Read μ‹œ, Phantom Read κ°€ λ°œμƒν•  κ²½μš°κ°€ μžˆμŠ΅λ‹ˆλ‹€.

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

 

Non-Locking Read : 쿼리가 [1]μ™Όμͺ½ νŠΈλžœμž­μ…˜μ—μ„œ select ... for update → [2]였λ₯Έμͺ½ νŠΈλžœμž­μ…˜ insert μˆœμ„œλ‘œ 진행 될 경우

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

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

 

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

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

 

이런 λ³΅μž‘ν•œ 상황이 같은 ν…Œμ΄λΈ”(μ—”ν‹°ν‹°)μ—μ„œ λ°œμƒν•  수 μžˆλ‚˜ 싢기도 ν•˜μ§€λ§Œ, κ·Έλ ‡λ‹€κ³  또 100%λΌλŠ” 보μž₯은 μ—†μœΌλ‹ˆκΉŒ μ–΄λ–»κ²Œ 해야할지 μ°Έ μ–΄λ ΅μŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ 이유둜 κΈ€ λ§ˆμ§€λ§‰μ— μ„€λͺ…λ“œλ¦΄ν…λ°, 제게 μžˆμ–΄μ„œ μ΄λŠ” μ•žμœΌλ‘œ 비관적 락을 μ‚¬μš©ν•˜μ§€ μ•Šμ„ 이유 쀑 ν•˜λ‚˜κ°€ λ©λ‹ˆλ‹€.

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

 

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

MySQL은 λ‚΄λΆ€μ μœΌλ‘œ UPDATE/DELETE 쿼리λ₯Ό μ‹€ν–‰ν•  λ•Œ ν–‰(ν…Œμ΄λΈ”)에 배타 락을 μ„€μ •ν•˜μ§€λ§Œ, 이 락은 쿼리 μ‹€ν–‰ μ‹œμ μ—λ§Œ μ„€μ •λ©λ‹ˆλ‹€. 이 λ•Œλ¬Έμ— 데이터 쑰회 μ‹œμ λΆ€ν„° 데이터λ₯Ό λ³΄ν˜Έν•˜κΈ° μœ„ν•΄ select...for update와 같은 λͺ…령을 μ‚¬μš©ν•΄ 배타락을 직접 μ„€μ •ν•˜λŠ” 것이 ν•„μš”ν•©λ‹ˆλ‹€. 데이터 쑰회 ν›„ UPDATEλ₯Ό μ‹€ν–‰ν•  λ•Œ 쑰회 μ‹œμ λΆ€ν„° λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ΄ 데이터λ₯Ό μˆ˜μ •ν•˜μ§€ λͺ»ν•˜λ„둝 배타락을 μ„€μ •ν•˜μ—¬ 데이터 μΆ©λŒμ„ 막아야 ν•©λ‹ˆλ‹€. 

 

그렇지 μ•ŠμœΌλ©΄ μ•„λž˜μ²˜λŸΌ Lost Update λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€. 

Lost Update λž€ 두 νŠΈλžœμž­μ…˜μ΄ 같은 row 에 λŒ€ν•΄ μ“°κΈ° μž‘μ—…(insert, update, delete)을 μˆ˜ν–‰ν•  λ•Œ, ν•œ νŠΈλžœμž­μ…˜μ˜ κ²°κ³Όκ°€ λ°˜μ˜λ˜μ§€ μ•ŠλŠ” 것을 μ˜λ―Έν•©λ‹ˆλ‹€. λ§Œμ•½, 돈과 κ΄€λ ¨λœ νŠΈλžœμž­μ…˜μ΄λΌλ©΄ ν°μΌλ‚˜κ² μ£ . κ·Έλž˜μ„œ μ˜ˆμ œλ„ 돈과 κ΄€λ ¨λœ κ²ƒμœΌλ‘œ κ°€μ Έμ™”μŠ΅λ‹ˆλ‹€.

 

- 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 λ‚΄λΆ€μ μœΌλ‘œ λ°μ΄ν„°μ˜ 무결성(μ™Έλž˜ν‚€, μœ λ‹ˆν¬ ν‚€)을 지킀기 μœ„ν•΄ 락을 κ±Έκ³ ,
  • UPDATE / DELETE 쿼리 μ‹œ μΈλ±μŠ€κ°€ μ—†λŠ” μ»¬λŸΌμ„ WHERE μ ˆμ— λ„£μœΌλ©΄ μ˜ˆμƒμΉ˜ λͺ»ν•˜κ²Œ μˆ˜λ§Žμ€ λ ˆμ½”λ“œμ— 락이 걸릴 μˆ˜λ„ 있고,
  • Repetable Read μ—μ„œ 비관적 락을 μ‚¬μš©ν•  λ•Œ, phantom read 도 λ°œμƒν•˜λŠ” κ²½μš°λ„ μžˆλ‹€λ³΄λ‹ˆ(λŒ€μ²΄μ μœΌλ‘œλŠ” λ°œμƒν•˜μ§€ μ•Šμ§€λ§Œ..)

여기에 λ™μ‹œμ„± μ œμ–΄λ₯Ό μœ„ν•΄ 비관적 락을 μΆ”κ°€λ‘œ μ‚¬μš©ν•΄μ„œ λ°λ“œλ½ μœ„ν—˜μ„ 더 증가 μ‹œμΌœμ•Ό ν• κΉŒ? λž€ 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ 이유둜 비관적 락에 λŒ€ν•΄ 회의적이게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.  λ˜ν•œ, λˆ„κ΅°κ°€ 제게 '비관적 락은 μ™œ μ‚¬μš© μ•ˆν•˜μ…¨λ‚˜μš”?' 라 λ¬Όμ–΄λ³Έλ‹€λ©΄ 이 κ²½ν—˜μ„ ν† λŒ€λ‘œ 말씀 λ“œλ¦΄ 것 κ°™μŠ΅λ‹ˆλ‹€.

 

이에 κ΄€λ ¨ν•΄μ„œ, μΈν”„λŸ° 질의 μ‘λ‹΅μ—μ„œ μ˜ν•œλ‹˜κ»˜μ„œ λ™μ‹œμ„± μ œμ–΄λ₯Ό μœ„ν•΄ DB 락을 κ±°λŠ” 방법을 ν”Όν•˜κ³ , μ›μžμ  μ—°μ‚°μ΄λ‚˜ 둜직의 μž…κ΅¬μ—μ„œ λ§‰λŠ” 방법을 μ„ ν˜Έν•œλ‹€κ³  말씀을 ν•΄μ£Όμ‹  것을 보고 이번 κ²½ν—˜μ„ 톡해 μ‘°κΈˆμ€ μ΄ν•΄ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.