[Spring] 분산락에서 발생하는 레이스 컨디션을 해결..?.part1

2025. 4. 3. 23:24Backend/Spring

개요

프로젝트에서 동시성 제어를 mvp를 만들면서 생긴 레이스 컨디션에 대한 이야기를 정리를 해보려고 한다.

 


 

소스 코드

@Transactional
public void payment(/*Long userId, */Long bookId) {
	Book book;
	RLock lock = redissonClient.getFairLock("book:"+bookId);

	try {
		boolean acquired = lock.tryLock(10L, 1L, TimeUnit.SECONDS);
		if (!acquired) {
			throw new InterruptedException();
		}
		book = bookRepository.findById(bookId).orElseThrow(
			() -> new NotFoundException(ErrorMessage.NOT_FOUND_BOOK.getMessage())
		);
		// log.info("" +book.getCount());
		book.CountMinusOne();
		log.info("" +book.getCount());
		//오더 로직 들어갈 예정
	} catch (InterruptedException ex) {
		throw new InvalidRequestException(ErrorMessage.REDIS_ERROR.getMessage());
	} catch (Exception ex) {
		throw new InvalidRequestException(ErrorMessage.ERROR.getMessage());
	} finally {
		if( lock.isLocked() )
		lock.unlock();
	}
}

 

간단하게 만들어두고 동시성 문제를 해결을 하려고 진행을 하였다.

 

위의 내용은 공식문서을 참고를 해서 진행을 하였다.

 

레디스 공식 문서

 

redisson-examples/locks-synchronizers-examples/src/main/java/org/redisson/example/locks/FairLockExamples.java at master · redis

Redisson java examples. Contribute to redisson/redisson-examples development by creating an account on GitHub.

github.com

 

다음 내용중에 페어 락에 대한 예시가 존재한다.

 

package org.redisson.example.locks;

import java.util.ArrayList;
import java.util.List;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

public class FairLockExamples {

    public static void main(String[] args) throws InterruptedException {
        // connects to 127.0.0.1:6379 by default
        RedissonClient redisson = Redisson.create();
        
        RLock lock = redisson.getFairLock("test");

        int size = 10;
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < size; i++) {
            final int j = i;
            Thread t = new Thread() {
                public void run() {
                    lock.lock();
                    lock.unlock();
                };
            };
            
            threads.add(t);
        }
        
        for (Thread thread : threads) {
            thread.start();
            thread.join(5);
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
    }
    
}

 

다음 내용을 참고를 하길 바란다.

 


테스트 코드

@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {

	@Mock
	private BookRepository bookRepository;

	@InjectMocks
	private PaymentService paymentService;

	@Mock
	private RedissonClient redissonClient;

	@Mock
	private RLock rlock;

	@Test
	@DisplayName("단일 호출 테스트")
	void paymentTest() throws InterruptedException {
		//given
		Book book = new Book();
		ReflectionTestUtils.setField(book, "bookId", 1L);
		ReflectionTestUtils.setField(book, "count", 1000L);

		given(bookRepository.findById(anyLong())).willReturn(Optional.of(book));
		given(redissonClient.getFairLock("book:"+book.getBookId())).willReturn(rlock);
		given(rlock.tryLock(10L, 1L, TimeUnit.SECONDS)).willReturn(true);


		// when
		paymentService.payment(1L);

		// then
		verify(bookRepository).findById(1L);
		assertEquals(999, book.getCount());
	}


	@Test
	@DisplayName("멀티 쓰래드 테스트")
	void paymentTest_thread() throws InterruptedException {
		//given
		Book book = new Book();
		ReflectionTestUtils.setField(book, "bookId", 1L);
		ReflectionTestUtils.setField(book, "count", 1000L);

		int threadCount = 1000;
		ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
		CountDownLatch latch = new CountDownLatch(threadCount);

		given(redissonClient.getFairLock("book:"+book.getBookId())).willReturn(rlock);
		given(bookRepository.findById(anyLong())).willReturn(Optional.of(book));
		given(rlock.tryLock(10L, 1L, TimeUnit.SECONDS)).willReturn(true);


		//when
		for (int i = 0; i < threadCount; i++) {
			executorService.execute(() -> {
				try {
					paymentService.payment(1L);
				} catch (Exception e) {
					System.out.println(e.getMessage());
				} finally {
					latch.countDown();
				}
			});
		}

		latch.await();
		executorService.shutdown();

		assertEquals(0, book.getCount());
	}
}

 

이렇게 작성을 진행을 하였다.

 

단일 테스트는 동시성 제어에 대한 문제가 안생기니 넘기도록 하고, 멀티 쓰레드 테스트에 대해서 살펴 보도록하겠다.

 

멀티 쓰레드 테스트

	@DisplayName("멀티 쓰래드 테스트")
	void paymentTest_thread() throws InterruptedException {
		//given
		Book book = new Book();
		ReflectionTestUtils.setField(book, "bookId", 1L);
		ReflectionTestUtils.setField(book, "count", 1000L);

		int threadCount = 1000;
		ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
		CountDownLatch latch = new CountDownLatch(threadCount);

		given(redissonClient.getFairLock("book:"+book.getBookId())).willReturn(rlock);
		given(bookRepository.findById(anyLong())).willReturn(Optional.of(book));
		given(rlock.tryLock(10L, 1L, TimeUnit.SECONDS)).willReturn(true);


		//when
		for (int i = 0; i < threadCount; i++) {
			executorService.execute(() -> {
				try {
					paymentService.payment(1L);
				} catch (Exception e) {
					System.out.println(e.getMessage());
				} finally {
					latch.countDown();
				}
			});
		}

		latch.await();
		executorService.shutdown();

		assertEquals(0, book.getCount());
	}

 

테스트 결과

 

문제가 없이 통과를 한다...?

 

테스트를 돌린 환경은 리눅스 환경인데, 원래 메인 환경인 리눅스에서는 동시성 이슈를 통과를 하지 못하였다.

 

1000개를 하나씩 빼는 로직을 동시성 제어를 해서 여러개의 멀티쓰레드가 총 1000번을 돌리는데

리눅스 환경에서는 3~16개 정도가 항상 남았다.

 

근대 맥 환경에서는 돌아가는 것이 왜 그런 것인지 모르겠다.

 

같은 리눅스 베이스여서 비슷할 탠데 이유를 잘 모르겠다.

 

결과

다음에 리눅스와 맥에서 왜 결과 값이 다른지를 자세하게 찾아봐야겠다.

 

이유는 서버에서는 보통 맥 서버를 돌리는 것이 아닌 리눅스 서버 ubuntu나 arch를 사용할 것이기 때문이다.