Transaction-ACID

์žฅ์—ฌ์ง„ยท2022๋…„ 4์›” 21์ผ
0

1. Transaction๋ž€?๐Ÿ‘€

=> ์ž‘์—…์˜ ๋‹จ์œ„!!

์˜ˆ์‹œ)
1) PointTransaction ํ…Œ์ด๋ธ”์— ์ถฉ์ „ํ–ˆ๋‹ค๊ณ  1์ค„ ์ž‘์„ฑ(insert)
2) User ํ…Œ์ด๋ธ”์—์„œ ์ฒ ์ˆ˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ด(select)
3) ๊ธฐ์กด 500์›์— ์ถฉ์ „ 3000์›์„ ๋”ํ•œ 3500์›์œผ๋กœ ์—…๋ฐ์ดํŠธ (update)

[Case1]
1๋ฒˆ์€ ์„ฑ๊ณต 2,3๋ฒˆ์€ ์‹คํŒจ => ์ฐจ๋ผ๋ฆฌ ์ „์ฒด ๋‹ค ์‹คํŒจํ•˜๊ณ  ๋‹ค์‹œ ์‹œ๋„ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Œ
ํ•˜๋‚˜๋กœ ๋ฌถ๊ธฐ(3๊ฐœ๋ชจ๋‘ ์„ฑ๊ณตํ•ด์•ผ ์™„์ „ ์„ฑ๊ณต, ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด rollback)

โ—โ—์„œ๋น„์Šค์—์„œ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ์  ==> ๋ฐ์ดํ„ฐ์˜ ์˜ค์—ผโ—โ—
=> transaction-commit-rollback ์œผ๋กœ ๋ฐ์ดํ„ฐ์˜ ์˜ค์—ผ์„ ๋ฐฉ์ง€ ๊ฐ€๋Šฅ

typeOrm-queryRunner์‚ฌ์šฉ
=> ์‚ฌ์šฉํ•˜๊ธฐ์œ„ํ•ด์„œ๋Š” typeOrm,mysql์—ฐ๊ฒฐ ํ•„์š”
import { Connection } from ''typeorm'์„ ํ†ตํ•ด ์˜์กด์„ฑ ์ฃผ์ž…๊ฐ€๋Šฅ
์˜์กด์„ฑ์„ ํ† ๋Œ€๋กœ createQueryRunner()ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜์—ฌ Transaction Manager์ˆ˜ํ–‰ ๊ฐ€๋Šฅ

์ „์ฒด์ ์ธ ๋กœ์ง์€ try-catch-finally๋กœ ๊ฐ์‹ธ์ฃผ๊ณ  Transction์„ ์ฒ˜๋ฆฌํ•˜๋Š” save Method๋Š” queryRunner.manager๋ฅผ ์ด์šฉ

๐Ÿ”Ž๋ชจ๋“  ์ฝ”๋“œ๊ฐ€ Error์—†์ด ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋ฉด Transaction์ด ์™„๋ฃŒ๋˜์–ด commitTransction()์„ ํ˜ธ์ถœ์„ ํ†ตํ•ด ์ตœ์ข…์ ์œผ๋กœ ์„ฑ๊ณต ํ™•์ •ํ•˜๊ณ  finally์—์„œ release()ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด Transaction์ข…๋ฃŒ!
โ—์ค‘๊ฐ„์— Error๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค๋ฉด catch์—์„œ ์žก์•„๋‚ด์„œ rollbackTransaction()์„ ํ†ตํ•ด rollack์ˆ˜ํ–‰!

[Transaction ์˜ˆ์‹œ ์ฝ”๋“œ]

import { HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { throws } from 'assert';
import { Connection, Repository } from 'typeorm';
import { User } from '../users/entities/user.entity';
import {
  PointTransaction,
  POINT_TRANSACTION_STATUS_ENUM,
} from './entities/pointTransaction.entity';

@Injectable()
export class PointTransactionService {
  constructor(
    @InjectRepository(PointTransaction)
    private readonly pointTransacrionRepository: Repository<PointTransaction>,
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,

    private readonly connection: Connection,
  ) {}

  // ๊ฒฐ์ œ ์ •๋ณด ์ €์žฅ
  async create({ impUid, amount, currentUser }) {
    const queryRunner = await this.connection.createQueryRunner();
    await queryRunner.connect();

    // transaction ์‹œ์ž‘!!
    await queryRunner.startTransaction();
    try {
      // 1. pointTransaction ํ…Œ์ด๋ธ”์— ๊ฑฐ๋ž˜๊ธฐ๋ก๋งŒ 1์ค„ ์ƒ์„ฑ(์•„์ง ์‹ค์ œ ํฌ์ธํŠธ๋Š” ๋ณ€๋™X)
      const pointTransaction = await this.pointTransacrionRepository.create({
        impUid: impUid,
        amount: amount,
        user: currentUser,
        status: POINT_TRANSACTION_STATUS_ENUM.PAYMENT,
      });

      //  queryRunner๋ฅผ ํ†ตํ•ด save!
      await queryRunner.manager.save(pointTransaction); // === await this.pointTransacrionRepository.save(pointTransaction);

  
      // ์œ ์ €์˜ ๊ธฐ์กด ํฌ์ธํŠธ ์ฐพ์•„์˜ค๊ธฐ
      const user = await this.userRepository.findOne({ id: currentUser.id });

      // await this.userRepository.update(
      //   { id: user.id },
      //   { point: user.point + amount },
      // );

      //  queryRunner๋ฅผ ํ†ตํ•ด save!
      const updatedUser = this.userRepository.create({
        ...user, // ๊ธฐ์กด ์œ ์ €
        point: user.point + amount, // ํฌ์ธํŠธ๋งŒ ๋ณ€๊ฒฝ
      });
      await queryRunner.manager.save(updatedUser); // === this.userRepository.save(updatedUser)

      // commit ์„ฑ๊ณต ํ™•์ •!!!
      await queryRunner.commitTransaction();

      //save๋Š” ์‹ค์ œ ์ €์žฅ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ๊ฐ€๋Šฅ update๋Š” ๊ณผ์ •๋งŒ ์ถœ๋ ฅ
      // 3. ์ตœ์ข… ๊ฒฐ๊ณผ ํ”„๋ก ํŠธ์—”๋“œ์— ๋Œ๋ ค์ฃผ๊ธฐ
      return pointTransaction;
    } catch (error) {
      // rollback ๋˜๋Œ๋ฆฌ๊ธฐ!!
      await queryRunner.rollbackTransaction();
    } finally {
      // ์„ฑ๊ณตํ•ด๋„ ์‹คํŒจํ•ด๋„ ๋ชจ๋‘ ์‹คํ–‰๋˜์–ด์•ผํ•จ
      // queryRunner ์—ฐ๊ฒฐ ํ•ด์ œ!
      await queryRunner.release();
    }
  }
}

2. Transaction์˜ ์†์„ฑ(ACID)๐Ÿค”

Atomicity(์›์ž์„ฑ) : ๋ชจ๋‘ ์„ฑ๊ณตํ• ๊ฑฐ ์•„๋‹ˆ๋ฉด ๋‹ค ์‹คํŒจํ•˜๊ธฐ ํ•ด์ค˜! ์˜ค์—ผ์€ ์‹ซ์–ด!
Consistency(์ผ๊ด€์„ฑ) : ๋˜‘๊ฐ™์€ ์ฟผ๋ฆฌ๋Š” ์กฐํšŒํ•  ๋•Œ ๋งˆ๋‹ค ๋™์ผํ•ด์•ผ๋ผ!
Isolation(๊ฒฉ๋ฆฌ์„ฑ) : ์ฒ ์ˆ˜๊บผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋™์•ˆ ์˜ํฌ๋Š” ๊ธฐ๋‹ค๋ ค์ค„๋ž˜?
Durability(์ง€์†์„ฑ) : ํ•œ ๋ฒˆ ์„ฑ๊ณตํ–ˆ์œผ๋ฉด ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์‚ด์•„์žˆ์–ด์•ผ๋ผ!

3. Transaction-isolation

isolation์€ Transaction์˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€โ—

๐Ÿ“Œ read-uncommitted => commit๋˜์ง€ ์•Š์€ ๊ฒƒ๋„ ์กฐํšŒ ๊ฐ€๋Šฅ
==> dirty-read(๋”๋Ÿฌ์šด ์ฝ๊ธฐ) - Transaction์ž‘์—…์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜๋Š”๋ฐ๋„ ๋‹ค๋ฅธ Transaction์—์„œ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ํ˜„์ƒ

[read-uncommitted ์ ์šฉ ์˜ˆ์‹œ]

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';
import { Payment } from './entities/payment.entity';

@Injectable()
export class PaymentService {
constructor(
 @InjectRepository(Payment)
 private readonly paymentRepository: Repository<Payment>,

 private readonly connection: Connection,
) {}

async create({ amount }) {
 const queryRunner = await this.connection.createQueryRunner();
 await queryRunner.connect();
 await queryRunner.startTransaction('READ UNCOMMITTED');
 try {
   const payment = await this.paymentRepository.create({ amount });
   await queryRunner.manager.save(payment);

   // 5์ดˆ ๋’ค์— ํŠน์ • ์ด์œ ๋กœ ์‹คํŒจํ•จ!!!
   setTimeout(async () => {
     await queryRunner.rollbackTransaction();
   }, 5000);
   //   await queryRunner.commitTransaction();
 } catch (error) {
   await queryRunner.rollbackTransaction();
 }
}

async findAll() {
 const queryRunner = await this.connection.createQueryRunner();
 await queryRunner.connect();
 await queryRunner.startTransaction('READ UNCOMMITTED');
 try {
   // ๋งŒ์•ฝ 5์ดˆ ์ด๋‚ด์— ์กฐํšŒํ•˜๋ฉด, ์œ„์—์„œ ๋“ฑ๋กํ•œ ๊ธˆ์•ก(์ปค๋ฐ‹๋˜์ง€ ์•Š์€ ๊ธˆ์•ก)์ด ์กฐํšŒ๋จ
   const payment = await queryRunner.manager.find(Payment);
   await queryRunner.commitTransaction();
   return payment;
 } catch (error) {
   await queryRunner.rollbackTransaction();
 }
}
}

๐Ÿ“Œ Read-committed => commite๋œ ๊ฒƒ๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ
==> Non-Repeatable-Read(๋ฐ˜๋ณต์ ์ด์ง€ ์•Š์€ ์กฐํšŒ)
Transaction-1์ด commit์ „ Transaction-2๊ฐ€ ํ…Œ์ด๋ธ” ๊ฐ’์„ ์ฝ์œผ๋ฉด ๊ธฐ์กด ๊ฐ’ ์ถœ๋ ฅ
Transaction-1์ด commitํ›„ ์•„์ง ๋๋‚˜์ง€ ์•Š์€ Transaction-2๊ฐ€ ๋‹ค์‹œ ํ…Œ์ด๋ธ” ๊ฐ’์„ ์ฝ์œผ๋ฉด ๋ณ€๊ฒฝ๋œ ๊ฐ’์ด ์ถœ๋ ฅ
*MySQL์—์„œ๋Š” Transaction๋งˆ๋‹ค TransactionID๋ฅผ ๋ถ€์—ฌํ•˜์—ฌ TransactionID๋ณด๋‹ค ์ž‘์€ Transaction๋ฒˆํ˜ธ์—์„œ ๋ณ€๊ฒฝํ•œ ๊ฒƒ๋งŒ ์ฝ์Œ
=> Undo ๊ณต๊ฐ„์— ๋ฐฑ์—…ํ•ด๋‘๊ณ  ์‹ค์ œ ๋ ˆ์ฝ”๋“œ ๊ฐ’์„ ๋ณ€๊ฒฝ

  • ๋ฐฑ์—…๋œ ๋ฐ์ดํ„ฐ๋Š” ๋ถˆํ•„์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•˜๋Š” ์‹œ์ ์— ์ฃผ๊ธฐ์ ์œผ๋กœ ์‚ญ์ œ ํ•„์š”
  • Undo์— ๋ฐฑ์—…๋œ ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋งŽ์•„์ง€๋ฉด MySQL ์„œ๋ฒ„์˜ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ ์ €ํ•˜
    ==> ์ด๋Ÿฌํ•œ ๋ณ€๊ฒฝ๋ฐฉ์‹์€ย MVCC(Multi Version Concurrency Control)

[read-committed ์ ์šฉ ์˜ˆ์‹œ]

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';
import { Payment } from './entities/payment.entity';

@Injectable()
export class PaymentService {
  constructor(
    @InjectRepository(Payment)
    private readonly paymentRepository: Repository<Payment>,

    private readonly connection: Connection,
  ) {}

  async findAll() {
    const queryRunner = await this.connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction('READ COMMITTED');
    try {
      // ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ 500์›์ด ์กฐํšŒ๋์œผ๋ฉด,
      // ํ•ด๋‹น ํŠธ๋žœ์žญ์…˜์ด ๋๋‚˜๊ธฐ ์ „๊นŒ์ง€๋Š”(์ปค๋ฐ‹ ์ „๊นŒ์ง€๋Š”) ๋‹ค์‹œ ์กฐํšŒํ•˜๋”๋ผ๋„ ํ•ญ์ƒ 500์›์ด ์กฐํšŒ(Repeatable-Read) ๋˜์–ด์•ผ ํ•จ
      // 1์ดˆ๊ฐ„ ๋ฐ˜๋ณตํ•ด์„œ ์กฐํšŒํ•˜๋Š” ์ค‘์—, ๋ˆ„๊ตฐ๊ฐ€ ๋“ฑ๋กํ•˜๋ฉด(create), Repeatable-Read ๋ณด์žฅ๋˜์ง€ ์•Š์Œ => Non-Repeatable-Read ๋ฌธ์ œ!!!
      setInterval(async () => {
        const payment = await queryRunner.manager.find(Payment);
        console.log(payment);
      }, 1000);

      //   await queryRunner.commitTransaction()const payment = await queryRunner.manager.find(Payment);;
    } catch (error) {
      await queryRunner.rollbackTransaction();
    }
  }

  async create({ amount }) {
    const queryRunner = await this.connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction('READ COMMITTED');
    try {
      // ์ค‘๊ฐ„์— ๋ˆ ์ถ”๊ฐ€ํ•ด๋ณด๊ธฐ
      const payment = await this.paymentRepository.create({ amount });
      await queryRunner.manager.save(payment);
      await queryRunner.commitTransaction();
    } catch (error) {
      await queryRunner.rollbackTransaction();
    }
  }
}

๐Ÿ“Œ Repeatable-Read => ๋ฐ˜๋ณต์ ์ธ ์กฐํšŒ
==> Phantom-Read(์œ ๋ น ์ฝ๊ธฐ) (= Mysql์€ ๋””ํดํŠธ ์ฐจ๋‹จ)
์ข…์ข… ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ ์ˆ˜ํ–‰ํ•œ ๋ณ€๊ฒฝ ์ž‘์—…์— ์˜ํ•ด ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ณด์˜€๋‹ค๊ฐ€ ์•ˆ ๋ณด์˜€๋‹ค๊ฐ€ ํ•˜๋Š” ํ˜„์ƒ

๐Ÿ“Œ Seriaizable
์„ฑ๋Šฅ ์ธก๋ฉด์—์„œ๋Š” ๋™์‹œ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ์ด ๊ฐ€์žฅ ๋‚ฎ์œผ๋ฉฐ ๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์ด์ง€๋งŒ ๊ฐ€์žฅ ์—„๊ฒฉโ—โ—
DB์—์„œ๋Š” ๊ฑฐ์˜ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ

๋‚™๊ด€์ ๋ฝ vs ๋น„๊ด€์ ๋ฝ
[๋น„๊ด€์ ๋ฝ์˜ ์ข…๋ฅ˜]
๊ณต์œ ๋ฝ(Shared Lock) - ์ฝ๊ธฐ์ „์šฉ ์“ฐ๊ธฐ์ž ๊ธˆ( pessimistic_read)
๋ฒ ํƒ€๋ฝ(Exclusive Lock) - ์ฝ๊ธฐ์“ฐ๊ธฐ ๋ชจ๋‘ ์ž ๊ธˆ(pessimistic_write)

[Seriaizable ์ ์šฉ ์˜ˆ์‹œ]

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';
import { Payment } from './entities/payment.entity';

@Injectable()
export class PaymentService {
  constructor(
    @InjectRepository(Payment)
    private readonly paymentRepository: Repository<Payment>,

    private readonly connection: Connection,
  ) {}

  async findAll() {
    const queryRunner = await this.connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction('SERIALIZABLE');
    try {
      // ์กฐํšŒ์‹œ ๋ฝ์„ ๊ฑธ๊ณ  ์กฐํšŒํ•จ์œผ๋กœ์จ, ๋‹ค๋ฅธ ์ฟผ๋ฆฌ์—์„œ ์กฐํšŒ ๋ชปํ•˜๊ฒŒ ๋ง‰์Œ(๋Œ€๊ธฐ์‹œํ‚ด) => Select ~ For Update
      const payment = await queryRunner.manager.find(Payment, {
        lock: { mode: 'pessimistic_write' },
      });
      console.log(payment);

      // ์ฒ˜๋ฆฌ์— 5์ดˆ๊ฐ„์˜ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆผ์„ ๊ฐ€์ •!!
      setTimeout(async () => {
        await queryRunner.commitTransaction();
      }, 5000);
      return payment;

      // await queryRunner.commitTransaction()const payment = await queryRunner.manager.find(Payment);;
    } catch (error) {
      await queryRunner.rollbackTransaction();
    }
  }

  async create({ amount }) {
    const queryRunner = await this.connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction('SERIALIZABLE');
    try {
      // ์กฐํšŒ๋ฅผ ํ–ˆ์„๋•Œ, ๋ฐ”๋กœ ์กฐํšŒ๋˜์ง€ ์•Š๊ณ  ๋ฝ์ด ํ’€๋ฆด ๋•Œ ๊นŒ์ง€ ๋Œ€๊ธฐ
      const payment = await queryRunner.manager.find(Payment);
      console.log('========== ์ฒ ์ˆ˜๊ฐ€ ์‹œ๋„ ==========');
      console.log(payment);
      console.log('==============================');
      await queryRunner.commitTransaction();
      return payment;
    } catch (error) {
      await queryRunner.rollbackTransaction();
    }
  }
}

โ—โ—โ—โ—Serializable๊ฐ€ ๋ฌด์กฐ๊ฑด์ ์œผ๋กœ ์ข‹์€ ๊ฑด ์•„๋‹˜! ์†๋„๊ฐ€ ๋Š๋ ค์งˆ ์ˆ˜ ์žˆ์–ด์„œ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์‚ฌ์šฉํ•ด์•ผํ•จโ—โ—โ—โ—
๐Ÿ”Ž ๋ฐ๋“œ๋ฝ(๋™์‹œ์ ์œผ๋กœ ๋ฝ์ด ๊ฑธ๋ฆฐ ์ƒํƒœ)
=> ํ•˜๋‚˜๋ฅผ ๊ฐ•์ œ๋กœ rollbackํ•„์š”(๊ฐ€๊ธ‰์ ์ด๋ฉด ํ”ผํ•˜๋Š”๊ฒŒ ์ข‹์Œ)


๊ณต๋ถ€ํ•˜๋ฉฐ ์ž‘์„ฑํ•˜๊ณ  ์žˆ๋Š” ๋ธ”๋กœ๊ทธ์ž…๋‹ˆ๋‹ค.
์ž˜๋ชป๋œ ๋‚ด์šฉ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ ํ˜น์‹œ ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€ ๋‹ฌ์•„์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค ๐Ÿ˜Š

0๊ฐœ์˜ ๋Œ“๊ธ€