본문 바로가기
📕 Spring Framework/Spring Project

[Redisson]을 이용한 분산 Lock 구현 & 동시성 문제 해결

by GroovyArea 2022. 9. 27.

프로젝트의 Payment를 개발하면서 가장 기본 중에 기본이 되는 문제를 직면했었다.

그것은 바로 동시성 문제!

스프링부트의 내장 서버는 기본적으로 톰캣, 언더토우 등등의 WAS로 돌아가는데 이 WAS는 멀티스레드 기반으로 동작한다.

A라는 상품 (재고 3개) 을 [가]군이 2개 구매하려 한다. 동시에 [나]군이 2개 구매하려 한다.
미세하게 나마 0.00001초의 차이가 있을 수 있다.

결국 각각의 스레드가 같은 상품의 재고를 조회한다. 원래대로라면 한 명은 못 사야 정상이다.

위 문제를 해결하기 위한 방법이 뭐가 있을까?

1. Synchronized

자바로 해결하는 방법이다.
Thread-Safe 하기 때문에 매우 좋아보이나, 서버가 증설될 경우 의미가 없어진다.

2. Database Lock

Database에 Lock을 걸 수 있는 방법이다.
테이블에 Lock을 걸거나 Mysql의 Innodb엔진은 레코드 Lock을 제공하므로 이를 통해 동시성 문제를 해결할 수 있다.
하지만 DB에 직접적인 Lock을 거는 것은 결코 좋은 방법은 아니다. 교착 상태(deadlock)에 빠질 수도 있고,
무엇보다 성능적인 부분에서 속도가 느려질 수 있다는 문제점이 있다.

3. Redisson을 이용한 분산 Lock

Redis를 이용해서 Lock을 걸 수 있는 방법이 있다.
서버가 여러 대일 경우에도 문제 없이 분산 Lock을 걸어 데이터의 정합성을 보장할 수 있다.
자바에서 제공하는 Redisson 클라이언트를 이용해 분산 Lock을 획득해 분산 환경에서도 동시성 문제를 해결할 수 있다.

4. 구현 과정

 

Build.gradle 의존성 추가

// Redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.17.6'

 

Redisson Configuration

  • Redisson Client의 설정을 추가해 Bean으로 등록한다.
  • 앞선 bean들은 기존에 사용하던 Lettuce 라이브러리가 있기 때문이고, 추가하고자 하는 Redisson client를 Bean으로 등록하자.
@Configuration
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.connectionTimeout}")
    private Long connectionTimeout;

    /**
     * redis와 connection을 생성해 주는 객체
     * @return LettuceConnectionFactory
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration =
                new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        redisStandaloneConfiguration.setPassword(password);

        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }

    /**
     * redis 서버와 통신 및 유저에게 redis 모듈 기능 제공하기 위해 추상화를 통해
     * 오퍼레이션 제공
     * @return redisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate =
                new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
        stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
        stringRedisTemplate.setValueSerializer(new StringRedisSerializer());
        return stringRedisTemplate;
    }

    @Bean
    public RedissonClient redissonClient() {
        String address = "redis://" + host + ":" + port;

        Config config = new Config();
        config.useSingleServer()
                .setAddress(address)
                .setPassword(password)
                .setConnectTimeout(connectionTimeout.intValue());

        return Redisson.create(config);
    }
}

 

Lock을 적용하는 방법을 직접 코드에 적용 시키면 필요 이상의 비즈니스가 하나의 메서드에 들어갈 것 같다.
그래서 AOP를 이용해서 분산 Lock을 필요로 하는 곳에 적용 시키기 위해 애노테이션을 선언해 적용했다.

애노테이션 정의

  • Lock 유지 시간과 대기 시간을 프로퍼티로 정의
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLocked {

    /**
     * redisson lock 유지 시간 <br>
     * 단위 : milliseconds
     */
    long leaseTime() default 1000;

    /**
     * redisson lock 획득 대기 시간 <br>
     * 단위 : milliseconds
     */
    long waitTime() default 3000;
}

 

