DB

[MySQL/InnoDB] Update와 Insert의 Locking, Blocking(대기) - 코드를 중심으로

폭풍저그김탁구 2024. 9. 30. 16:24

MySQL 코드를 분석하면서 배운 점을 정리한 글입니다.

학부생 수준의 이해를 기반으로 한 글이니, 참고용으로만 보시면 됩니다.

웬만하면 공식 문서를 직접 보거나 코드를 확인해주시는 걸 추천드립니다.

 

(일반 서버 개발자들을 위한 글이 아닙니다!! MySQL 연구를 위한 글입니다!! 개발에는 아무 도움이 안 되는 글입니다!!)

 

 

Serializable mode를 가정한 설명입니다.

Lock 충돌을 확인하고, 충돌에 대해 blocking되어 대기 상태로 빠지는 부분을 위주로 공부하였습니다.

 


 

 

Update의 Locking

업데이트의 경우 Locking이 좀 더 일반적이고 직관적으로 진행된다.

 

코드로는

row_search_mvcc() -> sel_set_rec_lock() -> lock_clust_rec_read_check_and_lock() -> lock_rec_lock() -> lock_rec_lock_fast() -> lock_rec_lock_slow() -> add_to_waitq() 함수 순서대로 확인하면 좋습니다.

 

단순하게 설명하자면,

1. Fast Lock 시도 

- 해당 레코드에 선행 Lock이 없다던가, 무시할 수 있는 Lock이라던가... 아무튼 대기를 하지 않아도 되는 확실한 상황일 때이다.

- 즉시 Lock을 획득할 수있다.

 

2. Slow Lock 시도

- Fast Lock이 실패하면 실행하는 함수다.

- 우선, 해당 record에 대해 내가 걸어둔 Lock이 있는지 확인한다. 이미 Lock을 획득한 상태라면 추가로 얻을 필요가 없으므로 return.

- 만약 없다면, 다른 Lock을 기다려야 할 수 있다. 이때 add_to_waitq() 함수를 실행한다.

 

3. add_to_waitq()

- 실제로 Lock 충돌 여부를 검사하는 주요 함수이니, 관심 있게 뜯어봐야 하는 함수다. 

- 이름에서는 wait queue라는 말을 사용하고 있지만, 실제로 queue 구조체를 사용하거나 그런 건 아니다. (hash를 사용함) 논리적으로 봤을 땐 queue가 맞긴 하지만,  명시적으로 사용하는 것은 아니다.

- create(): Lock을 할당받고, Lock을 hash의 맨 끝에 추가한다.

- deadlock_check(): 이름 그대로 데드락인지 확인하는 함수. 여기서 wait 여부를 결정한다.

- 다른 트랜잭션을 기다려야 하는 상황이면 DB_LOCK_WAIT 를 반환한다. 

 

4. Wait

- row_search_mvcc() 함수에서 lock_wait_or_error: 부분으로 이동하여 row_mysql_handle_errors() 함수에서 해당 에러를 핸들링한다.

- lock_wait_suspend_thread() 함수에서os_event_wait()을 이용해 스레드를 sleep 상태로 전환한다. 

 

 

즉 레코드를 읽기 전 Lock을 획득하고, 읽은 레코드를 수정한다.

 


 

Insert의 Locking

Insert는 새로운 레코드를 생성하는 것이다. mvcc에서 해당 레코드를 읽을지 말지는 중요한 문제이자 까다로운 문제이다.

update는 레코드를 읽기 전 lock을 획득하는데, 그럼 새 레코드에 대해서는 어떻게 해야 할까?

 

코드로는

sel_set_rec_lock → lock_clust_rec_read_check_and_lock → lock_rec_convert_impl_to_expl → lock_rec_add_to_queue

순서로 확인하면 좋다.

 

구체적인 Lock 획득 과정은 생략하고 update와 다른 점만 설명하자면,

Insert는 레코드에 접근할 때가 아닌, 후행 트랜잭션이 이 레코드에 접근할 때 Lock을 생성한다.

 

예를 들어,

1. T1: Insert R1;
2. T2: Select All;

같은 쿼리가 진행될 때, Serializable에서는 T1은 R1을 읽을 수 없고 기다려야 한다.

그래서, T2가 R1에 접근하려 하면 R1에 적힌 trx_id를 읽고 blocking 여부를 판단한다.

그리고 R1에 대한 T1의 lock을 생성한 다음 T2의 lock을 생성하기 때문에, T2는 T1에 의해 Blocking 당한다.

 


 

 

MySQL은 trx_id라는 어찌 보면 쉬워보이는..? 값을 잘 활용해먹는듯. 특히 MVCC에서...

 

다음엔 rollback 중심으로 정리해보겠다.