# ๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (5)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (1) - synchronized (opens new window)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (2) - Pessimistic Lock (opens new window)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (3) - Optimistic Lock (opens new window)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (4) - Named Lock (opens new window)
๐ ๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (5) - Spinlock (opens new window)
์์ฑ์ ์ฌ์ฉ๋ ์์ ์ฝ๋๋ concurrency-spinlock (opens new window)์์ ํ์ธํด๋ณผ ์ ์๋ค.
# ์คํ๋ฝ
์ด๋ฒ ์๊ฐ์๋ Redis
๋ฅผ ํ์ฉํ ์คํ๋ฝ ๋ฐฉ์์ผ๋ก ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํด๋ณด๋ ค ํ๋ค. ์คํ๋ฝ์ Redis
์ SETNX
๋ช
๋ น์ด๋ฅผ ํตํด ๋ถ์ฐ๋ฝ
์ ๊ตฌํํ ์ ์๋ค.
SETNX
๋ SET
if N
ot eX
ists์ ์ค๋ง์ด๋ค. ํค๊ฐ ์๋ ๋ฌธ์์ด ๊ฐ์ ์ ์งํ๋๋ก ํค๋ฅผ ์ค์ ํ๋ค. ์ด ๊ฒฝ์ฐ์๋ SET
๊ณผ ๋์ผํ๋ค. ์ฐจ์ด์ ์ ํค๊ฐ ์ด๋ฏธ ๊ฐ์ ๋ณด์ ํ๊ณ ์๋ ๊ฒฝ์ฐ ์์
์ด
์ํ๋์ง ์๋๋ค. ์ฆ ์ด๊ฒ์ ํตํด ๋ถ์ฐ๋ฝ์ ๊ตฌํํ ์ ์๋ค.
# redis-cli
127.0.0.1:6379> SETNX 1 lock
(integer) 1 // ๋ฝ ํ๋
127.0.0.1:6379> SETNX 1 lock
(integer) 0 // ๋ฝ ํ๋ ์คํจ
SET
์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐ์ดํฐ ์ ๋ ฅ ํํ๋ก ํค์ ๊ฐ์ ์ ๋ ฅํ๋ค. ๋ง์ฝ ํค๊ฐ ์ด๋ฏธ ์ง์ ๋์ด ์๋ค๋ฉด ๋ฎ์ด์์ ์ง๋ค.
๋ ๋์ค๋ฅผ ํ์ฉ ํ์ ๋์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ ๋์คํฌ์ ์ ๊ทผํ๋ ๊ฒ์ด ์๋๋ผ ๋ฉ๋ชจ๋ฆฌ์ ์ ๊ทผํ๋ ๊ฒ ์ด๊ธฐ ๋๋ฌธ์ ๋ ๋น ๋ฅด๊ฒ ๋ฝ์ ํ๋ํ๊ณ ํด์ ํ ์ ์๋ค.
# Redis ์์กด์ฑ
๋ค๋ฅธ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์์ ๋ ๋์ค ํ๋กํ ์ฝ ์ฌ์ฉ์ ์ํด์๋ ๋ ๋์ค ํด๋ผ์ด์ธํธ๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค. Java์๋ ๋ํ์ ์ผ๋ก Jedis
, lettuce
, Redisson
๋ฑ์ด ์กด์ฌํ๋ค.
๋จผ์ ์คํ๋ง์์ ๋ ๋์ค ์ฌ์ฉ์ ์ํด์๋ ์์กด์ฑ์ ์ถ๊ฐํด์ผ ํ๋ค.
dependencies {
// ...
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// ...
}
๋๊ท๋ชจ Spring Data ์ ํ๊ตฐ์ ์ผ๋ถ์ธ Spring Data Redis
๋ ์คํ๋ง ์ ํ๋ฆฌ์ผ์ด์
์์ ์ฝ๊ฒ ๊ตฌ์ฑํ๊ณ ๋ ๋์ค์ ์ ๊ทผํ ์ ์๋๋ก ์ง์ํ๋ค. ์ด๊ฒ์ ์ ์ฅ์์ ์ํ ์์ฉํ๊ธฐ ์ํด ๋ฎ์ ์์ค๊ณผ ๋์
์์ค์ ์ถ์ํ๋ฅผ ๋ชจ๋ ์ ๊ณตํ๊ณ ์๋ค. ์ ์์กด์ฑ์ ์ถ๊ฐํ๋ฉด ๋ ๋์ค ํด๋ผ์ด์ธํธ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก lettuce
์ ๋ํ ์์กด์ฑ์ด ์ถ๊ฐ๋๋ค.
Lettuce
๋ ๋๊ธฐ์, ๋น๋๊ธฐ์ ๋ฐ reactive ์ฌ์ฉ์ ์ํ ํ์ฅ ๊ฐ๋ฅํ ์ค๋ ๋ ์์ ํ ๋ ๋์ค ํด๋ผ์ด์ธํธ์ด๋ค.
# RedisTemplate
๋ ๋์ค ๋ฐ์ดํฐ ์ ๊ทผ ์ฝ๋๋ฅผ ๋จ์ํํ๋๋ก ๋์์ฃผ๋ ํด๋์ค์ด๋ค. ๋ ๋์ค ์ ์ฅ์์์ ์ง์ ๋ ๊ฐ์ฒด์ ๊ธฐ๋ณธ ์ด์ง ๋ฐ์ดํฐ ๊ฐ์ ์๋ ์ง๋ ฌํ/์ญ์ง๋ ฌํ๋ฅผ ์ํํ๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก JdkSerializationRedisSerializer
์ ํตํด ๊ฐ์ฒด์ Java ์ง๋ ฌํ๋ฅผ ์ฌ์ฉํ๋ค. ๋ฌธ์์ด ์ง์ค ์์
์ ๊ฒฝ์ฐ ์ ์ฉ StringRedisTemplate
๋ฅผ ๊ณ ๋ คํ ์ ์๋ค.
RedisTemplate
์ ๋ ๋์ค๊ฐ ์ ๊ณตํ๋ ๋ค์ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ง์ํ๊ธฐ ์ํด opsForXXX()
์ ๊ฐ์ ํํ์ ๋ฉ์๋๊ฐ ์กด์ฌํ๋ค.
์ฌ์ฉํ๊ณ ์ ํ๋ redis command
์ ๋์ํ๋ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด operation
๊ฐ์ฒด๊ฐ ๋ฐํ๋๋ค.
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
opsForValue()
:ValueOperations
opsForHash()
:HashOperations
opsForList()
:ListOperations
opsForSet()
:SetOperations
opsForZSet()
:ZSetOperations
opsForStream()
:StreamOperations
opsForGeo()
:GeoOperations
opsForHyperLogLog()
:HyperLogLogOperations
opsForCluster()
:ClusterOperations
๋ถ์ฐ๋ฝ ๊ตฌํ์ ์ํด์๋ ๊ฐ๋จํ ๋ฌธ์์ด
์ ํ์ฉํ ์ ์์ ๊ฒ์ด๋ค. MySQL์ ๋ค์๋ ๋ฝ๊ณผ ๋์ผํ๊ฒ ํน์ ๋ฌธ์์ด์ SETNX
๋ฅผ ํตํด ๋ฝ์ ๊ตฌํํ ์ ์๋ค.
์๋๋ ์ค์ ๋ฝ์ ํ๋ํ๊ณ ํด์ ํ๊ธฐ ์ํ LockRepository
์ด๋ค.
@Component
public class RedisLockRepository {
private final StringRedisTemplate redisTemplate;
public RedisLockRepository(final StringRedisTemplate stringRedisTemplate) {
this.redisTemplate = stringRedisTemplate;
}
public Boolean lock(final Long key) {
return redisTemplate.opsForValue()
.setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
}
public Boolean unlock(final Long key) {
return redisTemplate.delete(generateKey(key));
}
private String generateKey(final Long key) {
return key.toString();
}
}
StringRedisTemplate
: RedisTemplate์ ๋ฌธ์์ด ์ค์ฌ์ ํด๋์ค์ด๋ค. ๋ ๋์ค์ ๋ํ ๋๋ถ๋ถ์ ์์ ์ ๋ฌธ์์ด ๊ธฐ๋ฐ์ด๊ธฐ ๋๋ฌธ์ ์ด ํด๋์ค๋ ํนํ ์ง๋ ฌํ ๋ณํ๊ธฐ ์ธก๋ฉด์์ ๋ณด๋ค ์ผ๋ฐ์ ์ธ ํ ํ๋ฆฟ์ ๊ตฌ์ฑ์ ์ต์ํํ๋ ์ ์ฉ ํด๋์ค๋ฅผ ์ ๊ณตํ๋ค.lock()
: ๋ฝ ํ๋์ ์ํ ๋ฉ์๋์ด๋ค. ๋ฝ ํ๋์ ์ฑ๊ณตํ๋ฉดtrue
, ์คํจํ๋ฉดfalse
๋ฅผ ๋ฐํํ๋ค.opsForValue()
: ๋จ์ ๊ฐ์ ๋ํด ์ํ๋ ์์ ์ ๋ฐํํ๋ค.setIfAbsent()
: ํค๊ฐ ์๋ ๊ฒฝ์ฐ ๋ฌธ์์ด value๋ฅผ ๋ณด์ ํ๋๋ก ํค๋ฅผ ์ค์ ํ๋ค.SETNX
๋ช ๋ น์ด๋ฅผ ์ํํ๋ ๊ฒ์ผ๋ก ํ๋จ๋๋ค. ํน๋ณํ ์กฐ์น๊ฐ ์๋ค๋ฉด ๋ ๋์ค์ ํค๋ ์ญ์ ๊ฐ ์๋ ๋ณด๊ด ์ฒ๋ฆฌ๋๋ค. ์ถ์ธกํ๊ธฐ๋ก๋ timeout ์๊ฐ์ ์ง์ ํ์ฌ ์๋์ ์ผ๋ก ํค๋ฅผ ์ ๊ฑฐํ์ฌ ๋ฝ์ด ์ค๋์๊ฐ ์กํ๋ ๊ฒ์ ๋ง๋ ๊ฒ์ผ๋ก ์ถ์ธก๋๋ค.unlock()
: ๋ฝ์ ํด์ ํ๋ค.
๋ฝ์ ํ๋ํ ๋ค ๋ก์ง์ ์ํํ๊ณ ๋ฝ์ ํด์ ํ๊ธฐ ์ํ Facade
๋ฅผ ์ถ๊ฐํ๋ค.
@Component
public class LockProductFacade {
private final RedisLockRepository redisLockRepository;
private final ProductService productService;
public LockProductFacade(final RedisLockRepository redisLockRepository,
final ProductService productService) {
this.redisLockRepository = redisLockRepository;
this.productService = productService;
}
public void purchase(final Long key, final Long quantity) {
try {
redisLockRepository.lock(key);
productService.purchase(key, quantity);
} finally {
redisLockRepository.unlock(key);
}
}
}
์ ์ด์ ํ ์คํธ๋ฅผ ์ํํด๋ณด์.
@SpringBootTest
@DisplayNameGeneration(ReplaceUnderscores.class)
class SpinlockProductFacadeTest {
private final ProductRepository productRepository;
private final SpinlockProductFacade spinlockProductFacade;
@Autowired
SpinlockProductFacadeTest(final ProductRepository productRepository,
final SpinlockProductFacade spinlockProductFacade) {
this.productRepository = productRepository;
this.spinlockProductFacade = spinlockProductFacade;
}
@Test
void ๋์์_100๊ฐ์_์ํ์_๊ตฌ๋งคํ๋ค() throws InterruptedException {
var product = productRepository.save(new Product("์นํจ", 100L));
var executorService = Executors.newFixedThreadPool(10);
var countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
executorService.submit(() -> process(product, countDownLatch));
}
countDownLatch.await();
var actual = productRepository.findById(product.getId()).orElseThrow();
assertThat(actual.getQuantity()).isEqualTo(0L);
}
@Test
void ์ํ์_์๋์_100๊ฐ_์ด์ง๋ง_๋์์_200๊ฐ์_์ํ์_๊ตฌ๋งคํ๋ค() throws InterruptedException {
var product = productRepository.save(new Product("์นํจ", 100L));
var executorService = Executors.newFixedThreadPool(10);
var countDownLatch = new CountDownLatch(200);
for (int i = 0; i < 200; i++) {
executorService.submit(() -> process(product, countDownLatch));
}
countDownLatch.await();
var actual = productRepository.findById(product.getId()).orElseThrow();
assertThat(actual.getQuantity()).isEqualTo(0L);
}
private void process(final Product product, final CountDownLatch countDownLatch) {
try {
spinlockProductFacade.purchase(product.getId(), 1L);
} catch (final InterruptedException e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
}
}
}
์์ฝ๊ฒ๋ ์คํจํ๋ค. ์ ๋ฐฉ์์ ํ์ฉํ๋ฉด ๋ฝ ์คํจ ์ ์ถ๊ฐ์ ์ธ ์ฌ์๋ ๋ก์ง์ ์์ฑํด์ผ ํ๋ค. ๋ณดํต์ ํน์ ์๊ฐ ์ดํ ์ฌ์์ฒญ์ ํ ์ ์๋ ์คํ๋ฝ ๋ฐฉ์
์ ํ์ฉํ ์ ์๋ค.
# ์คํ๋ฝ ์ ์ฉํ๊ธฐ
@Component
public class SpinlockProductFacade {
private final RedisLockRepository redisLockRepository;
private final ProductService productService;
public SpinlockProductFacade(final RedisLockRepository redisLockRepository,
final ProductService productService) {
this.redisLockRepository = redisLockRepository;
this.productService = productService;
}
public void purchase(final Long key, final Long quantity) throws InterruptedException {
while (!redisLockRepository.lock(key)) {
Thread.sleep(100);
}
try {
productService.purchase(key, quantity);
} finally {
redisLockRepository.unlock(key);
}
}
}
๋ก์ง์ ๊ฐ๋จํ๋ค. ์คํ๋ฝ์ ํตํด ๋ฝ์ด ํ๋๊ฐ๋ฅํ์ง ์ฃผ๊ธฐ์ ์ผ๋ก ํ์ธ ํ ๋ก์ง์ ์ํํ๋ค. ๋ก์ง์ด ์ ์์ ์ผ๋ก ์ํ๋๋ฉด ๋ฝ์ ํด์ ํด์ผ ํ๊ธฐ ๋๋ฌธ์ finally
๋ก ๋ฝ์ ํด์ ํ๋ค.
ํ ์คํธ๋ฅผ ์ํํ๋ฉด ์ ์์ ์ผ๋ก ํต๊ณผํ๋ ๊ฒ์ ์ ์ ์๋ค.
# ์ ๋ฆฌ
์ง๊ธ ๊น์ง ๋ ๋์ค ํด๋ผ์ด์ธํธ์ธ lettuce
๋ฅผ ํ์ฉํ์ฌ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ์๋ค. lettuce
๋ Spring Data Redis ์์กด์ฑ ์ถ๊ฐ ์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ฉ๋๊ธฐ ๋๋ฌธ์ ๊ฐํธํ๊ฒ ์ ์ฉ์ด ๊ฐ๋ฅํ์ง๋ง ๋ฝ ํ๋ ๋ฐ ๋ฐ๋ฉ ๋ก์ง์ ๊ฐ๋ฐ์๊ฐ ์ง์ ์์ฑํด์ผ ํ๋ค.
๋ํ ๋ฝ ํ๋ ์คํจ ์ ์ฌ์๋๋ฅผ ์ํ ๋ก์ง๊น์ง ๊ตฌํํด์ผ ํ๋ค. ๋ํ์ ์ผ๋ก๋ ์คํ๋ฝ์ ํ์ฉํ์ฌ ๊ตฌํํ ์ ์๋ค. ์คํ๋ฝ ๋ฐฉ์์ผ๋ก๋ ์ถฉ๋ถํ ๋์์ฑ ์ด์๊ฐ ํด๊ฒฐ๋์ง๋ง ๋ ๋์ค ์๋ฒ์ ์ง์์ ์ธ ์์ฒญ์ ๋ณด๋ด๊ธฐ ๋๋ฌธ์ ๋ถํ๊ฐ ์ฌ ์ ์๋ค.
๋ค์ ์๊ฐ์๋ pub/sub
๋ฐฉ์์ ๊ธฐ๋ฐ์ผ๋ก ๋ฝ์ ๊ตฌํํ๋ ๋ ๋์ค ํด๋ผ์ด์ธํธ Redisson
์ ํ์ฉํ์ฌ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํด๋ณด๋ ค ํ๋ค.
# References.
SETNX (opens new window)
setIfAbsent() (opens new window)
Redis client (opens new window)
Lettuce - Advanced Java Redis client (opens new window)
Spring Boot Data Redis ์ฌ์ฉํด๋ณด๊ธฐ (opens new window)