๐ค ์๋ก
์ด๋ฒ ๊ธ์์๋ Redis๋ฅผ ์ฌ์ฉํ์ฌ ๋ถ์ฐ๋ฝ์ ๊ตฌํํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
Java์ Redis ํด๋ผ์ด์ธํธ๋ก๋ Jedis, Lettuce, Redisson ๋ฑ์ด ์์ผ๋ฉฐ, ๊ฐ๊ฐ์ ํน์ง์ด ์๋ก ๋ค๋ฆ ๋๋ค.
์ด์ค์์ Jedis๋ ์ ์ธํ๊ณ Lettuce์ Redisson์ ์ฌ์ฉํด์ ๋ถ์ฐ๋ฝ์ ๊ตฌํํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
(Jedis์ Lettuce์ ๋น๊ต๋ ๋ค์ ๊ธ์ ์ฐธ๊ณ ํด์ฃผ์ธ์)
๐ค Redis ํ๊ฒฝ ์ค์
์๋ ๋ช ๋ น์ด๋ฅผ ํตํด redis๋ฅผ ๋ค์ด๋ฐ์์ค๋๋ค. (๋ฐ๋์ ๋์ปค๋ฅผ ์ฌ์ฉํ ํ์๋ ์์ต๋๋ค.)
docker pull redis
์ดํ ๋ ๋์ค๋ฅผ ์คํ์์ผ์ค๋๋ค.
docker run --name redis -d -p 6379:6379 redis
ํ๋ก์ ํธ์์๋ Redis ๊ด๋ จ ์์กด์ฑ์ ์ถ๊ฐํด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
์ดํ ํ๋ก์ ํธ๋ฅผ ์คํํ์ ๋ ๋ฌธ์ ๊ฐ ์์ผ๋ฉด ์ ์ค์ ๋ ๊ฒ์ ๋๋ค.
๐ค Lettuce ๋ฅผ ์ฌ์ฉํ ๋ถ์ฐ๋ฝ ๊ตฌํ
Lettuce๋ Netty๊ธฐ๋ฐ์ Redis Client์ด๋ฉฐ, ์์ฒญ์ ๋ ผ๋ธ๋กํน์ผ๋ก ์ฒ๋ฆฌํ์ฌ ๋์ ์ฑ๋ฅ์ ๊ฐ์ง๋๋ค.
spring-data-redis ์์กด์ฑ์ ์ถ๊ฐํ๋ค๋ฉด, ๊ธฐ๋ณธ์ ์ผ๋ก Lettuce ๊ธฐ๋ฐ์ Redis Client๊ฐ ์ ๊ณต๋ฉ๋๋ค.
Lettuce๋ฅผ ์ฌ์ฉํ๋ค๋ฉด SETNX ๋ช ๋ น์ด๋ฅผ ํตํด์ ๋ถ์ฐ๋ฝ์ ๊ตฌํํ ์ ์์ต๋๋ค.
๋ง์ฝ docker๋ก ์คํํ redis์ ์ง์ ์ ๊ทผํด์ ํ ์คํธํด๋ณด๊ณ ์ถ๋ค๋ฉด ๋ค์ ๋ช ๋ น์ด๋ฅผ ํตํด ์ ๊ทผํ ์ ์์ต๋๋ค.
docker exec -it redis redis-cli
์ด์ Redis๋ฅผ ํตํด ๋ฝ์ ์ป์ด์ค๊ณ ํด์ ํ๋ ๊ธฐ๋ฅ์ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
@RequiredArgsConstructor
@Repository
public class RedisLockRepository {
private final RedisTemplate<String, String> redisTemplate;
public Boolean lock(Object key) {
return redisTemplate
.opsForValue()
.setIfAbsent(key.toString(), "lock", Duration.ofMillis(3000));
}
public Boolean unlock(Object key) {
return redisTemplate.delete(key.toString());
}
}
lock() ๋ฉ์๋ ๋ด๋ถ์์ ์ฌ์ฉํ๋ setIfAbsent()๋ฅผ ํตํด SETNX๋ฅผ ์ฌ์ฉํฉ๋๋ค.
setIfAbsent() ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ ์์๋๋ก key, value, timeout ์ ๋ฐ์ต๋๋ค.
value๋ก๋ "lock"์ ์ค์ ํด ์ฃผ์์ง๋ง, ๋ค๋ฅธ ๊ฐ์ด์ด๋ ์๊ด์์ต๋๋ค.
์ด์ด์ ์ฌ๊ณ ๊ฐ์ ๋ก์ง ์ ํ๋ก ๋ฝ์ ํ๋ํ๊ณ ๋ฐํํ๋ ๋ก์ง์ด ํ์ํ๋ฏ๋ก, ๊ธฐ์กด๊ณผ ๊ฐ์ด Facade๋ฅผ ๋์ ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
@RequiredArgsConstructor
@Service
public class LettuceLockStockFacade {
private final RedisLockRepository redisLockRepository;
private final StockService stockService;
public void decrease(Long id) {
while (!redisLockRepository.lock(id)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
stockService.decrease(id);
} finally {
redisLockRepository.unlock(id);
}
}
}
์คํ๋ฝ ๋ฐฉ์์ผ๋ก ๊ตฌํํ์์ต๋๋ค.
์ด์ ํ ์คํธ๋ฅผ ์์ฑํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
@SpringBootTest
class LettuceLockStockFacadeTest {
@Autowired
private LettuceLockStockFacade lettuceLockStockFacade;
@Autowired
private StockRepository stockRepository;
private Long stockId;
@BeforeEach
void setUp() {
stockId = stockRepository.saveAndFlush(new Stock(1L, 100))
.getId();
}
@AfterEach
void tearDown() {
stockRepository.deleteAll();
}
@Test
void decrease_with_100_request_name_lock() throws InterruptedException {
// given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
lettuceLockStockFacade.decrease(stockId);
} catch (Exception e) {
System.out.println(e);
} finally {
latch.countDown();
}
});
}
latch.await();
// then
Stock stock = stockRepository.getById(stockId);
assertThat(stock.getQuantity()).isEqualTo(0);
}
}
์ฌ๊ณ ๊ฐ์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ด๋ฃจ์ด์ง๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๐ณ Lettuce ์ฌ์ฉ์ ์ฅ๋จ์
Lettuce๋ redis ์์กด์ฑ์ ์ถ๊ฐํ๋ ๊ฒฝ์ฐ ๊ธฐ๋ณธ Redis Client๋ก ์ ๊ณต๋๋ฏ๋ก, ๋ณ๋์ ์ค์ ์์ด ๊ฐ๋จํ ๊ตฌํํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
๊ทธ๋ฌ๋ ๊ตฌํ ๋ฐฉ์์์ ์คํ๋ฝ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ ๋์ค์ ๋ถํ๋ฅผ ์ค ์ ์๋ค๋ ๊ฒ์ด ๋จ์ ์ ๋๋ค.
๐ค Redisson ์ ์ฌ์ฉํ ๋ถ์ฐ๋ฝ ๊ตฌํ
Redis๋ Pub/Sub ๊ธฐ๋ฅ์ ์ ๊ณตํด์ฃผ๊ณ ์์ต๋๋ค.
์ด๋ฅผ ์ฌ์ฉํ๋ฉด ์์ฒ๋ผ ์คํ๋ฝ ๋ฐฉ์์ ์ฌ์ฉํ์ง ์๊ณ ๋ถ์ฐ๋ฝ์ ๊ตฌํํ ์ ์์ต๋๋ค.
์ง์ ๊ตฌํํ ์๋ ์์ง๋ง Redisson์์๋ Pub/Sub ๊ธฐ๋ฐ์ ๋ถ์ฐ๋ฝ์ ์ด๋ฏธ ๊ตฌํํ์ฌ ์ ๊ณตํด์ฃผ๊ณ ์์ผ๋ฏ๋ก, ์ด๋ฅผ ์ฌ์ฉํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๋จผ์ Redisson ์์กด์ฑ์ ์ถ๊ฐํด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
implementation 'org.redisson:redisson-spring-boot-starter:3.24.3'
Redisson์์๋ ๋ถ์ฐ๋ฝ์ ์ด๋ฏธ ๊ตฌํํ์ฌ ์ ๊ณตํด์ฃผ๊ณ ์์ผ๋ฏ๋ก, ๋ฐ๋ก ์ด๋ฅผ ์ฌ์ฉํ๋ Facade๋ฅผ ๋ง๋ค์ด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
@RequiredArgsConstructor
@Component
public class RedissonLockStockFacade {
private final RedissonClient redissonClient;
private final StockService stockService;
public void decrease(Long id) {
RLock lock = redissonClient.getLock(id.toString());
try {
boolean acquireLock = lock.tryLock(10, 1, TimeUnit.SECONDS);
if (!acquireLock) {
System.out.println("Lock ํ๋ ์คํจ");
return;
}
stockService.decrease(id);
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
}
}
ํ ์คํธ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
@SpringBootTest
class RedissonLockStockFacadeTest {
@Autowired
private RedissonLockStockFacade redissonLockStockFacade;
@Autowired
private StockRepository stockRepository;
private Long stockId;
@BeforeEach
void setUp() {
stockId = stockRepository.saveAndFlush(new Stock(1L, 100))
.getId();
}
@AfterEach
void tearDown() {
stockRepository.deleteAll();
}
@Test
void decrease_with_100_request_name_lock() throws InterruptedException {
// given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
redissonLockStockFacade.decrease(stockId);
} catch (Exception e) {
System.out.println(e);
} finally {
latch.countDown();
}
});
}
latch.await();
// then
Stock stock = stockRepository.getById(stockId);
assertThat(stock.getQuantity()).isEqualTo(0);
}
}
์ฌ๊ณ ๊ฐ์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ด๋ฃจ์ด์ง๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๐ณ Redisson ์ฌ์ฉ์ ์ฅ๋จ์
Redisson์์๋ ๋ถ์ฐ๋ฝ์ ์ด๋ฏธ ๊ตฌํํ์ฌ ์ ๊ณตํ๊ณ ์์ผ๋ฏ๋ก, ๋ณ๋์ ๊ตฌํ ๋ก์ง์ด ํ์ํ์ง ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
๋ํ Pub/Sub ๊ธฐ๋ฐ์ผ๋ก ๋์ํ์ฌ ๊ธฐ์กด Lettuce๋ฅผ ํตํ ์คํ๋ฝ ๊ธฐ๋ฐ์ ๋ถ์ฐ๋ฝ์ ๋นํด Redis์ ๋ถํ๋ฅผ ๋ ์ค๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
๊ทธ๋ฌ๋ ๋ณ๋์ ์์กด์ฑ์ ์ถ๊ฐํด์ผ ํ๋ค๋ ๊ฒ์ด ๋จ์ ์ด๋ผ๊ณ ๋ณผ ์ ์์ ๊ฒ ๊ฐ์ต๋๋ค.
๐ค ์ฐธ๊ณ - Redis Vs MySQL
Redis๋ ๊ธฐ๋ณธ์ ์ผ๋ก In-Memory DB์ด๋ฏ๋ก Disk ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ MySQL์ ๋นํด ์ฑ๋ฅ์ด ๋ฐ์ด๋ฉ๋๋ค.
๊ทธ๋ฌ๋ ์ฌ์ฉ์๋ค์ ์ด๋ฅผ ํฌ๊ฒ ์ฒด๊ฐํ์ง ๋ชปํ ๊ฐ๋ฅ์ฑ์ด ํฌ๋ฏ๋ก, ์ฑ๋ฅ๋ณด๋ค๋ ํ์ฌ ์ธํ๋ผ ์ํฉ์ ๊ณ ๋ คํ์ฌ ์ ํํ๋ ๊ฒ์ด ์ข์๋ณด์ ๋๋ค.
Redis๋ฅผ ์ฌ์ฉํ์ง ์๋ ํ๋ก์ ํธ์์ ๋ถ์ฐ๋ฝ ๊ตฌํ์ ์ํด Redis๋ฅผ ์ฌ์ฉํ๋ ค ํ๋ค๋ฉด, ๋ณ๋์ Redis ๊ตฌ์ถ์ ๋ํ ๋น์ฉ๊ณผ, ์ด๋ฅผ ํ์ตํ๋ ๋น์ฉ์ด ๋ฐ์ํฉ๋๋ค.
๊ทธ์ ๋นํด ์ด๋ฏธ ํ๋ก์ ํธ์์ ์ฌ์ฉํ๊ณ ์๋ MySQL ๋ฑ์ DB๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ๋ณ๋์ ์ธํ๋ผ ๊ตฌ์ถ ๋น์ฉ๊ณผ ํ์ต ๋น์ฉ ์์ด ์ ์ฉ์ด ๊ฐ๋ฅํฉ๋๋ค.
Redis์ MySQL ์ฌ์ด์ ์ฑ๋ฅ ์ฐจ์ด๊ฐ ์กฐ๊ธ ์๋ค๊ณ ๋ ํ์ง๋ง, ์ผ๋ฐ์ ์ผ๋ก๋ ํฌ๊ฒ ์ฒด๊ฐ๋์ง ์์ ๊ฐ๋ฅ์ฑ์ด ํฌ๋ฏ๋ก, ์ฌ๋ฌ ์กฐ๊ฑด๊ณผ ์ํฉ์ ๊ณ ๋ คํ์ฌ ์ ์ ํ ๊ธฐ์ ์ ๋์ ํ๋ ๊ฒ์ด ์ข์๋ณด์ ๋๋ค.
๐ Reference
https://redis.io/commands/setnx/
https://redis.io/docs/interact/pubsub/
https://jojoldu.tistory.com/418
https://redisson.org/feature-comparison-redisson-vs-lettuce.html
https://redisson.org/lettuce-replacement-why-redisson-is-the-best-lettuce-alternative.html