Skip to content

Commit befa7cc

Browse files
committed
mysql transaction,lock
1 parent b0ee217 commit befa7cc

File tree

1 file changed

+249
-1
lines changed

1 file changed

+249
-1
lines changed
+249-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,249 @@
1-
# Chapter05. 트랜잭션과 잠금
1+
# Chapter05. 트랜잭션과 잠금
2+
- MySQL의 동시성에 영향을 미치는 요소: 잠금, 트랜잭션(트랜잭션 격리 수준)
3+
4+
- 트랜잭션: 작업의 완전성을 보장해주는 것
5+
- 잠금: 트랜잭션과 비슷한 개념 같지만, 동시성을 제어하기 위한 기능
6+
7+
| 트랜잭션 | 잠금 |
8+
|----------------------|-----------------|
9+
| 데이터의 정합성을 보장하기 위한 기능 | 동시성을 제어하기 위한 기능 |
10+
11+
- MySQL에서 사용되는 잠금은 크게 [MySQL 엔진 레벨](#MySQL-엔진의-잠금)[스토리지 엔진 레벨](#InnoDB-스토리지-엔진-잠금)로 나눌 수 있음
12+
13+
<br/>
14+
15+
# 트랜잭션
16+
- 작업의 완전성을 보장해주기 위한, 데이터의 정합성을 보장하기 위한 기능
17+
- InnoDB는 트랜잭션을 지원하지만, MyISAM과 Memory는 지원하지 않음
18+
- 트랜잭션때문에 속도가 떨어져서 MyISAM, Memory를 고려할 수 있지만 오히려 트랜잭션이 없을 때 고려해야할 문제가 더 많이 발생할 수 있음
19+
20+
## InnoDB vs MyISAM
21+
```mysql
22+
# 두 종류의 테이블 생성 후 레코드 생성
23+
CREATE TABLE tab_myisam (fdpk INT NOT NULL, PRIMARY KEY (fdpk)) ENGINE=MYISAM;
24+
INSERT INTO tab_myisam (fdpk) VALUES (3);
25+
26+
CREATE TABLE tab_innodb (fdpk INT NOT NULL, PRIMARY KEY (fdpk)) ENGINE=INNODB;
27+
INSERT INTO tab_innodb (fdpk) VALUES (3);
28+
29+
# AUTO-COMMIT 활성화
30+
SET autocommit=ON;
31+
32+
# 레코드 추가
33+
INSERT INTO tab_myisam (fdpk) VALUES (1), (2), (3); # PK 중복으로 인한 에러 발생
34+
INSERT INTO tab_innodb (fdpk) VALUES (1), (2), (3); # PK 중복으로 인한 에러 발생
35+
36+
# 조회
37+
SELECT COUNT(*) FROM tab_myisam; # output: 3 / 1, 2는 추가됨
38+
SELECT COUNT(*) FROM tab_innodb; # output: 1 / 트랜잭션에 의해 해당 쿼리가 수행되지 않음
39+
```
40+
41+
## 트랜잭션 주의사항
42+
- 트랜잭션은 최소한의 코드에만 적용하는 것이 좋음
43+
- 단건 작업에 대한 트랜잭션의 범위가 커질 수록 커넥션을 오래 유지하는 문제 발생
44+
- 외부 기능(ex. FTP, 메일, ...)이 트랜잭션에 묶여져 있다면 외부 기능에 문제가 발생했을 떄 DB까지 위험해질 수 있음
45+
46+
<br/>
47+
48+
# MySQL 엔진의 잠금
49+
- MySQL에서 사용되는 잠금은 크게 스토리지 엔진 레벨과 MySQL 엔진 레벨로 나눌 수 있음
50+
- MySQL 엔진 레벨의 잠금은 모든 스토리지 엔진에 영향을 미치지만, 스토리지 엔진 레벨의 잠금은 스토리지 엔진 간 상호 영향을 미치지 않음
51+
- MySQL 엔진에서는 테이블 데이터 동기화를 위한 테이블 락 외에도 메타데이터 락, 네임드 락 기능도 제공
52+
- 메타데이터 락(Metadata Lock): 테이블의 구조를 잠금
53+
- 네임드 락(Named Lock): 사용자의 필요에 맞게 사용할 수 있는 잠금
54+
55+
## 글로벌 락(GLOBAL LOCK)
56+
- MySQL에서 제공하는 잠금 가운데 가장 범위가 큰 잠금으로, `FLUSH TABLES WITH READ LOCK` 명령으로 획득할 수 있음
57+
- 한 세션에서 글로벌 락을 획득하면 다른 세션에서 SELECT를 제외한 대부분의 DDL, DML 문장을 실행하는 경우 글로벌 락이 해제될 때까지 대기
58+
59+
### 백업 락
60+
- 8.0부터 InnoDB가 기본 스토리지 엔진으로 채택되면서 글로벌 락보다 가벼운 락이 필요했고, 이를 지원하기 위한 백업 락이 도입
61+
- cf. Xtrabackup, Enterprise Backup
62+
- 특정 세션에서 백업 락을 획득하면 모든 세션에서 아래와 같은 테이블의 스키마나 사용자 인증 관련 정보를 변경할 수 없음
63+
- 데이터베이스 및 테이블 등 모든 객체 생성 및 변경, 삭제
64+
- REPAIT TABLE과 OPTIMIZE TABLE 명령
65+
- 사용자 관리 및 비밀번호 변경
66+
- 다만 백업 락은 일반적인 테이블의 데이터 변경은 허용
67+
68+
## 테이블 락(Table Lock)
69+
- 개별 테이블 단위로 설정하는 잠금으로, `LOCK TABLES table name [READ | WRITE ]` 명령으로 권한 획득
70+
- `UNLOCK TABLES`로 잠금 해제
71+
- 특별한 상황이 아니면 애플리케이션에서 거의 사용할 필요 없음
72+
- 테이블을 잠그는 작업은 글로벌 락과 동일하게 온라인 작업에 영향을 끼침
73+
74+
### 묵시적인 테이블 락
75+
- MyISAM이나 Memory 테이블에서 데이터를 변경하는 쿼리를 실행하면 발생
76+
- MySQL 서버가 데이터가 변경되는 테이블에 잠금을 설정하고, 데이터를 변경한 후, 즉시 잠금을 해제하는 형태로 사용
77+
- 쿼리가 실행되는 동안 자동으로 획득됐다가, 쿼리 실행 완료 후 해제
78+
- InnoDB 테이블의 경우 스토리지 엔진 차원에서 레코드 기반 잠금을 제공하기 때문에 단순 데이터 변경 쿼리로 인해 묵시적인 테이블 락이 설정되지는 않음
79+
- 정확히히 테이블 락이 설정되지만 대부분의 DML 쿼리에서는 무시되고, DDL에 한해 영향을 미침
80+
81+
## 네임드 락(Named Lock)
82+
- `GET_LOCK()`을 이용해서 임의의 문자열에 대해 잠금을 설정할 수 있음
83+
- ex. `SELECT GET_LOCK('mylock'`, 2)
84+
- 사용자가 지정한 문자열에 대해 획득하고, 해제하는 잠금으로 자주 사용되지는 않음
85+
- 예를 들어 DB 1대에 5대의 웹 서버가 접속하는 경우 웹 서버별 어떤 정보를 동기화해야 하는 경우처럼 여러 클라이언트가 상호 동기화를 처리해야하는 경우 네임드 락을 활용
86+
87+
## 메타데이터 락(Metadata Lock)
88+
- 데이터베이스 객체(테이블, 뷰, ...)의 이름이나 구조를 변경하는 경우에 획득하는 잠금
89+
- 명시적으로 획득하거나 해제할 수 있는 잠금은 아니고, 자동으로 획득/해제되는 잠금
90+
91+
<br/>
92+
93+
# InnoDB 스토리지 엔진 잠금
94+
- MySQL 엔진과 별개로 스토리지 엔진 내부에서 레코드 기반 잠금 방식을 탑재
95+
- 이로 인해 동시성 처리를 제공
96+
- 하지만 이원화된 잠금 처리때문에 과거에는 스토리지 엔진에서 사용되는 잠금 정보를 MySQL 명령을 통해 접근하기에 까다로움
97+
- 최근 버전에서는 InnoDB의 트랜잭션, 잠금, 잠금 대기 중인 트랜잭션 목록을 조회할 수 있음
98+
- `information_schema` 데이터베이스에 존재하는 `INNODB_TRX`, `INNODB_LOCKS`, `INNODB_LOCK_WAITS`를 조인해서 확인
99+
- InnoDB의 잠금에 대한 모니터링도 강화되면서 Performance Schema`를 이용해서 스토리지 엔진 내부 잠금(세마포어)에 대한 모니터링도 가능
100+
101+
## InnoDB 스토리지 엔진의 잠금
102+
- 레코드 기반의 잠금 기능 제공
103+
- 작은 공간으로 관리되기 때문에 레코드 락이 페이지 락, 테이브 락으로 레벨업되는 경우는 없음
104+
- 일반 DBMS와는 다르게 InnoDB 엔진에서는 레코드 락뿐 아니라 레코드와 레코드 사이의 간격을 잠그는 갭(GAP) 락이 존재
105+
106+
![image](https://user-images.githubusercontent.com/59307414/210166791-2cc00689-6c04-49e6-8853-1aa050a2fd79.png)
107+
108+
### 레코드 락
109+
- InnoDB 엔진에서 레코드 락은 레코드 자체가 아닌 인덱스의 레코드를 잠금
110+
- 인덱스가 하나도 없는 테이블이더라도 내부적으로 자동 생성된 클러스터 인덱스를 이용하여 잠금 설정
111+
- 레코드 자체를 잠그냐, 인덱스를 잠그냐는 많은 차이가 발생
112+
113+
#### 레코드 잠금 vs 인덱스 잠금
114+
- ㅇㅇ
115+
116+
### 갭 락
117+
- 레코드 자체가 아닌 레코드와 바로 인접한 레코드 사이의 간격만을 잠그는 것을 의미
118+
- 이를 통해 레코드와 레코드 사이의 간격에 새로운 레코드가 생성(INSERT)되는 것을 제어
119+
- 갭 락은 그 자체보다 넥스트 키 락의 일부분으로 자주 사용
120+
121+
### 넥스트 키 락
122+
- 레코드 락과 갭 락을 합쳐 놓은 형태의 잠금
123+
- `innodb_locks_unsafe_for_binlog` 시스템 변수가 비활성화(0으로 설정)되면 변경을 위해 검색하는 레코드에 넥스트 키 락 방식으로 잠금 설정
124+
- 바이너리 로그에 기록되는 쿼리가 레플리카 서버에서 실행될 때 소스 서버에서 만들어 낸 결과와 동일한 결과를 만들어내도록 보장하는 것이 목적
125+
- 하지만 의외로 넥스트 키 락과 갭 락으로 이한 데드락이 발생하거나 다른 트랜잭션을 기다리게 하는 일이 자주 발생
126+
- 가능하다면 바이너리 로그 포맷을 ROW 형태로 바꿔서 넥스트 키 락이나 갭 락을 줄이는 것이 좋음
127+
- 5.5 버전까지는 많이 사용되지는 않았지만, 5.7 / 8.0 버전으로 업그레이드되면서 ROW 포맷의 바이너리 로그에 대한 안정성이 높아짐
128+
- 8.0에서는 ROW 포맷의 바이너리 로그가 기본 설정
129+
130+
### 자동 증가 락
131+
- `AUTO_INCREMENT`와 관련, 중복되지 않고 저장된 순서대로 증가하는 일련번호를 제공하기 위해 자동 증가 락(Auto increment lock)이라고 하는 테이블 수준의 잠금 지원
132+
- 자동 증가 락은 INSERT, REPLACE 쿼리 같이 새로운 레코드를 저장하는 쿼리에서만 필요
133+
- UPDATE, DELETE 등에서는 걸리지 않음
134+
135+
#### 자등 증가 락 작동 방식
136+
| `innodb_autoinc_lock_mode=0` | `innodb_autoinc_lock_mode=1` |`innodb_autoinc_lock_mode=2`|
137+
|----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|
138+
| MySQL 5.0과 동일한 잠금 방식<br/><br/>모든 Insert 문장은 자등 증가 락을 사용 | 레코드를 처리할 때 MySQL 서버가 Insert되는 레코드의 수를 정확히 예측할 수 있을 떄는 자동 증가 락을 사용하지 않고, 그보다 가볍고 빠른 래치(mutex)를 이용하여 처리<br/><br/>래치는 자동 증가 락과 달리 아주 짧은 시간동안만 잠금을 걸고, 필요한 증가 값을 반환받으면 즉시 잠금 해제<br/><br/>예측할 수 없을 때에는 자동 증가 락 사용<br/><br/> 연속 모드(consecutive mode)라고도 함 | 절대 자동 증가 락을 사용하지 않고 경량화된 래치만을 사용<br/><br/> 단건 생성이어도 연속된 자동 증가 값을 보장하지 않음<br/><br/>인터리빙 모드(interleaved mode)라고 함<br/><br/>동시 처리 성능은 높아짐<br/><br/>자동 증가 기능에 대해서는 유니크한 값이 생성된다는 것은 보장<br/><br/>STATEMENT 포맷의 바이너리 로그를 사용하는 복제의 경우 소스 서버와 레플리카 서버의 자동 증가 값이 달라질 수 있기 때문에 주의해야 함
139+
140+
> 5.7까지는 `innodb_autoinc_lock_mode`의 기본값이 1이었지만, 8.0 버전부터는 기본값이 2로 변경
141+
>
142+
> 8.0부터는 바이너리 로그 포맷이 STATEMENT가 아니라 ROW 포맷이 기본값이 되었기 때문
143+
>
144+
> 만약 8.0에서 STATEMENT 포맷의 바이너리 로그를 사용한다면 `innodb_autoinc_lock_mode`를 1로 설정해서 사용할 것을 권장
145+
146+
## 인덱스와 잠금
147+
- InnoDB는 레코드를 잠그는 것이 아니라 인덱스를 잠그는 방식으로 처리
148+
- 변경해야 할 레코드를 찾기 위해 검색한 인덱스의 레코드를 모두 락을 걸어야 함
149+
150+
```mysql
151+
# 테이블
152+
CREATE TABLE employees (
153+
emp_no INT NOT NULL, PRIMARY KEY (emp_no),
154+
birth_date DATE,
155+
first_name VARCHAR(14),
156+
last_name VARCHAR(16),
157+
gender ENUM('M', 'F'),
158+
hire_date DATE
159+
) ENGINE=INNODB;
160+
161+
# first_name index 설정
162+
ALTER TABLE employees ADD INDEX first_name_index(first_name);
163+
164+
# first_name이 Georgi 레코드 253개가 있고, first_name이 Georgi면서 last_name이 Klassen인 레코드가 1개가 있음
165+
SELECT COUNT(*) FROM employees WHERE first_name = 'Georgi'; # output: 253
166+
SELECT COUNT(*) FROM employees WHERE first_name = 'Georgi' AND last_name = 'Klassen'; # output: 1
167+
168+
# first_name이 Georgi면서 last_name이 Klassen인 사원의 입사일(hire_date)을 오늘로 변경
169+
UPDATE employees SET hire_date = NOW() WHERE first_name = 'Georgi' AND last_name = 'Klassen';
170+
```
171+
172+
- 위 예제에서 단건 업데이트지만 first_name에 인덱스가 설정되어 있기 때문에 253건의 레코드가 모두 잠김
173+
- 테이블에 인덱스가 하나도 없다면 테이블을 풀스캔하면서 모든 레코드를 잠금
174+
- 그래서 인덱스를 설정하는 게 만능이 아니기 때문에 InnoDB에서는 인덱스 설계가 중요한 이유 중 하나
175+
176+
## 레코드 수준의 잠금 확인 및 해제
177+
- `information_schema` 데이터베이스에 존재하는 `INNODB_TRX`, `INNODB_LOCKS`, `INNODB_LOCK_WAITS`를 조인해서 확인
178+
- `SHOW PROCESSLIST` 명령어를 통해 프로세스 목록 및 스레드를 조회할 수도 있음
179+
- `KILL` 명령어를 통해 스레드 삭제
180+
181+
<br/>
182+
183+
# MySQL의 격리 수준
184+
185+
| Isolation level | DIRTY READ | NON-REPEATABLE READ | PHANTOM READ |
186+
|:----------------:|------|-----------------|--------------|
187+
| READ UNCOMMITTED | **발생** | **발생** | **발생** |
188+
| READ COMMITTED | | **발생** | **발생** |
189+
| REPEATABLE READ | | | 발생(InnoDB는 없음) |
190+
| SERIALIZABLE | | | |
191+
192+
### 부정합 문제
193+
#### Dirty read
194+
- 트랜잭션에서 처리한 작업이 완료되지 않았음에도 불구하고 다른 트랜잭션에서 볼 수 있게 되는 현상
195+
- 데이터가 나타났다가 사라졌다하는 현상을 초래
196+
197+
#### Non-repeatable read
198+
- 하나의 트랜잭션 내에서 동일한 SELECT 쿼리를 실행했을 때 항상 같은 결과를 보장해야 한다는 REPEATABLE READ 정합성에 어긋나는 현상
199+
200+
#### Phantom read
201+
- SELECT ... FOR UPDATE 쿼리와 같은 쓰기 잠금을 거는 경우 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상
202+
203+
## 격리 수준
204+
### READ UNCOMMITTED
205+
- 한 트랜잭션의 변경된 내용을 커밋이나 롤백과 상관 없이 다른 트랜잭션에서 읽을 수 있는 격리 수준
206+
- 모든 부정합 문제 발생
207+
208+
### READ COMMITTED
209+
- COMMIT이 완료된 데이터만 조회 가능한 격리 수준 (undo 영역 활용)
210+
- 더티 리드 해결
211+
- 오라클에서 기본 설정으로 주로 사용
212+
213+
### REPEATABLE READ
214+
- 트랜잭션이 시작되기 전에 커밋된 내용에 관해서만 조회할 수 있는 격리 수준 (트랜잭션 번호를 기준으로 활용)
215+
- NON-REPEATABLE-READ 해결
216+
- PHANTOM READ 발생 (InnoDB에서는 PHANTOM READ 해결)
217+
- ex. `FOR UPDATE`
218+
> FOR UPDATE
219+
>
220+
> SELECT FOR UPDATE쿼리는 가정 먼저 LOCK을 획득한 SESSION의 SELECT 된 ROW들이 UPDATE 쿼리후 COMMIT 되기 이전까지 다른 SESSION들은 해당 ROW들을 수정하지 못하도록 하는 기능
221+
- MySQL에서 기본 설정으로 주로 사용
222+
223+
224+
225+
226+
> **InnoDB 스토리지 엔진에서 PHANTOM READ 해결**
227+
> - InnoDB 스토리지 엔진은 레코드 락과 갭 락을 합친 넥스트 키 락을 사용
228+
>
229+
> 테이블에 c1 = 13 , c1 = 17 인 두 레코드가 있다고 가정할 때 `SELECT c1 FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE` 쿼리를 수행하면,
230+
>
231+
> 10 <= c1 <= 12, 14 <= c1 <= 16, 18 <= c1 <= 20 인 영역은 전부 갭 락에 의해 락이 걸려서 해당 영역에 레코드를 삽입할 수 없음
232+
>
233+
> 또한 c1 = 13, c1 = 17인 영역도 레코드 락에 의해 해당 영역에 레코드를 삽입할 수 없다. 참고로 INSERT 외에 UPDATE, DELETE 쿼리도 마찬가지이다.
234+
>
235+
> 이러한 방식으로 InnoDB 스토리지 엔진은 넥스트 키 락을 이용하여 PHANTOM READ 문제를 해결
236+
237+
### SERIALIZABLE
238+
- 한 트랜잭션을 다른 트랜잭션으로부터 완전히 분리하는 격리 수준
239+
- 모든 부정합 문제 해결
240+
241+
> ref.
242+
>
243+
> 격리수준
244+
> - https://zzang9ha.tistory.com/381
245+
> - https://steady-coding.tistory.com/562
246+
> - https://tecoble.techcourse.co.kr/post/2022-11-07-mysql-isolation/
247+
>
248+
> FOR UPDATE 구문
249+
> - https://jinhokwon.github.io/mysql/mysql-select-for-update/

0 commit comments

Comments
 (0)