=> ์์ ์ ๋จ์!!
์์)
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();
}
}
}
Atomicity(์์์ฑ) : ๋ชจ๋ ์ฑ๊ณตํ ๊ฑฐ ์๋๋ฉด ๋ค ์คํจํ๊ธฐ ํด์ค! ์ค์ผ์ ์ซ์ด!
Consistency(์ผ๊ด์ฑ) : ๋๊ฐ์ ์ฟผ๋ฆฌ๋ ์กฐํํ ๋ ๋ง๋ค ๋์ผํด์ผ๋ผ!
Isolation(๊ฒฉ๋ฆฌ์ฑ) : ์ฒ ์๊บผ ์ฒ๋ฆฌํ๋ ๋์ ์ํฌ๋ ๊ธฐ๋ค๋ ค์ค๋?
Durability(์ง์์ฑ) : ํ ๋ฒ ์ฑ๊ณตํ์ผ๋ฉด ์ฅ์ ๊ฐ ๋ฐ์ํด๋ ์ด์์์ด์ผ๋ผ!
๐ 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 ๊ณต๊ฐ์ ๋ฐฑ์
ํด๋๊ณ ์ค์ ๋ ์ฝ๋ ๊ฐ์ ๋ณ๊ฒฝ
[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ํ์(๊ฐ๊ธ์ ์ด๋ฉด ํผํ๋๊ฒ ์ข์)