2022 년도에 작성된 글 입니다.
동시성 문제를 해결하는데에는 4가지 방법이 떠오른다.
- 트렌젝션을 통해 Read 와 Write 을 반복적으로 수행하는 방법
- Table 의 유니크키 결합을 통한 방법으로 Multi Index 을 사용하는 방법
- PK 을 생성할 때 2개의 특정 값을 조합하여 사용하는 방법 (상품번호:주문순서)
- 데이터를 하나의 스택에 담고 Pub/Sub 으로 Database 에 순차적으로 Insert 하는 방법
이커머스로 이직한 두번째 날 1번의 방식으로 이벤트 응모를 처리하다가 1등 당첨자가 여러명 나오는 문제가 발생했었다.
이러한 예시로 민트패스가 있었다. (구매하려고 들어갔는데 후기 보고 알게됨)
https://www.ssg.com/item/itemView.ssg?itemId=1000577694047
[일본 무제한 왕복 이용권] 에어서울 민트패스
여기를 눌러 링크를 확인하세요.
www.ssg.com
만약 ACID 의 원칙에 따라 테이블 생성을 했다면 이러한 문제는 발생하지 않았을 것 같다.
1. 트렌젝션을 통해 Read 와 Write 을 반복적으로 수행하는 방법
Postgres 는 Returning 을 통해 Insert/update 된 값을 확인할 수 있지만 MySQL 은 불가능하여 READ 을 한버 더 해야한다.
임의로 코드를 작성했을 때 이런 결과가 나오겠지만, 일단 좋은 방법이 아니란건 알 수 있다.
단일 인스턴스에서 실행되는 어플리케이션이라면 괜찮겠지만
비동기 프로그래밍이나 여러개의 인스턴스, 멀티 쓰레드에서는 좋은 방법은 아니다.
// true: 데이터 Insert 가능
// false: 다음 번호로 insert 가능
// throw: 롤백 후 재시도
async function readFromDatabase(conn, number) {
const [rows] = await conn.query('SELECT column1 FROM example_table WHERE column1 = (?)', [number]);
if (rows.length === 0) {
return 'insert';
}
if (rows.length === 1) {
return 'next';
}
throw new Error();
}
async function lastData(conn) {
const [rows] = await conn.query('SELECT MAX(column1) as max FROM example_table');
return rows[0].max;
}
async function writeToDatabase(conn, number) {
await conn.query('INSERT INTO example_table (column1) VALUES (?, ?)', [number]);
}
try {
const conn = await mysql.createConnection(connectionConfig);
await conn.beginTransaction();
let i = await lastData(conn);
while(true) {
i += 1;
const result = await readFromDatabase(conn, i);
if (result === 'insert') {
await writeToDatabase(conn);
const result = await readFromDatabase(conn, i);
if (result === 'next') {
break;
}
}
if (result === 'next') {
continue;
}
}
// 트랜잭션 커밋
await conn.commit();
// MySQL 연결 종료
await conn.end();
} catch (error) {
await conn.rollback();
// 재귀함수로 다시 시도하는데 횟수 제한을 둘 필요는 있음
}
Table 의 유니크키 결합을 통한 방법으로 Multi Index 을 사용하는 방법
아래의 경우 event_winnsers 테이블에 다중 인덱스를 추가했다.
다중 인덱스를 사용했기 때문에 user_id 와 evenv_id 는 1개밖에 못 들어간다.
아래 예시는 중복 이벤트 당첨은 되지 않겠지만, 정해두었던 당첨자수를 초과할 수 있다.
이런 경우 하나의 Worker 을 만들어 Worker 에서 데이터 입력을 처리하면 된다.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
...
);
CREATE TABLE events (
id INT AUTO_INCREMENT PRIMARY KEY,
event_title VARCHAR(255) NOT NULL,
event_content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
winner_list INT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE event_winners (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
event_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (event_id) REFERENCES users(id),
UNIQUE KEY idx_user_event_winners (user_id, event_id)
);
PK 을 생성할 때 2개의 특정 값을 조합하여 사용하는 방법 (상품번호:주문순서)
테이블 필드가 약간 변경되었다.
id 컬럼이 AUTO_INCREMENT 가 아니라 TEXT 으로 변경되었다.
TEXT 에 데이터를 입력할 때 event_id:winner_list 을 저장하면 된다. winner_list 는 1씩 증가하도록 어플리케이션에서 입력해주면 된다. 만약 중복되는 값이 입력될 경우 ACID 의 일관성(Consistency) 원칙으로 인해 중복 값이 입력될 수 없다.
CREATE TABLE event_winners (
id TEXT,
user_id INT NOT NULL,
event_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (event_id) REFERENCES users(id),
UNIQUE KEY idx_user_event_winners (user_id, event_id)
);
데이터를 하나의 스택에 담고 Pub/Sub 으로 Database 에 순차적으로 Insert 하는 방법
이러한 방법을 사용하여 실무에서 활용해 본적은 없지만, 2번째 방식에 데이터 입력을 할 때 Redis/Kafka/RabbitMQ 을 통해 데이터를 입력한 후 하나의 Worker 에서 구독중인 데이터를 찾아 데이터베이스에 순차적으로 입력하는 방식이 될 것 같다.
하지만 Redis/Kafka/RabbitMQ 의 데이터가 유실될 경우 사용자의 접수 내역을 확인할 수 없는 문제가 발생한다.
Restful 을 Pooling 으로 처리할 것도 아니기 때문에 2번과 4번의 방법보단 3번을 이용하는게 빠른 처리가 가능할 것 같다.
개인적으로 공부할 때 아래 글을 참고해 봐야겠다.
[DB] 동시성 문제 해결방법
동시성 문제가 무엇이고 해결방법에 대하여 알아보도록 한다.
chrisjune-13837.medium.com
https://untitledtblog.tistory.com/131?category=714634
[관계형 데이터베이스] - 동시성 (Concurrency)
1. 데이터베이스에서의 동시성 데이터베이스는 다수의 사용자들이 동시에 접근하는 경우가 빈번하게 발생한다. 그러나 여러 사용자가 동시에 데이터베이스에 접근하는 상황에서 사용자들에 대
untitledtblog.tistory.com
'Database > RDBMS' 카테고리의 다른 글
데이터베이스 인덱스(클러스터드, 세컨더리, 커버링) (0) | 2024.04.18 |
---|---|
MySQL 로그 종류 알아보기 (0) | 2024.03.02 |
MySQL CDC Replication with Kafka (0) | 2024.02.14 |
MySQL Master-Slave Replication with Docker (0) | 2024.02.11 |
[MySQL 8 접속 에러] - caching_sha2_password (0) | 2024.02.09 |