티스토리 뷰
Transaction이란?
예를 들어 하나의 데이터에 두 개의 접근이 있다고 가정해보자
A가 B에게 돈을 입금한다.
-> A의 계좌에서 해당 금액만큼 차감한다.
-> B의 계좌에서 해당 금액만큼 추가한다.
두 개처럼 보이지만 하나의 과정이 Transaction
→ 둘 중의 하나라도 실패한다면 이 과정의 이전으로 상태를 돌려야한다.
ACID
트랜잭션의 특징을 줄여서 ACID라고 부른다.
ACID는 줄임말로 각각 원자성, 일관성, 독립성, 지속성을 의미한다.
- Atomicity
- 원자성으로 트랜잭션이 DB에 모두 반영되거나, 모두 반영되지 않아야 한다.
- Consistency
- 일관성으로 작업 처리 결과가 늘 일관성 있어야 한다.
- Isolation
- 독립성으로 트랜잭션이 여러 개가 동시에 실행되고 있는 경우 다른 트랜잭션의 연산에 끼어들 수 없다.
- Durability
- 지속성으로 트랜잭션이 완료되었을 때 결과가 반영되야야 한다.
Commit, Rollback
- Commit - 트랜잭션이 성공적으로 끝나 DB가 일관성 있는 상태일 때를 의미한다.
- Rollback - 트랜잭션이 비정상적으로 종료되어 원자성이 깨진 경우 다시 시작하거나 처리된 결과를 취소시킨다.
그러면 언제 트랜잭션이 필요할까?
앱잼 내 두 과정에서 트랜잭션이 필요했다.
- 동시에 하나의 데이터가 수정되는 경우 → transaction 필요
- 사진 2장을 저장할 때 한장만 저장되는 경우 → transaction 필요
이제 Transaction을 사용해보자!
3개의 경우에 따라 이용할 수 있다.
- Dependent writes → Nested writes
- Independent writes → $transaction API, batch operations
- Read, modify, write → Idempotent operations, Optimistic concurrency control, Interactive transactions
- Dependent Writes - Nested writes
✅ 동시에 ID에 관련된 2개 이상의 데이터를 만들고 싶을 때 사용한다.
✅ 동시에 ID에 관련된 2개 이상의 데이터를 수정하고 생성하고 싶을 때 사용한다.
const createVote = async (...) => {
const data = await prisma.vote.create({
data: {
...
status: true,
count: 0,
created_at: dayjs().add(9, "hour").format(),
date: +dayjs().format("YYYYMM"),
** Picture: {
create: [
{ url: voteDTO.pictures[0], count: 0 },
{ url: voteDTO.pictures[1], count: 0 },
],
},**
},
});
상황: 투표 테이블에 데이터를 추가한 후에 사진 테이블에 해당 url을 추가한다.
→ Transaction 중 Nested writes를 이용하여 Picture 테이블에 저장하여 한번에 과정이 이루어지도록 구현했다.
- Independent writes - Bulk operations
✅ 하나의 트랜잭션에서 같은 타입의 여러 레코드를 쓸 때 사용한다.
✅ updateMany, deleteMany
await prisma.email.updateMany({
where: {
user: {
id: 10,
},
unread: true,
},
data: {
unread: false,
},
})
위의 예시처럼 한번에 여러 데이터에 대해서 true과 false 값을 줄 수 있다.
- Independent writes - $transaction API
✅ 하나의 트랜잭션으로 여러 작업을 실행할 수 있다.
✅ 독립적인 쓰기를 돕는 일반적인 방법이다.
const deletePicture = prisma.picture.deleteMany({
...
});
const deleteVote = prisma.vote.deleteMany({
...
});
const deleteUser = prisma.user.delete({
...
});
await prisma.$transaction([deletePicture, deleteVote, deleteUser])
deletePicture, deleteVote, deleteUser → 이 세개의 함수를 하나의 트랜잭션으로 다 같이 성공하거나 다 같이 실패한다.
- Read, modify, write - Idempotent(멱등성) APIs
✅같은 파라미터로 같은 로직을 여러 번 실행하여 동일한 결과를 얻을 수 있다.
✅ 멱등성이란 이름 그대로 연산을 여러 번 적용하더라도 결과가 달라지지 않는다.
const teamId = 9
const planId = 'plan_id'
// Count team members
const numTeammates = await prisma.user.count({
where: {
teams: {
some: {
id: teamId,
},
},
},
})
// Create a customer in Stripe for plan-9454549
const customer = await stripe.customers.create({
externalId: teamId,
plan: planId,
quantity: numTeammates,
})
// Update the team with the customer id to indicate that they are a customer
// and support querying this customer in Stripe from our application code.
await prisma.team.update({
data: {
customerId: customer.id,
},
where: {
id: teamId,
},
})
위의 코드는 겉으로 보기엔 문제가 없어 보인다.
하지만 여러 번 연산을 하게 되면 문제가 발생한다.
- Team의 externalId가 이미 존재하면 customerId를 반환받을 수 없다.
- Team의 externalId가 unique한 값이 아니기에 다른 subscription을 생성한다. → 멱등성을 지키지 않는다.
// Calculate the number of users times the cost per user
const numTeammates = await prisma.user.count({
where: {
teams: {
some: {
id: teamId,
},
},
},
})
// Find customer in Stripe
let customer = await stripe.customers.get({ externalId: teamID })
if (customer) {
// If team already exists, update
customer = await stripe.customers.update({
externalId: teamId,
plan: 'plan_id',
quantity: numTeammates,
})
} else {
customer = await stripe.customers.create({
// If team does not exist, create customer
externalId: teamId,
plan: 'plan_id',
quantity: numTeammates,
})
}
// Update the team with the customer id to indicate that they are a customer
// and support querying this customer in Stripe from our application code.
await prisma.team.update({
data: {
customerId: customer.id,
},
where: {
id: teamId,
},
})
그렇기에 위의 코드로 수정하는 것이 바람직하다.
멱등성을 지키기 위해 먼저 get을 통해 customer를 받아와 상황에 맞는 함수를 실행하는 것이 좋다.
- Read, modify, write - Optimistic concurrency control
✅ OCC는 locking에 의존하지 않는 하나의 객체에 대한 동시 작업을 수행하는 모델이다.
✅ 동시에 요청 수가 많을 때, 동시 요청 간의 충돌이 거의 발생하지 않기 위해 사용한다.
const movieName = 'Hidden Figures'
// Find first available seat
const availableSeat = await prisma.seat.findFirst({
where: {
movie: {
name: movieName,
},
claimedBy: null,
},
})
// Throw an error if no seats are available
if (!availableSeat) {
throw new Error(`Oh no! ${movieName} is all booked.`)
}
// Claim the seat
await prisma.seat.update({
data: {
claimedBy: userId,
},
where: {
id: availableSeat.id,
},
})
영화관 좌석을 예시로 본다면 두 사람에게 A라는 좌석이 보이고, 두 사람 모두 A라는 좌석을 선택할 수 있다. 첫 번째 사람이 선택하였더라도 시스템에선 결과적으로 두 번째 사람의 선택만이 남게 된다. 이를 version을 통해 관리할 수 있다.
const userEmail = 'alice@prisma.io'
const movieName = 'Hidden Figures'
// Find the first available seat
// availableSeat.version might be 0
const availableSeat = await client.seat.findFirst({
where: {
Movie: {
name: movieName,
},
claimedBy: null,
},
})
if (!availableSeat) {
throw new Error(`Oh no! ${movieName} is all booked.`)
}
// Only mark the seat as claimed if the availableSeat.version
// matches the version we're updating. Additionally, increment the
// version when we perform this update so all other clients trying
// to book this same seat will have an outdated version.
const seats = await client.seat.updateMany({
data: {
claimedBy: userEmail,
version: {
increment: 1,
},
},
where: {
id: availableSeat.id,
version: availableSeat.version, // This version field is the key; only claim seat if in-memory version matches database version, indicating that the field has not been updated
},
})
if (seats.count === 0) {
throw new Error(`That seat is already booked! Please try again.`)
}
version을 통해 첫 번째 사람과 두 번째 사람에게 둘 다 version이 0인 A좌석이 보인다. 첫 번째 사람이 예약한 순간 version이 0에서 1로 바뀌게 된다. 두 번째 사람이 A좌석을 예약할 때 DB의 version과 맞지 않아 예약에 실패하게 된다.
- Read, modify, write - Interactive transactions
✅ $transaction에 비동기 함수를 전달한다.
✅ 비동기 함수에 전달되는 첫 번째 인자는 PrismaClient의 인스턴스(= tx)이다.
tx에서 호출된 모든 Prisma 호출은 트랜잭션으로 캡슐화된다.
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function transfer(from: string, to: string, amount: number) {
return await prisma.$transaction(async (tx) => {
// 1. Decrement amount from the sender.
const sender = await tx.account.update({
data: {
balance: {
decrement: amount,
},
},
where: {
email: from,
},
})
// 2. Verify that the sender's balance didn't go below zero.
if (sender.balance < 0) {
throw new Error(`${from} doesn't have enough to send ${amount}`)
}
// 3. Increment the recipient's balance by amount
const recipient = tx.account.update({
data: {
balance: {
increment: amount,
},
},
where: {
email: to,
},
})
return recipient
})
}
async function main() {
// This transfer is successful
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
// This transfer fails because Alice doesn't have enough funds in her account
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
하나의 트랜잭션으로 묶어 처리할 수 있으며 함수의 끝에 도달한 경우 트랜잭션이 commit되고, 함수의 끝에 error를 만나게 되면 비동기 함수는 예외를 던져 자동적으로 트랜잭션이 rollback된다.
const data = await prisma.$transaction(async (tx) => {
const sticker = await findSticker(
tx,
stickerCreateDto.pictureId,
stickerCreateDto.emoji
);
...
});
위의 코드도 마찬가지로 interactive transaction을 이용하여 하나의 트랜잭션으로 묶어 실행하게 구현하였습니다.
참고: https://www.prisma.io/docs/guides/performance-and-optimization/prisma-client-transactions-guide
- Total
- Today
- Yesterday
- 괄호회전하기
- 응답코드
- xv6
- 다음큰숫자
- 우분투설치
- 최고의집합
- interrupt
- 뉴스클러스터링
- qemu
- 실패율
- Auditing
- 백준
- dp
- 시스템콜
- 영어끝말잇기
- 머신러닝
- 정수삼각형
- OS
- 프로그래머스
- 최솟값구하기
- ubuntu
- PostgreSQL
- 이진변환반복하기
- Android
- 프리티어
- 운영체제
- PasswordEncoder
- springboot
- AWS
- RDS
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |