Blog

[Spring][258] redisson 분산 락 구현

Category
Author
Tags
PinOnMain
1 more property
분산락 이란 동시성 문제를 해결하기 위한 방법 중 하나로 같은 자원에 접근할 시 락을 제공하여 접근이 완료되면 락을 해제하여 다음 순번에게 넘어가는 방식이다.
분산락을 사용하는 이유는 분산된 서버 및 분산된 DB환경에서도 동시성 이슈를 해결하기 위함이다.
분산락을 Redis로 구현하는 방법에는 lettuce, redisson 라이브러리가 있는데 lettuce방식은 스핀락(락을획득하지 못할 경우 계속 요청) 방식으로 redis에 부하가 갈 수 있어 redisson을 사용하여 구현해보았다.
RedissonConfig를 작성하여 Bean에 등록하고
@Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.port}") private int redisPort; private static final String REDISSON_HOST_PREFIX = "redis://"; @Bean public RedissonClient redissonClient() { RedissonClient redisson = null; Config config = new Config(); config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort); redisson = Redisson.create(config); return redisson; } }
Plain Text
복사
BookApplyDonationService에 아래와 같이 구현하였다.
public MessageDto createBookApplyDonationV4(BookApplyDonationRequestDto bookApplyDonationRequestDto) { RLock lock = redissonClient.getLock(String.valueOf(bookApplyDonationRequestDto.getBookId())); try { if (!lock.tryLock(3, 3, TimeUnit.SECONDS)) { log.info("락 획득 실패"); throw new IllegalArgumentException("락 획득 실패"); } log.info("락 획득 성공"); bookApplyDonationService2.createBookApplyDonation(bookApplyDonationRequestDto); } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } finally { log.info("finally문 실행"); if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); log.info("언락 실행"); } } return new MessageDto("책 나눔 신청이 완료되었습니다."); }
Plain Text
복사
public class BookApplyDonationService2 { private final BookRepository bookRepository; private final BookDonationEventRepository bookDonationEventRepository; private final BookApplyDonationRepository bookApplyDonationRepository; private final UserRepository userRepository; @Transactional public void createBookApplyDonation(BookApplyDonationRequestDto bookApplyDonationRequestDto) { Book book = bookRepository.findById(bookApplyDonationRequestDto.getBookId()) .orElseThrow(() -> new IllegalArgumentException("나눔 신청한 책이 존재하지 않습니다.")); if (book.getBookApplyDonation() != null) { throw new IllegalArgumentException("이미 누군가 먼저 신청했습니다."); } BookDonationEvent bookDonationEvent = bookDonationEventRepository.findFetchJoinById(bookApplyDonationRequestDto.getDonationId()) .orElseThrow(() -> new IllegalArgumentException("해당 이벤트가 존재하지 않습니다.")); if (LocalDateTime.now().isBefore(bookDonationEvent.getCreatedAt()) || LocalDateTime.now().isAfter(bookDonationEvent.getClosedAt())) { throw new IllegalArgumentException("책 나눔 이벤트 기간이 아닙니다."); } User user = userRepository.findFetchJoinById(SecurityUtil.getPrincipal().get().getUserId()).orElseThrow( () -> new IllegalArgumentException("해당 사용자는 도서관 사용자가 아닙니다.") ); BookApplyDonation bookApplyDonation = new BookApplyDonation(bookApplyDonationRequestDto); bookApplyDonationRepository.save(bookApplyDonation); bookApplyDonation.addBook(book); user.getBookApplyDonations().add(bookApplyDonation); bookDonationEvent.getBookApplyDonations().add(bookApplyDonation); book.changeStatus(BookStatusEnum.SOLD_OUT); } }
Plain Text
복사
잘 구현은 되나 bookApplyDonation2 를 추가로 생성하여 만드는 찝찝함이 있는데 이에 대한 내용은 별도 트러블슈팅 문서에 정리하겠다.