티스토리 뷰
다시, 맨처음 이야기를 시작한 원래 코드로 돌아가자.
최초 작성한 코드는 다음과 같다.
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void sendRegistered(Long scheduleId){
// 스케쥴된 아이템을 찾는다
Schedule sending = scheduleRepository.findById(alimtalkScheduleId).
orElseThrow(() -> new GeneralException(Type.NO_STORED_SCHEDULED_BY_ID));
List < ScheduledItem > items = sending.getItems();
boolean hasError = false;
// 해당 아이템의 하위 아이템들을 순회한다.
for (ScheduledItem item: items) {
// 해당 아이템이 수행되거나 끝났을 경우 넘긴다.
if (item.getBatchStatus().equals(BatchStatus.FINISHED) | |
item.getBatchStatus().equals(BatchStatus.RUNNING)) {
continue;
}
// RUNNING으로 마크한다.
scheduledItemService.markItemStatus(item.getId(), BatchStatus.RUNNING);
boolean curError = false;
try {
// A 함수를 하위 아이템에 수행한다.
curError = scheduledItemService.send(item);
} catch(Exception e) {
// 만약 에러가 있다면, 스케쥴된 아이템
hasError = curError = true;
scheduledItemService.markItemStatus(
item.getId(), BatchStatus.FAILED);
} finally {
hasError |= curError;
if (!curError) {
scheduledItemService.markItemStatus(
item.getId(), BatchStatus.FINISHED);
}
}
}
if (hasError) {
sending.setBatchStatus(BatchStatus.FAILED);
} else {
sending.setBatchStatus(BatchStatus.FINISHED);
}
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public boolean send(AlimtalkItem item){
// send함수의 메인 로직 1초 정도 소요된다.
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void markItemStatus(Long itemId, BatchStatus status){
// 아이템의 상태를 바꾸는 함수
ScheduledItem item = scheduledItemRepository.findById(itemId).orElseThrow(
() -> new GeneralException(Type.NO_STORED_SCHEDULED_BY_ID)
)
item.setBatchStatus(status)
}
sendRegistered는 부모 아이템의 자식을 순회한다.
여기서 다음 규칙을 지켜야한다.
1-> sendRegistered는 여러번 호출되어도 되나 단 한번만 호출되어야한다.
2-> 만약, sendRegistered가 동일한 아이템에 여러번 호출될 수 있나?
---> 그럴 수 있다. 여러 인스턴스에서 다음과 같은 로직으로 호출할 수 있다.
SELECT sch.schedule_id FROM schedules sch
WHERE sch.status = 'FAILED' or sch.status = 'WAITING'
for (Item i: ids){
sendRegistered(i);
}
sendRegistered 함수를 여러 쓰레드에서 호출하게 되면 다음과 같은 상태 비교는 아무런 효과가 없다.
// 해당 아이템의 하위 아이템들을 순회한다.
for (ScheduledItem item: items) {
// 해당 아이템이 수행되거나 끝났을 경우 넘긴다.
if (item.getBatchStatus().equals(BatchStatus.FINISHED) | |
item.getBatchStatus().equals(BatchStatus.RUNNING)) {
continue;
}
// RUNNING으로 마크한다.
scheduledItemService.markItemStatus(item.getId(), BatchStatus.RUNNING);
boolean curError = false;
왜냐하면 트랜잭션의 읽는 시점에서 다른 트랜잭션이 커밋한 것을 보장받지 못하는 점이다.
해당 함수의 목적은 전송되지 않은 레코드를 모두 읽고, 해당 것을 전송 하는 것이다.
전송의 시간이 오래 소요되면, 트랜잭션이 길어지므로 커밋되기 전에 여러 트랜잭션이 동일한 레코드를 읽어 전송할 가능성이 있다.
따라서, 트랜잭션을 시작할 때 X-Lock 또는 S-Lock으로 해당 레코드의 잠금을 획득해야한다. 낙관적락은 전송 이후에 레코드를 업데이트
이후 충돌을 알기 때문에 적절하지 못하다. 낙관적 락을 활용하려면, 전송 직전에 충돌을 감지하고 롤백할 수도 있다.
단, 잠금의 획득시간이 길면 다른 트랜잭션의 성능이 저하될 수 있다.
MySQL에서는 레코드 락이 걸린 레코드를 제외하고 잠금을 획득할 수 있는 기능이 있다. (SKIP LOCKED)
하지만 트랜잭션이 길면, 어플리케이션과 데이터베이스의 부하가 늘어난다.
상태의 직렬성을 보장하기위해 *-LOCK을 사용하되, 작업시간이 긴 부분은 분리하여 처리하는 방법이 있을 것이다.
W: WAITING, F: FAILED, R: RUNNING, FIN: FINISHED
T1 BEGIN ------- READ (W/F)(X-LOCK SKIP LOCKED) ---------- WRITE (W/F -> R) -------- COMMIT
↓
↓ -----------------------------JOB(send + success)--------------------------------------------
T2 BEGIN ---------------------------UPDATE(R->F/FIN) --------------------------------- COMMIT
T1은 레코드 별 명확한 분리가 필요없다면 update 로 갈음하여도 괜찮다.
JOB은 별도 레코드 조회없이 메모리 단, 또는 다른 어플리케이션/서비스 로 전송하여야한다.
T1 - JOB - T2 을 연결하는 지점은 같은 쓰레드 여도 되며, 같은 어플리케이션이여도 되며, 디른 어플리케이션이여도 된다.
그 이후 작업이 완료되면, T2는 연결하는 지점으로부터 데이터를 전달받아 FIN 처리를 한다.
- 외부 작업인 JOB이 정말 한번만 호출되는지 어떻게 확인 할 수 있을까? 작업자의 실수로 중복이 전송되었는지 확인할 수 있을까?
- 중복 작업을 막기위해서 JOB은 어떤 부분을 지원해야할까?
- 외부 작업인 JOB이 실패하였으면 성공하였지만 T2가 전달받지 못 한 경우는 어떻게 처리해야할까?
그것은 재밌는 질문으로 남겨둔다.
Further readnings..
- 멱등성이 뭔가요? https://docs.tosspayments.com/blog/what-is-idempotency
멱등성이 뭔가요? | 토스페이먼츠 개발자센터
생소한 표현이지만 알고 보면 쉬워요. 멱등성에 대해 이해하고 API를 멱등하게 제공하기 위한 방법도 함께 알아봐요.
docs.tosspayments.com
- Pattern: Transactional outbox https://microservices.io/patterns/data/transactional-outbox.html
- Total
- Today
- Yesterday
- grafana cloud
- rosen
- paul wilton
- arena simulation
- 아레나 시뮬레이션
- 아레나
- 항해99
- 대규모 시스템 설계 기초
- 가상 면접 사례로 배우는 대규모 시스템 설계 기초
- Propositional and Predicate Logic
- beginning javascript
- Discrete Mathematics
- 아레나시뮬레이션
- 데이터 중심 애플리케이션 설계
- 명제논리
- 이산수학
- 백준
- 트랜잭션
- 최단경로 알고리즘
- 그라파나
- javascript
- 동시성
- 자바스크립트
- Simulation
- 자바스크립트 예제
- 엄청난 인내심과 시뮬레이션을 위한 아레나 툴
- 이산 수학
- 시뮬레이션
- Arena
- 로젠
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 31 |