동시성 제어

송준섭 Junseop Song·2023년 11월 13일
1

📑 Reference

[5분순삭] 데이터베이스 01 동시성제어
동시성 제어( Concurrency Control )


🖋 동시성 제어란

다중 사용자 환경을 지원하는 데이터베이스 시스템에서 여러 트랜잭션들이 동시에 성공적으로 실행될 수 있도록 지원하는 기능
병행 수행 시 같은 데이터에 접근하여 연산을 실행해도 문제가 발생하지 않고 정확한 수행 결과를 얻을 수 있도록 트랜잭션의 수행을 제어하는 것을 의미
트랜잭션의 직렬화 수행 보장

  • 목적

    • 트랜잭션의 직렬성 보장
    • 공유도 최대, 응답시간 최소, 시스템 활동의 최대 보장
    • 데이터 무결성&일관성 보장
  • 동시성 제어가 보장되지 않는 경우 문제점

    • 갱신 내용의 손실
      • 두 개의 트랜잭션이 한 개의 데이터를 순서 없이 동시에 갱신할 경우
      • ex) 좋아요 버튼을 두 명 이상이 동시에 누를 경우 총 좋아요 수인 likeCount가 인원 수 만큼이 아닌 인원수보다 적게 증가하는 경우
    • 현황 파악 오류
      • 트랜잭션의 중간 수행 결과를 다른 트랜잭션이 참조함으로 발생하는 오류
      • ex) T1(트랜잭션 1)이 x값을 읽고 100 증가시키고 다시 쓰고, y값을 읽고 100 증가시키고 다시 쓰는 트랜잭션이라고 하고, T2가 x, y값을 읽어 합을 계산하는 트랜잭션이라고 할 때, 만약 T1의 x값 읽고 쓰기는 완료했으나 y값 읽고 쓰기 전에 T2의 x, y읽기가 실행된다면 T2의 결과 합 값은 원래 결과보다 100 작은 값이 나올 것 (y의 값이 100 증가 되기 전이므로)
    • 모순성
      • 두 개의 트랜잭션이 병행 수행 될 때 일관성이 없는 모순된 상태로 남는 문제
      • ex) 만약 T1의 연산 일부만 실행한 상태에서 T2가 자신의 연산을 모두 실행하고 종료된 경우
    • 연쇄 복귀 또는 회복 불능
      • 병행 수행되던 트랜잭션 중 한 트랜잭션에 문제가 생겨 롤백하는 경우, 다른 처리된 트랜잭션도 함께 롤백이 필요한 현상
      • ex) T1이 위와 같은 트랜잭션일 때, x값의 읽고 쓰기가 완료된 후 y의 읽고 쓰기가 진행되기 전에 T2 트랜잭션이 완료가 된 후, T1의 y 읽고 쓰기 과정에 장애가 발생하여 롤백이 필요할 때, T2가 이미 완료되었으므로 완전 초기 상태로 복귀가 불가능한 경우

⌛ 동시성 제어 기법

Locking 기법

트랜잭션이 사용하는 자원에 대하여 상호 배제 기능을 제공하는 기법

  • 공유 lock: 여러 트랜잭션이 동시에 읽기 실행 가능, 쓰기 연산을 실행 중인 트랜잭션이 있으면 다른 트랜잭션은 대기
  • 전용 lock: 한 트랜잭션이 데이터에 전용 lock을 획득하면 다른 트랜잭션들은 해당 데이터에 어떠한 lock도 획득할 수 없음 (읽기, 쓰기 불가)

공유 lock은 읽기에 대한 동시성을 제공하고, 전용 lock(배타적 lock)은 쓰기에 대한 동시성을 제어
평소엔 공유 lock으로 모든 트랜잭션이 읽기가 가능하다가 한 트랜잭션이 쓰기 연산을 수행하면 해당 트랜잭션은 전용 lock을 얻고 나머지 트랜잭션은 읽기, 쓰기 등 그 데이터에 대한 모든 권한 불가능

Locking 단위로는 데이터베이스 자체, 릴레이션(파일), 투플(레코드), 속성(필드)가 될 수 있음
Locking 단위가 클 수록 병행성 수준이 낮아지고 기법이 간단해짐

2PL(2-Phase Locking)

모든 트랜잭션들이 Lock과 Unlock 연산을 확장단계와 수축단계로 구분하여 수행

  • 확장단계: 트랜잭션은 lock만 수행할 수 있고, unlock은 수행할 수 없는 단계
  • 수축단계: 트랜잭션은 unlock만 수행할 수 있고, lock은 수행할 수 없는 단계

