서비스에서 가장 치명적인 문제는 데이터의 오염이다.
중요한 데이터를 오염시키지 않기 위해
트랜잭션을 만들어 성공했을때는 모두 성공을 실패했을 때는 롤백 시켜주어야 한다.
typeorm에서는 트랜잭션 기능을 제공해준다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';
import { User } from '../user/entities/user.entity';
import {
PointTransaction,
POINT_TRANSACTION_STATUS_ENUM,
} from './entities/pointTransaction.entity';
@Injectable()
export class PointTransactionService {
constructor(
@InjectRepository(PointTransaction)
private readonly pointTransactionRepository: 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(); // 디비에 연결 (연결 제한 개수가 정해져있으므로 디비에서 늘려줘야한다.)
await queryRunner.startTransaction(); // 트랜잭션 시작
try {
// 1. pointTransaction 테이블에 거래기록 생성
const pointTansaction = await this.pointTransactionRepository.create({
impUid,
amount,
user: currentUser,
status: POINT_TRANSACTION_STATUS_ENUM.PAYMENT,
});
// await this.pointTransactionRepository.save(pointTansaction);
await queryRunner.manager.save(pointTansaction); // 임시저장
// throw new Error(); // 임의의 에러발생
// 2. 유저정보를 조회
const user = await this.userRepository.findOne({ id: currentUser.id });
// 3. 유저의 돈 업데이트
// await this.userRepository.update(
// { id: user.id },
// { point: user.point + amount },
// );
// 유저 객체 생성
const updatedUser = this.userRepository.create({
...user,
point: user.point + amount,
});
await queryRunner.manager.save(updatedUser); // 임시저장
await queryRunner.commitTransaction(); // 에러없을시 커밋
// 4. 최종결과 돌려주기
return pointTansaction;
} catch (error) {
await queryRunner.rollbackTransaction(); // 임시저장 하나라도 실패시 트랜잭션 전체 롤백.
} finally {
await queryRunner.release(); // 접속(커넥트) 종료 // 종료시켜주지않으면 허용개수 초과시 에러발생.
}
}
}
show databases; -- 데이터베이스 목록 조회
use myproject; -- myproject 데이터베이스 사용
show tables; -- 테이블 목록 조회
show variables; -- 데이터베이스에서 관리하는 변수 목록 조회 (max_connections로 커넥션 최대값 확인)
show status; -- 현재 데이터베이스 상태 조회 (Threads_connected로 현재 커넥션 개수 확인)
set GLOBAL max_connections = 1000; -- 커넥션 최대갓 조정
show processlist; -- 현재 프로세스 목록 확인 (현재 연결된 커넥션 목록 확인)
kill 20; -- 프로세스 아이디로 강제종료 시키기 (실수로 종료 안시킨 프로세스 종료시 사용)
Isolation-Level | Dirty-Read | Non-Repeatable-Read | Phantom-Read |
---|---|---|---|
Read-Uncommitted | O | O | O |
Read-Committed | X | O | O |
Repeatable-Read | X | X | O |
Serializable | X | X | X |
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'); // READ UNCOMMITTED => 가장 낮은 단계(Dirty-Read 등 발생): 롤백되기전까진 잠깐의 저장된 상태가 조회가된다.
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();
} finally {
//await queryRunner.release();
}
}
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();
} finally {
//await queryRunner.release();
}
}
}
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'); // 커밋되지않은 데이터가 조회되는 Dirty-Read는 해결됐지만 Non-Repeatable-Read => 한 트랜잭션에서 반복된 조회시 커밋된 데이터들의 조회값이 다르게 나옴.
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();
} catch (error) {
await queryRunner.rollbackTransaction();
} finally {
//await queryRunner.release();
}
}
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();
} finally {
//await queryRunner.release();
}
}
}
팬텀리드는 Non-Repeatable-Read는 트랜잭션에 ID를 부여하여 트랜잭션이 끝나기전까지는 데이터가 커밋되어도 조회 결과가 바뀌지 않도록 해결하였지만 데이터의 개수를 조회하거나 업데이트를 해주거나 한 경우에는 한 조회 트랜잭션내에서 조회값이 바뀌는 경우이다. (데이터 저장 커밋 같은 경우에는 이전값을 undo에 임시 백업해두어 트랜잭션ID로 비교후 값을 가져오기때문에 해결가능했다)
SERIALIZABLE은 이 문제를 차단해준다.
(mysql에서는 phantom-read를 자동차단해준다. - 트랜잭션 설정없을시 Repeatable-Read가 기본값 + 팬텀리드차단 (보통 DB들은 Read-Committed가 기본값 Repeatable-Read시에도 팬텀리드 자동차단X))
또한 SERIALIZABLE 레벨에서는 lock을 걸 수 있다.
락을 검으로써 해당 데이터를 여러사용자가 동시에 조작(읽고 쓰기)하는것을 방지할 수 있다.
대기상태가 되었다가 실행되므로 성능은 느려지기에 오염이 발생할 수 있는 중요 데이터에만 락을 건다.
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;
} catch (error) {
await queryRunner.rollbackTransaction();
} finally {
//await queryRunner.release();
}
}
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();
} finally {
//await queryRunner.release();
}
}
}