π μμ½
SUMMARY
InnoDB μμ§μμ λΉμΈλ±μ€ 컬λΌμ WHERE 쑰건μΌλ‘ PESSIMISTIC_WRITEλ₯Ό μ¬μ©νλ©΄ λ μ½λ λ½(Record Lock)λΏλ§ μλλΌ κ° λ½(Gap Lock)μ΄ ν¨κ» λμν μ μμΌλ©°, μ΄λ μλμΉ μμ λ°λλ½μ μ λ°νλ μμΈμ΄ λ μ μλ€.
- 곡μλ¬Έμλ REPEATABLE READ 격리 μμ€ μ΄μμμ Gap Lock μ΄ λ°μνλ κ²½μ°λ₯Ό μ€λͺ νμ§λ§ READ_COMMITTED μ READ_UNCOMMITTED 격리 μμ€μμλ Gap Lock μ΄ λ°μνλ€.
- λΉκ΅μ (Non-Unique) μΈλ±μ€λ₯Ό WHERE 쑰건μΌλ‘ μ¬μ©ν΄λ λμΌνκ² λ°λλ½μ΄ λ°μν κ²μ΄λΌ μκ°νμ§λ§ μμΈλ‘ λ°λλ½μ΄ λ°μνμ§ μμλ€.
- λ°λλ½μ νΌνκΈ° μν΄μλ μν©μ λ°λΌ μλ λ°©λ² λ±μ κ³ λ―Όν΄λ³Ό μ μλ€.
- λΉμ λν¬ μΈλ±μ€ 쑰건μ
WHERE PK IN (A, B)μ κ°μ΄ κΈ°λ³Έ ν€(PK)λ₯Ό μ΄μ©ν 쑰건μΌλ‘ λ³κ²½- λΉκ΄μ λ½μ΄ μλ λκ΄μ λ½μΌλ‘ λ‘μ§ λ³κ²½
- νΈλμ μ μ€λ₯ μ μ¬μλ λ‘μ§ μΆκ°
βοΈ νκ²½
- MariaDB 10.8.3 (InnoDB)
- Spring Boot 2.5.1
- JDK 1.8
π¬ μ΄μ
μ¬λ΄ μ루μ μμ λΉκ΄μ λ½(PESSIMISTIC_WRITE)μ μ¬μ©νλ λ‘μ§μ νΈλμμ κ° κ²½ν©μ΄ λ°μνλ©° κ°νμ μΌλ‘ λ°λλ½μ΄ λ°μνλ λ¬Έμ λ₯Ό κ²ͺμλ€. λ¬Έμ μν©μ μ¬ννκΈ° μν΄ μλμ κ°μ΄ μ½λλ₯Ό ꡬμ±ν΄λ³΄μλ€.
μν°ν°
@Entity
@Table(name = "target_table", catalog = "test")
@Data
public class TargetTable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Integer id;
@Column(name = "col1")
private Integer col1;
}μ€λ₯ λ°μ μ§μ
private List<TargetTable> findEntityListWithLock(int col1) {
return rdbService.getQueryFactory()
.selectFrom(QTargetTable.targetTable)
.where(QTargetTable.targetTable.col1.eq(col1))
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.fetch();
}μ€λ₯ λ©μμ§
Caused by: javax.persistence.OptimisticLockException: org.hibernate.exception.LockAcquisitionException: could not extract ResultSetβ μλ¬Έμ
μΌλ°μ μΈ κ²½ν© μν©μ΄λΌλ©΄, λ¨Όμ λ½μ νλν νΈλμμ
μ΄ λλ λκΉμ§ innodb_lock_wait_timeout μ€μ κ°μΈ 50μ΄ λμ λκΈ°ν ν νμμμ μ€λ₯κ° λ°μν΄μΌ νλ€κ³ μμνλ€. νμ§λ§ μ€μ λ‘λ λ½ νλ μλ ν 1μ΄λ μ λμ΄ λ°λλ½ μμΈκ° λ°μνλ€.
κ²λ€κ° μμΈ μ’
λ₯λ λ½ νλ μ€ν¨ μ μμνλ PessimisticLockExceptionμ΄ μλλΌ OptimisticLockExceptionμ΄μ΄μ μμνλ€.
μ΄λ¬ν μ λ€ λλ¬Έμ λ¨μν κ²½ν© λ¬Έμ κ° μλ, λ‘μ§ μμμ μλμΉ μμ λ€λ₯Έ μμΈμ΄ μμ κ²μ΄λΌ νλ¨νκ³ , λ κΉμ΄ νκ³ λ€μ΄ λ³΄κΈ°λ‘ νλ€.
π§ ν΄κ²°
MariaDB λ‘κ·Έ νμΈ
μλΉμ€ μ€λ₯ λ‘κ·Έλ§μΌλ‘λ μ νν μμΈ νμ μ΄ μ΄λ €μ, MariaDBμ λ°λλ½ λ‘κ·Έλ₯Ό μ§μ νμΈν΄λ³΄κΈ°λ‘ νλ€. (νμΈ λ°©λ²μ MariaDB λ°λλ½ λ‘κ·Έ νμΈ μ°Έκ³ )
2024-09-27 18:01:00 0x7f86936af700
*** (1) TRANSACTION:
TRANSACTION 28480029, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
MariaDB thread id 901455, OS thread handle 140215975606016, query id 53815334 172.19.0.1 root Sending data
select targettabl0_.id as id1_184_, targettabl0_.col1 as col2_184_ from test.target_table targettabl0_ where targettabl0_.col1=50 for update
*** WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 218104 page no 3 n bits 8 index PRIMARY of table `test`.`target_table` trx id 28480029 lock_mode X locks rec but not gap waiting
...
*** (2) TRANSACTION:
TRANSACTION 28480030, ACTIVE 0 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 4 row lock(s)
MariaDB thread id 901452, OS thread handle 140215980521216, query id 53815330 172.19.0.1 root Sending data
select targettabl0_.id as id1_184_, targettabl0_.col1 as col2_184_ from test.target_table targettabl0_ where targettabl0_.col1=20 for update
*** WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 218104 page no 3 n bits 8 index PRIMARY of table `test`.`target_table` trx id 28480030 lock_mode X locks rec but not gap waiting
...
*** WE ROLL BACK TRANSACTION (0)
λ‘κ·Έμ ν΅μ¬μ λ νΈλμμ
μ΄ record lockμ νλνμ§λ§ gap lockμ κΈ°λ€λ¦¬λ€ κ΅μ°© μνμ λΉ μ‘κ³ (lock_mode X locks rec but not gap waiting), κ²°κ΅ InnoDB μμ§μ΄ λ μ€ νλλ₯Ό ν¬μμ(victim)μΌλ‘ μ νν΄ λ‘€λ°±νλ€λ κ²μ΄λ€.
NOTE
κ° λ½(Gap Lock)μ΄λ? κ° λ½μ μΈλ±μ€ λ μ½λ μ¬μ΄μ κ°κ²©(Gap)μ μ κ·Έλ κΈ°λ₯μ΄λ€. μ¦, μ€μ μ‘΄μ¬νλ λ μ½λλΏλ§ μλλΌ, 쑰건μ ν΄λΉνμ§λ§ μ‘΄μ¬νμ§ μλ λ μ½λμ λ²μκΉμ§ μ κ·Όλ€. μ΄λ‘ μΈν΄ λ€λ₯Έ νΈλμμ μ΄ κ·Έ κ°κ²© λ΄μ μλ‘μ΄ λ°μ΄ν°λ₯Ό μΆκ°(INSERT)νλ κ²μ λ°©μ§νμ¬
Phantom Readνμμ λ§λλ€.
μ¬κΈ°μ μλ‘μ΄ μλ¬Έμ΄ μκ²Όλ€. λ νΈλμμ
μ μλ‘ λ€λ₯Έ λ μ½λλ₯Ό λμμΌλ‘ λ½μ μλνλλ° μ κ΅μ°© μνκ° λ°μνμΌλ©°, μμν Gap Lockμ΄λ λ체 무μμΌκΉ? μΌλ°μ μΌλ‘ FOR UPDATE 쿼리λ λ μ½λ(Row) λ¨μλ‘ λ½μ νλνλ―λ‘, μλ‘ λ€λ₯Έ λ μ½λλ₯Ό λμμΌλ‘ ν λλ κ²½ν©μ΄ λ°μνμ§ μμμΌ νλ€κ³ μκ°νλ€. νμ§λ§ μ¬κΈ°μλ ν κ°μ§ μ€μν μ‘°κ±΄μ΄ μ¨μ΄μμλ€.
μ€λ₯ μ¬μ°
μ νν μμΈμ νμ νκΈ° μν΄ λ°λλ½μ΄ λ°μνλ μν©μ μ§μ μ¬νν΄λ³΄μλ€.
ν μ΄λΈ μΈν
MariaDB [test]> CREATE TABLE target_table (
-> id INT NOT NULL,
-> col1 INT DEFAULT NULL,
-> PRIMARY KEY (id)
-> ) ENGINE=InnoDB;
MariaDB [test]> INSERT INTO target_table VALUES (1, 10), (2, 20), (3, 30), (4, 20), (5, 50), (6, 10), (7, 20), (8, 30), (9, 40), (10, 50);
MariaDB [test]> select * from target_table;
+----+------+
| id | col1 |
+----+------+
| 1 | 10 |
| 6 | 10 |
| 2 | 20 |
| 7 | 20 |
| 3 | 30 |
| 8 | 30 |
| 4 | 40 |
| 9 | 40 |
| 5 | 50 |
| 10 | 50 |
+----+------+ν μ€νΈ μ½λ
@Slf4j
@RequiredArgsConstructor
@DisplayName("λ°λλ½ ν
μ€νΈ")
@Transactional
public class DeadLockTest extends IntegrationTest {
private final DeadLockTestService deadLockTestService;
final int NUM_THREADS = 5;
@Test
@DisplayName("μλ‘ λ€λ₯Έ μ€λ λ(νΈλμ μ
)κ° λμμ λ½ νλ μλ")
void test() {
ExecutorService executorService = new ThreadPoolExecutor(NUM_THREADS, NUM_THREADS, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
List<Runnable> runnables = new ArrayList<>();
for (int i = 0; i < NUM_THREADS; i++) {
final int threadId = i + 1; // μ€λ λ λ²νΈ
Runnable runnable = () -> {
deadLockTestService.process(threadId);
};
runnables.add(runnable);
}
// μ€λ λ μμ
μ€ν
runnables.forEach(executorService::execute);
// μ€λ λ ν μ’
λ£
executorService.shutdown();
try {
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}μλΉμ€ ν΄λμ€
@Service
@Slf4j
@RequiredArgsConstructor
public class DeadLockTestService {
private final RdbService rdbService;
private List<TargetTable> findEntityListWithLock(int col1) {
return rdbService.getQueryFactory()
.selectFrom(QTargetTable.targetTable)
.where(QTargetTable.targetTable.col1.eq(col1))
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.fetch();
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void process(int threadId) {
// νμ¬ μκ°
LocalTime now = LocalTime.now();
// λ€μ λΆμ 00μ΄ 000λ°λ¦¬μ΄κΉμ§ λ¨μ μκ° κ³μ°
LocalTime nextMinute = now.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES);
long millisUntilNextMinute = ChronoUnit.MILLIS.between(now, nextMinute);
try {
// λ€μ 00μ΄ 000λ°λ¦¬μ΄κΉμ§ λκΈ°
Thread.sleep(millisUntilNextMinute);
log.info("λΉκ΄μ λ½ μλ - 쑰건 : col1={}", threadId * 10);
List<TargetTable> list = findEntityListWithLock(threadId * 10);
log.info("λΉκ΄μ λ½ νλ μλ£ - {}", list.size());
list = findEntityListWithLock(threadId * 10);
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (OptimisticLockException e) {
e.printStackTrace();
log.info("μ‘μλ€!");
} catch (PessimisticLockException e) {
e.printStackTrace();
log.info("μ μμ μΈ κ²½μ°");
} }
}ν μ€νΈ μ§ν
μ μ½λλ₯Ό λ² μ΄μ€λ‘ λ€μκ³Ό κ°μ μΌμ΄μ€λ‘ ν μ€νΈλ₯Ό μ§ννκ³ , μλ κ²°κ³Όλ₯Ό μ»μλ€.
| μΌμ΄μ€ | λ°λλ½ λ°μ μ¬λΆ |
|---|---|
| WHERE 쑰건μ PK λ₯Ό μ¬μ©νλ κ²½μ° | λ°μ μν¨ |
격리μμ€μ REPEATABLE_READ λ SERIALIZABLE λ‘ μ€μ νλ κ²½μ° | λ°μ μν¨ |
ν νΈλμ μ
μ FOR UPDATE 쿼리λ₯Ό 1λ²μ©λ§ νΈμΆνλ κ²½μ° | λ°μ μν¨ |
| λΉκ³ μ (Non-unique)μΈλ±μ€λ‘ λ±λ‘ν 컬λΌμ WHERE 쑰건μΌλ‘ μ¬μ©νλ κ²½μ° | λ°μ μν¨ |
setHint("javax.persistence.lock.timeout", 5000) λ‘ μ€μ νλ κ²½μ° | 무κ΄νκ² λ°μ |
IMPORTANT
λΉκ³ μ (Non-unique) μΈλ±μ€λ₯Ό WHERE 쑰건μΌλ‘ μ¬μ©ν΄λ λμΌνκ² λ°λλ½μ΄ λ°μν κ²μ΄λΌ μκ°νμ§λ§ μμΈλ‘ λ°λλ½μ΄ λ°μνμ§ μμλ€. μ΄ λΆλΆμ λν΄μλ μΆκ°μ μΌλ‘ νμ ν΄λ³΄μ§ λͺ»νλ€.
λ°λλ½ λ°μ 쑰건 λΆμ
μ¬λ¬ ν μ€νΈλ₯Ό ν΅ν΄ λ°λλ½μ΄ λ°μνλ νΉμ 쑰건μ μ’ ν©ν΄λ³Ό μ μμλ€.
- μλ‘ λ€λ₯Έ νΈλμ μ μ΄ κ±°μ λμμ λ½μ νλνλ € νλ€.
- 격리 λ 벨μ READ_COMMITTED μ READ_UNCOMMITTED μΌ κ²½μ°μλ§ λ°μνλ€.
- μΈλ±μ€κ° μλ 컬λΌμ 쑰건μΌλ‘ μ¬μ©νλ€.
- 1κ° νΈλμ μ μμ for update λ₯Ό λ λ² νΈμΆνλ€.
μ’
ν©νλ©΄ READ_COMMITTED λλ READ_UNCOMMITTED 격리 μμ€μμ, μΈλ±μ€κ° μλ 컬λΌμ WHERE 쑰건μΌλ‘ μ¬μ©νμ¬, νλμ νΈλμμ
μμ FOR UPDATEλ₯Ό λ λ² μ΄μ νΈμΆνλ©°, μ¬λ¬ νΈλμμ
μ΄ κ±°μ λμμ λ½μ νλνλ € ν λ λ°μνλ€.
IMPORTANT
곡μ λ¬Έμμμλ
REPEATABLE READ격리 μμ€ μ΄μμμGap Lockμ΄ λ°μνλ€κ³ μ€λͺ νμ§λ§, μ§νν ν μ€νΈμμλREAD_COMMITTEDμREAD_UNCOMMITTED격리 μμ€μμλGap LockμΌλ‘ μΈν λ°λλ½μ΄ λ°μνλ€. MariaDB λ²μ μ μ΄μλ νΉμ μν©μ λ°λΌ λμμ΄ λ€λ₯Ό μ μλμ§μ λν΄μλ νμ νμ§ λͺ»νλ€.
ν΄κ²° λ°©μ
Gap locking is not needed for statements that lock rows using a unique index to search for a unique row. - MariaDB 곡μ λ¬Έμ
μμΈμ΄ Gap Lockμ μλ€λ κ²μ νμ
ν ν, μλΉμ€ λ‘μ§μ μμ νκΈ°λ‘ νλ€. λ°λΌμ κΈ°μ‘΄ FOR UPDATE 쿼리μ WHERE μ μμ λΉ μΈλ±μ€ μ»¬λΌ μ‘°κ±΄μ μ¬μ©νλ λμ , WHERE id IN (...)κ³Ό κ°μ΄ PK κΈ°λ°μΌλ‘ λ μ½λλ₯Ό μ‘°ννλλ‘ λ‘μ§μ λ³κ²½νμ¬ λ¬Έμ λ₯Ό ν΄κ²°νλ€.

μΆκ°μ μΌλ‘, λ°λλ½μ΄ μλλλΌλ λ°μν μ μλ νμμμ(PessimisticLockException)μ λλΉνμ¬ νΈλμμ
μ€λ₯ μ λΉμ¦λμ€ λ‘μ§μ μ¬μλνλ λ‘μ§μ λνλ€λ©΄ λμ± μμ μ μΈ μλΉμ€λ₯Ό λ§λ€ μ μμ κ²μ΄λΌ μκ°νλ€. μ΄λ νΈλμμ
μΈλΆμμ μ²λ¦¬νκ±°λ λ³λμ μ€μΌμ€λ§ λ‘μ§μΌλ‘ ꡬννλ κ²μ΄ μ΄λ¨κΉ μΆλ€.
π μ°Έκ³
- mariadb 곡μλ¬Έμ innodb-lock-modes
- mysql 곡μλ¬Έμ innodb-locking
- mysql 곡μλ¬Έμ innodb-deadlocks
- mysql 곡μλ¬Έμ innodb-information-schema-transactions
- νΈλμμ μ 격리 μμ€(Isolation Level)μ λν΄ μ½κ³ μλ²½νκ² μ΄ν΄νκΈ°
- MySQL Gap Lock λ€μ보기
- MySQL Gap Lock (λλ²μ§Έ μ΄μΌκΈ°)
- Lockμ μ’ λ₯ (Shared Lock, Exclusive Lock, Record Lock, Gap Lock, Next-key Lock)
- InnoDBμ Lock Escalation