Database/RDBMS

DB 동시성 문제를 해결하는 방법 (이벤트 처리, 콘서트 예매 등)

귀찮은 개발자 2024. 2. 9. 22:37
더보기

2022 년도에 작성된 글 입니다.

동시성 문제를 해결하는데에는 4가지 방법이 떠오른다. 

  1. 트렌젝션을 통해 Read 와 Write 을 반복적으로 수행하는 방법
  2. Table 의 유니크키 결합을 통한 방법으로 Multi Index 을 사용하는 방법 
  3. PK 을 생성할 때 2개의 특정 값을 조합하여 사용하는 방법 (상품번호:주문순서)
  4. 데이터를 하나의 스택에 담고 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번을 이용하는게 빠른 처리가 가능할 것 같다. 

 

개인적으로 공부할 때 아래 글을 참고해 봐야겠다. 

https://chrisjune-13837.medium.com/db-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95-f5e52e2e3

 

[DB] 동시성 문제 해결방법

동시성 문제가 무엇이고 해결방법에 대하여 알아보도록 한다.

chrisjune-13837.medium.com

https://untitledtblog.tistory.com/131?category=714634

 

[관계형 데이터베이스] - 동시성 (Concurrency)

1. 데이터베이스에서의 동시성 데이터베이스는 다수의 사용자들이 동시에 접근하는 경우가 빈번하게 발생한다. 그러나 여러 사용자가 동시에 데이터베이스에 접근하는 상황에서 사용자들에 대

untitledtblog.tistory.com

https://youtu.be/oV9rvDllKEg