Skip to content

Commit

Permalink
Feat: Cache stampede 문제 Lock 으로 해결 (#193)
Browse files Browse the repository at this point in the history
* chore: redisson 설정 추가

* feat: redisson 분산락 기능 구현

* feat: 로그 및 에러 수정
  • Loading branch information
GGHDMS authored Oct 19, 2024
1 parent 19351d4 commit acb177d
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 13 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.redisson:redisson-spring-boot-starter:3.33.0'

//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/cotato/bookitlist/book/dto/BookApiDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,9 @@ public static BookApiDto of(
return new BookApiDto(title, author, publisher, pubDate, description, link, isbn13, price, cover);
}

public static BookApiDto of(BookDto dto) {
return new BookApiDto(dto.title(), dto.author(), dto.publisher(), dto.pubDate(), dto.description(), dto.link(), dto.isbn13(), dto.price(), dto.cover());
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cotato.bookitlist.book.redis;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class RedissonLockService {
private static final String LOCK_PREFIX = "LOCK";
private final RedissonClient redissonClient;

private static String getLockKey(String domain, String key) {
return LOCK_PREFIX + ":" + domain + ":" + key;
}

public boolean tryLock(String domain, Object key) throws InterruptedException {
RLock lock = redissonClient.getLock(getLockKey(domain, key.toString()));
log.debug("tryLock domain={} key={}", domain, key);
return lock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
}

public void unlock(String domain, Object key) {
RLock lock = redissonClient.getLock(getLockKey(domain, key.toString()));
log.debug("unlock domain={} key={}", domain, key);
lock.unlock();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void saveBookApiCache(BookApiDto bookApiDto) {
repository.save(new BookApiCache(bookApiDto.isbn13(), bookApiDto));
}

public Optional<BookApiCache> findBookApiCacheByIsbn13(String isbn13){
public Optional<BookApiCache> findBookApiCacheByIsbn13(String isbn13) {
return repository.findById(isbn13);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import cotato.bookitlist.book.dto.response.BookApiListResponse;
import cotato.bookitlist.book.dto.response.BookApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -12,6 +13,7 @@
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class BookApiComponent {
Expand Down Expand Up @@ -61,12 +63,13 @@ public BookApiDto findByIsbn13(String isbn13) {
JSONObject json = new JSONObject(aladinComponent.findByIsbn13(aladinKey, isbn13, "ISBN13", "JS", 20131101));

if (json.has("errorMessage")) {
log.info("존재하지 않는 isbn13 입니다. isbn13: {}", isbn13);
throw new IllegalArgumentException("존재하지 않는 isbn13 입니다.");
}

JSONObject item = json.getJSONArray("item").getJSONObject(0);

BookApiDto bookApiDto = BookApiDto.of(
return BookApiDto.of(
item.optString("title", ""),
item.optString("author", ""),
item.optString("publisher", ""),
Expand All @@ -77,9 +80,5 @@ public BookApiDto findByIsbn13(String isbn13) {
item.optIntegerObject("priceSales", null),
item.optString("cover", "")
);

bookApiCacheService.saveBookApiCache(bookApiDto);

return bookApiDto;
}
}
53 changes: 46 additions & 7 deletions src/main/java/cotato/bookitlist/book/service/BookService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import cotato.bookitlist.book.dto.response.BookRecommendListResponse;
import cotato.bookitlist.book.dto.response.BookRecommendResponse;
import cotato.bookitlist.book.redis.BookApiCache;
import cotato.bookitlist.book.redis.RedissonLockService;
import cotato.bookitlist.book.repository.BookRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Page;
Expand All @@ -19,9 +21,10 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class BookService {

Expand All @@ -31,6 +34,7 @@ public class BookService {
private final BookApiComponent bookApiComponent;
private final BookApiCacheService bookApiCacheService;
private final BookRepository bookRepository;
private final RedissonLockService redissonLockService;

public BookApiListResponse searchExternal(String keyword, int start, int maxResults) {
return bookApiComponent.findListByKeyWordAndApi(keyword, start, maxResults);
Expand All @@ -41,12 +45,34 @@ public BookApiDto getExternal(String isbn13) {
}

public BookDto getBookByIsbn13(String isbn13) {
return bookRepository.findByIsbn13(isbn13).map(BookDto::from)
.orElseGet(() -> bookApiCacheService.findBookApiCacheByIsbn13(isbn13)
.map(BookApiCache::getBookApiDto)
.map(BookDto::from)
.orElseGet(() -> BookDto.from(getExternal(isbn13)))
);
Optional<BookDto> book = getFromCache(isbn13);

if (book.isPresent()) {
return book.get();
}

try {
if (redissonLockService.tryLock("book", isbn13)) {
try {
book = getFromCache(isbn13);
if (book.isPresent()) {
return book.get();
}

BookDto bookDto = getFromDbOrExternal(isbn13);
bookApiCacheService.saveBookApiCache(BookApiDto.of(bookDto)); // 캐시에 저장
return bookDto;
} finally {
redissonLockService.unlock("book", isbn13);
}
} else {
log.info("Lock not acquired, skipping cache update for isbn13={}", isbn13);
throw new RuntimeException("Please try again later.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread was interrupted while trying to lock", e);
}
}

@Deprecated
Expand Down Expand Up @@ -83,4 +109,17 @@ public BookRecommendListResponse recommendBook() {

return new BookRecommendListResponse(bookRecommendList);
}

private Optional<BookDto> getFromCache(String isbn13) {
return bookApiCacheService.findBookApiCacheByIsbn13(isbn13)
.map(BookApiCache::getBookApiDto)
.map(BookDto::from);
}

private BookDto getFromDbOrExternal(String isbn13) {
return bookRepository.findByIsbn13(isbn13)
.map(BookDto::from)
.orElseGet(() -> BookDto.from(getExternal(isbn13)));
}

}

0 comments on commit acb177d

Please sign in to comment.