분산락과 Transaction ACID 유지

정명진·2023년 8월 10일
0
post-custom-banner

분산락 적용을 하면서 데이터 정합성이 깨지는 문제가 발생했다.

이로인해 Transaction의 ACID 원칙도 위반하게 되었다.

원인을 파악해보니 다음과 같은 시나리오가 발생하여 문제가 생긴것이다.

A, B 2개의 트랜잭션이 존재한다.

lock A -> A 트랜잭션에서 좌석 감소 -> unlock -> commit
lock B -> 대기 -> lock 획득 -> B 트랜잭션에서 좌석 감소 -> unlock -> commit

즉 2개 좌석이 감소해야 정상이나 A 트랜잭션이 커밋전 unlock을 하여 B가 lock을 획득하고 commit 전 좌석에서 감소하여 결국 1개의 좌석이 감소되는것이다.

해당 문제를 해결하려면 commit 이후 unlock을 하게 만들면 된다.

Annotation으로 관리하기 위해 우선 Annotation을 만들어 줍니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key();
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    long waitTime() default 10;
    long leaseTime() default 3;
}

그리고 트랜잭션 관리를 위한 AOP Transaction 을 만듭니다.

@Component
public class DistributedTransaction {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

그리고 해당 Annotation에 적용할 공통 분산 메모리 관리 AOP를 작성해주면 됩니다.

@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedLockAop {
    private static final String HAZELCAST_KEY = "hazelcast-";
    private final HazelcastInstance hazelcastInstance;
    private final DistributedTransaction distributedTransaction;

    @Around("@annotation(com.lotte.health.core.aop.DistributedLock)")
    public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);

        String key = HAZELCAST_KEY + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), distributedLock.key());

        var lock = hazelcastInstance.getCPSubsystem().getLock(key);
        if (lock.tryLock(distributedLock.waitTime(), distributedLock.timeUnit())) {
            try {
                log.info("get lock {}", key);
                return distributedTransaction.proceed(joinPoint);
            } finally {
                lock.unlock();
            }
        } else {
            throw new CustomException(CustomErrorCode.LOCK_TIMEOUT);
        }
    }
    
    public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
        ExpressionParser expressionParser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        return expressionParser.parseExpression(key).getValue(context, Object.class);
    }
}

실제 서비스 코드에 적용하면 다음과 같습니다.

	@Override
    @DistributedLock(key = "#request.seat")
    public String reserveSeat(ReserveSeatRequest request) {
        String result = "좌석 예약에 성공하였습니다.";
        int reservationCount = reservationCountPort.getReservationCount(ReservationStatus.USE, WorkType.OFFICE);
        if (reservationCount == limit) {
            reservationSavePort.reserveHome(request.getUserId());
            result = "좌석이 모두 사용중입니다. 자동으로 재택근무로 변경됩니다.";
        } else if (reservationCount < limit) {
            reservationSavePort.reserve(request.getUserId(), request.getSeat());
        }
        return result;
    }

이제 해당 좌석에 대한 트랜잭션이 완료후 다른 트랜잭션이 시작하므로 동시성과 데이터 정합성을 동시에 처리가 가능합니다.

profile
개발자로 입사했지만 정체성을 잃어가는중... 다시 준비 시작이다..
post-custom-banner

0개의 댓글