AOP 구현

  • Lock의 key는 필요에 따라 작명을 하면 될 것이다.
  • tryLock을 통해 lock의 획득 여부 파악과 획득을 진행하고, @Around로 정의해 중간에 핵심관심사를 실행한다.
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RedisLockAspect {

    private final RedissonClient redissonClient;

    private static final String LOCK_SUFFIX = ":lock";

    @Around("@annotation(com.daniel.mychickenbreastshop.global.aspect.annotation.RedisLocked)")
    public Object executeWithLock(ProceedingJoinPoint joinPoint) {
        String key = getLockableKey(joinPoint);
        return execute(key, joinPoint);
    }


    private String getLockableKey(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String methodName = method.getName();
        String className = method.getDeclaringClass().toString();
        return methodName + className + LOCK_SUFFIX;
    }

    private Object execute(String key, ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        long leaseTime = method.getAnnotation(RedisLocked.class).leaseTime();
        long waitTime = method.getAnnotation(RedisLocked.class).waitTime();

        RLock lock = redissonClient.getLock(key);

        Object result;

        try {
            boolean tryLock = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);

            if (!tryLock) {
                log.error("Lock 획득에 실패했습니다.");
            }

            result = joinPoint.proceed();
        } catch (Throwable e) {
            throw new InternalErrorException(e);
        } finally {
            unlock(lock);
            log.info("Redis unlocked!");
        }
        return result;
    }

    private void unlock(RLock lock) {
        if (lock != null && lock.isLocked()) {
            lock.unlock();
        }
    }
}

 

애노테이션 활용

  • 애노테이션을 활용해 분산 락이 필요한 메서드에 달아주면 된다.
  • 트랜잭션 처리가 필요한 곳을 AOP는 프록시 기반으로 동작하기 때문에 트랜잭션 전파를 따로 적용했다.
  • 이렇게 하지 않을 경우 직접 커밋과 롤백을 해줘야 한다.
@Override
@RedisLocked(leaseTime = 3000)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public PaymentResult cancelPayment(PayCancelRequestDto payCancelRequestDto, String loginId) {

 

이렇게 간단하게 Redisson을 이용하여 분산 Lock을 구현해봤다.
좀 더 나은 성능과 나은 방법을 찾는 것은 참 쉽지 않은 것 같다. 또, 최선의 방법을 계속 찾느라 시도 조차 안 하는 것이 아니라 일단 단계에 맞는 시도를 밟아나가며 필요에 의해 최선의 방법을 적용 시키는 과정이 필요할 것 같다고 생각한다.

 

트랜잭션 관련 예외 문제 해결 처리 포스팅!

https://sweeeetgoguma.tistory.com/entry/Redisson-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%AC%B8%EC%A0%9C-%EB%B0%9C%EC%83%9D-%EB%B0%8F-%ED%95%B4%EA%B2%B0

 

[Redisson] 트랜잭션 문제 발생 및 해결

지난 포스트 [Redisson]을 이용한 분산 Lock 구현 & 동시성 문제 해결 내 프로젝트의 Payment를 개발하면서 가장 기본 중에 기본이 되는 문제를 직면했었다. 그것은 바로 동시성 문제! 스프링부트의 내

sweeeetgoguma.tistory.com


참고 자료
https://it-hhhj2.tistory.com/102

 

redis 설치 및 redisson을 이용한 분산락 구현

설치 redis 설치 위 블로그 따라 순조롭게 설치 후 확인 완료 Redis와 분산락 분산락(Distributed Lock) 여러 독립된 프로세스에서 하나의 공유 자원에 접근할 때, 데이터에 결함이 발생하지 않도록 원자

it-hhhj2.tistory.com

https://devroach.tistory.com/82

 

Redisson 으로 분산 Lock 구현하기

회사 프로젝트를 진행하던 동시성 문제를 해결하기 위해 분산락이 필요할 것 같다는 판단이 들었습니다. 분산락이란 무엇일까요? 아주 가볍게 설명하자면 예를 들어, 가게는 하나의 주문만 받

devroach.tistory.com

 

반응형