Time stamp

시스템에서 생성하는 Time stamp를 고유한 트랜잭션의 식별자로 부여하여 실행 순서를 미리 선택하는 기법

  • read_TS(X): 항목 X의 읽기 타임 스탬프
  • write_TS(X): 항목 X의 쓰기 타임 스탬프
  • write_item(X) 연산을 수행하는 예시
    • read_TS(X) > TS(T) or write_TS(T) > TS(T) → T를 철회 및 복귀
    • 또는 트랜잭션을 수행하고 Write_TS(X)를 TS(T)로 설정
  • read_item(X) 연산을 수행하는 예시
    • write_TS(T) > TS(T) → T를 철회 및 복귀
    • 또는 트랜잭션을 수행하고 read_TS(X)를 TS(T)로 설정

Validation(낙관적 검증)

실행 전 실행 여부에 대한 검증을 수행하지 않고, 일단 트랜잭션을 수행하고, 트랜잭션 종료시 검증을 수행하는 기법
장기 트랜잭션 철회시 자원 낭비 가능성이 낮거나 동시 사용 빈도가 낮은 시스템 사용

과정
1. Read Phase(검증 없이 트랜잭션을 실행)
2. Validation Phase(트랜잭션 종료 시 동시성 검증 실행)
3. Execution Phase(문제 없을 경우 반영, 문제 발생 시 철회)

Start(T): 트랜잭션 T가 판독 단계에 들어가면서 실행을 시작한 시간
Validation(T): 트랜잭션 T가 판독 단계를 끝내고 확인을 시작한 시간
Finish(T): 트랜잭션 T가 최종 기록 단계를 완료한 시간

MVCC(다중 버전 동시성 제어)

데이터 변경 시 변경 내용 UNDO 영역 저장
읽는 시점 이후에 변경되었거나 완료된 값을 발견 시, CR Copy 생성(일관성있는 버전)
ex) 10023 시점에 시작 된 쿼리가 10023 시점 이후에 변경된 데이터 블록을 만나는 경우, Rollback 세그먼트에 저장된 정보를 이용해 이전 시점으로 되돌리고서 값을 읽음

Undo data를 활용하여 높은 수준의 동시성과 읽기 일관성을 유지할 수 있지만 snapshot too old 에러가 발생
→ Undo 영역의 크기 조정, 불필요한 커밋 최소화, 분할된 단계적 쿼리 실행 등이 필요

🦁 TypeORM에서의 동시성 제어

트랜잭션 적용

하나의 로직을 처리하는 SQL 질의들을 집합으로 묶어 도중에 예외가 발생할 경우 Rollback 처리, 모두 성공할 경우 Commit 처리
TypeORM에서는 connection.createQueryRunner() 메서드를 통해 QueryRunner 인스턴스를 만든 후 트랜잭션을 이용할 수 있음

// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async createUserWithTransaction(userData: any): Promise<User> {
		// QueryRunner 인스턴스 생성
    const queryRunner = this.userRepository.manager.connection.createQueryRunner();

    await queryRunner.connect();  // 커넥션 연결
    await queryRunner.startTransaction();  // 트랜잭션 시작

    try {
      // 여기서 원하는 데이터베이스 작업을 수행합니다.
      const newUser = await this.userRepository.save(userData);

      // 트랜잭션 성공 시 commit
      await queryRunner.commitTransaction();

      return newUser;
    } catch (error) {
      // 에러가 발생하면 롤백
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      // queryRunner를 닫아줍니다.
      await queryRunner.release();
    }
  }
}

버전 관리

TypeORM에서는 엔티티에 버전 관리를 위한 @VersionColumn 데코레이터를 사용 가능
이를 통해 동시성 충돌을 방지

//  users.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, VersionColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @VersionColumn()
  version: number;
}

위의 예시에서 version 필드가 엔티티의 버전
이 필드는 엔티티가 수정될 때마다 자동으로 증가
만약 동시에 여러 사용자가 같은 엔티티를 수정하려고 시도하면, 버전이 일치하지 않아 충돌이 발생
이러한 방식으로 트랜잭션과 버전 관리를 활용하여 TypeORM에서 동시성을 제어

1개의 댓글

comment-user-thumbnail
2023년 11월 13일

역시 DB는 어렵네요.. 후속글도 부탁드립니다 👍

답글 달기

관련 채용 정보