0%

Redis Lua로 동시성 문제 해결하기

image
2편의 내용은 이론적으로 Redis의 동시성 문제를 해결하는 방법을 다루었습니다. 이번에는 Lua script가 실제로 잘 작동하는지 확인해보겠습니다.

INCR을 사용하면 더 간단하게 구현할 수 있습니다. 하지만 동시성 문제를 발생시키기 위해서 GET SET을 사용하겠습니다.

요구사항

간략한 요구사항은 다음과 같습니다.

  • 동일한 주문 번호가 없음
  • 주문 번호는 주문 마다 1씩 증가

예시 코드

불필요한 부분은 생략하고 중요한 부분만 살펴보겠습니다. order엔티티 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
@Table(name = "orders")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long id;

// 주문 아이디 (UUID)
@Column(name = "order_id")
private String orderId;

// 주문번호 (시퀀스)
@Column(name = "order_number")
private Long orderNumber;

public Order(Long orderNumber) {
this.orderId = UUID.randomUUID().toString();
this.orderNumber = orderNumber;
}
}

기본 redis

1
2
3
4
5
6
7
8
9
public Long getSet() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
Long orderNumber = Long.parseLong(valueOperations.get("orderNumber"));
Long newOrderNumber = orderNumber + 1;
valueOperations.set("orderNumber", newOrderNumber.toString());

Order order = new Order(newOrderNumber);
return orderRepository.save(order).getOrderNumber();
}

orderNumber를 가져와서 1을 증가시키고 다시 저장하는 코드입니다.

테스트

  • 100개의 스레드가 동시에 getSet을 호출합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Test
    @DisplayName("레디스 GET SET 테스트")
    void redis_GET_SET_thread_test() throws InterruptedException {
    ConcurrentHashMap<Long, Long> orderNumbers = new ConcurrentHashMap<Long, Long>();
    ExecutorService executorService = Executors.newFixedThreadPool(100);
    CountDownLatch countDownLatch = new CountDownLatch(100);

    for (int i = 0; i < 100; i++) {
    executorService.execute(() -> {
    try {
    orderNumbers.put(raceService.getSet(), 0L);
    } finally {
    countDownLatch.countDown();
    }
    });
    }

    countDownLatch.await();
    assertEquals(100, orderNumbers.size());
    }

    image

여러 스레드일 때는 중복된 주문 번호가 발생하는 것을 확인할 수 있습니다.

  • 싱글 스레드 결과
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    @DisplayName("레디스 GET SET 단일 스레드 테스트")
    void redis_GET_SET_single_test(){
    ConcurrentHashMap<Long, Long> orderNumbers = new ConcurrentHashMap<Long, Long>();

    for (int i = 0; i < 100; i++) {
    orderNumbers.put(raceService.getSet(), 0L);
    }

    assertEquals(100, orderNumbers.size());
    }
    image

중복 주문 번호가 발생하지 않습니다.

Lua script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Long getSetWithLuaScript() {
String luaScript =
"local current = redis.call('get', KEYS[1]) " +
"if current == false then current = 0 end " +
"current = current + 1 " +
"redis.call('set', KEYS[1], current) " +
"return current";

// 스크립트 실행을 위한 DefaultRedisScript 객체 생성
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(Long.class); // 반환 타입 지정

// Lua 스크립트 실행
Long newOrderNumber = redisTemplate.execute(redisScript, Collections.singletonList("orderNumber"));

Order order = new Order(newOrderNumber);
return orderRepository.save(order).getOrderNumber();
}

테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
@DisplayName("레디스 Lua 스크립트 테스트")
void redis_lua_thread_test() throws InterruptedException {
ConcurrentHashMap<Long, Long> orderNumbers = new ConcurrentHashMap<Long, Long>();
ExecutorService executorService = Executors.newFixedThreadPool(100);
CountDownLatch countDownLatch = new CountDownLatch(100);

for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
try {
orderNumbers.put(raceService.getSetWithLuaScript(), 0L);
} finally {
countDownLatch.countDown();
}
});
}

countDownLatch.await();
assertEquals(100, orderNumbers.size());
}

image

루아 스크립트를 사용하면 멀티 스레드 환경에서도 중복 주문 번호가 발생하지 않습니다.

번외

  • INCR 명령어를 사용하면 동시성을 고려하지 않고 더 간단하게 구현할 수 있습니다.
    1
    2
    3
    4
    5
    6
       public Long incr() {
    ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
    Long orderNumber = valueOperations.increment("orderNumber", 1);
    Order order = new Order(orderNumber);
    return orderRepository.save(order).getOrderNumber();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
       @Test
    @DisplayName("레디스 INCR 테스트")
    void redis_INCR_test() throws InterruptedException {
    ConcurrentHashMap<Long, Long> orderNumbers = new ConcurrentHashMap<Long, Long>();
    ExecutorService executorService = Executors.newFixedThreadPool(100);
    CountDownLatch countDownLatch = new CountDownLatch(100);

    for (int i = 0; i < 100; i++) {
    executorService.execute(() -> {
    try {
    orderNumbers.put(raceService.incr(), 0L);
    } finally {
    countDownLatch.countDown();
    }
    });
    }

    countDownLatch.await();
    assertEquals(100, orderNumbers.size());
    }
    image