์ด์ ๊ธ์ ์ด์ด์ ๋ถ์ฐ๋ฝ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ํ ๊ธฐ์ค๊ณผ ๋ ๋์ค๋ฅผ ํตํ ๋ถ์ฐ ๋ฝ ์ ์ฉ๊ณผ์ ์ ๋ํด ๋ค๋ค๋ณผ ์์ ์ด๋ค.
โ๏ธ Lettuce / Redisson
์ด ๋ ๋ชจ๋ ๋ ๋์ค์ ๋ถ์ฐ ๋ฝ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ๋ถ์ฐ ๋ฝ์ ์ฒ์ ๊ตฌํํ์ ๋ ๋๋ ๋จ์ํ ๋ถ์ฐ ๋ฝ๋ง์ ์ฌ์ฉํ ๊ฒ์ด๋ผ๋ฉด Lettuce๋ฅผ ์ฌ์ฉํ์ฌ ๋ถ์ฐ๋ฝ์ ์ง์ ๊ตฌํํ๋ ๊ฒ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํฌ๊ธฐ๋ฅผ ๊ณ ๋ คํ์ ๋ ๋ ํจ์จ์ ์ผ ๊ฒ์ด๋ผ๊ณ ํ๋จํ๋ค. ํ์ง๋ง Lettuce์๋ ์คํ๋ฝ์ผ๋ก ์ธํ ๋ถํ, Atomicํ ํ์์์ ์ค์ ๋ถ๊ฐ๋ฅ ๋ฑ ์ฌ๋ฌ ๋จ์ ์ด ์กด์ฌํ๊ณ ์ด๋ฌํ ๋จ์ ๋ค์ ๋ณด์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Redisson์ ๋ํด ์์๋ดค๋ค.
1. Atomic TimeOut
Redisson์ tryLock ๋ฉ์๋์ ํ์์์์ ์ง์ ํ ์ ์๋ค. ์๋์ ๊ฐ์ด ์ฒซ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋ ๋ฝ ํ๋์ ๋๊ธฐํ ํ์์์์, ๋ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ์๋ ๋ฝ์ด ๋ง๋ฃ๋๋ ์๊ฐ์ ์ง์ ํ ์ ์๋ค.
์ฒซ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋งํผ์ ์๊ฐ์ด ์ง๋๋ฉด false๊ฐ ๋ฐํ๋๋ฉฐ ๋ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋งํผ์ ์๊ฐ์ด ์ง๋๋ฉด ๋ฝ์ด ๋ง๋ฃ๋์ด ์ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ง์ ๋ฝ์ ํด์ ํด์ฃผ์ง ์๋๋ผ๋ ๋ค๋ฅธ ์ค๋ ๋์์ ๋ฝ์ ํ๋ํ ์ ์๋ค.
์ด๋ก ์ธํด ๋ฝ์ด ํด์ ๋์ง ์์ ์ฑ๋ก ์ฑ์ด ์๊ธฐ์น ์๊ฒ ์ข ๋ฃ๋์ด ๋ฌดํ ๋ฃจํ์ ๋น ์ง ์ ์๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
// RedissonLock์ tryLock ๋ฉ์๋
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
2. PUBSUB
`setnx`, `setex`์ ๊ฐ์ ๋ช ๋ น์ด๋ฅผ ํตํด ์ง์์ ์ผ๋ก Redis์๊ฒ ๋ฝ์ด ํด์ ๋์๋์ง ์์ฒญ์ ๋ณด๋ด๋ ์คํ ๋ฝ ๋ฐฉ์์ ์ฌ์ฉํ๋ Lettuce์ ๋ฌ๋ฆฌ Redisson์ pub/sub ๊ธฐ๋ฅ์ ํตํด ๋ฝ ํด์ ์ ์๋ฆผ์ ํด๋ผ์ด์ธํธ์๊ฒ ๋ณด๋ด ๋ฝ ํ๋ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ค. ์ด๋ฅผ ํตํด Redis์ ๊ฐํด์ง๋ ๋ถํ์ํ ํธ๋ํฝ์ ์ค์ผ ์ ์๊ณ , ๋ฝ์ด ํด์ ๋๋ฉด ๋ฉ์์ง๋ฅผ ๋ฐ์ ํด๋ผ์ด์ธํธ๋ ๋ฝ ํญ๋์ ๋ค์ ์๋ํ๊ฒ ๋๋ค. ๋ํ ๋ฝ ํ๋์ tryLock ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ๊ฒฝํฉ์ด ์์ ๋ ๋น ๋ฅด๊ฒ ์ํ๋๋ฉฐ, ์คํจ ์ pub/sub์ ํตํด ๋๊ธฐํ๋ฉด์ ํ์์์์๊น์ง ์ฌ์๋๋ฅผ ๋ฐ๋ณตํ๋ค.
3. Lua Script
๋ฝ ๊ด๋ จ ์ฐ์ฐ์ ์์์ ์ด์ด์ผ ํ๊ธฐ ๋๋ฌธ์, ๋ช ๋ น์ด๊ฐ ์์ด์ง ์๋๋ก Lua ์คํฌ๋ฆฝํธ๋ฅผ ํ์ฉํ์ฌ Atomicํ ์ฐ์ฐ์ ๋ณด์ฅํ ์ ์๋ค. ๋ฝ์ ํ๋ ๋ฐ ํด์ ๊ณผ์ ์์ ์ฌ๋ฌ ์ฐ์ฐ์ ๋ฌถ์ด ํ ๋ฒ์ ์ฒ๋ฆฌํ๋ฉฐ, ํนํ pub/sub ์๋ฆผ๊ณผ ๋ฝ ํด์ ๋ฅผ ์์์ ์ผ๋ก ์ํํด์ผํ๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ๋ฝ ํ๋์ด ๊ฐ๋ฅํ๋ค๊ณ ์๋ต๋ฐ์ ๋ค์, ๋ฝ ํ๋์๋๋ฅผ ํ๋๋ฐ ๊ทธ ์ฌ์ด์ ์ด๋ฏธ ๋ค๋ฅธ ์ค๋ ๋์์ ๋ฝ์ ํ๋ํด๋ฒ๋ ค์ ๋ฝ ํ๋์ ์คํจํ๋ ๊ฒฝ์ฐ๊ฐ ์๊ธธ ์ ์๋ค. ๊ฒฐ๋ก ์ ์ผ๋ก, ์ด๋ฅผ ํตํด ์ฑ๋ฅ์ ์ ์งํ๋ฉด์๋ Redis์ ๊ฐํด์ง๋ ๋ถํ๋ฅผ ์ค์ด๊ณ , ์ ํํ ๋ฝ ๋์์ ๋ณด์ฅํ๋ค. Redisson์ ์ด๋ฌํ Lua Script๋ฅผ ์ ๊ทน์ ์ผ๋ก ํ์ฉํ๊ณ ์๋ค.
String luaScript =
"if redis.call('exists', KEYS[1]) == 0 then " +
"redis.call('set', KEYS[1], ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 1; " +
"else " +
"return 0; " +
"end;";
// Lua script ์คํ์ฝ๋
Object result = redisson.getScript().eval(RScript.Mode.READ_WRITE,
luaScript,
RScript.ReturnType.INTEGER,
Collections.singletonList(lockKey),
lockValue, lockTTL);
โ๏ธ ๋ถ์ฐ๋ฝ ๊ตฌํ
๋ถ์ฐ๋ฝ์ ํก๋จ ๊ด์ฌ์ฌ์ ํฌํจ๋๊ธฐ ๋๋ฌธ์ AOP๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํ๋ค๋ฉด ๋ฉ์ธ ๋ก์ง๊ณผ์ ๋ถ๋ฆฌํจ์ผ๋ก์จ ์ ์ง๋ณด์์ฑ์ ๋์ด๊ณ ์ง๊ด์ ์ธ ๊ตฌํ์ด ๊ฐ๋ฅํด์ง๋ค. ๋ํ ๋ถ์ฐ๋ฝ์ ํน์ ๋๋ฉ์ธ์ ํ์ ๋์ด ์ฌ์ฉ๋๋ ๊ฒ์ด ์๋๊ธฐ ๋๋ฌธ์ `execution`์ด๋ `args` ํฌ์ธํธ์ปท๋ณด๋ค๋ `annotation`์ ์ฌ์ฉํ๋ค. ์ด๋ฅผ ์ํด ๋จผ์ ์ ๋ ธํ ์ด์ ์ ๊ตฌํํ๋ค.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String key();
TimeUnit timeUnit() default TimeUnit.SECONDS;
long waitTime() default 5L;
long leaseTime() default 3L;
}
AOP๋ฅผ ๊ตฌํํ ๋์ ๋๋ถ๋ถ์ ๊ฒฝ์ฐ `@Around` ์ด๋๋ฐ์ด์ค๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ค. ๋ถ์ฐ ๋ฝ์ ๊ตฌํํ ๋๋ ๋ฉ์๋ ํธ์ถ ์ ํ์ ์คํ๋์ด์ผํ๊ธฐ ๋๋ฌธ์ ํด๋น ์ด๋๋ฐ์ด์ค๋ฅผ ์ฌ์ฉํ์ง๋ง, ์ด์ ๊ฐ์ ๊ฒฝ์ฐ๊ฐ ์๋๋ผ๋ฉด `@Before`, `@AfterReturning`๊ณผ ๊ฐ์ ์ด๋๋ฐ์ด์ค๋ฅผ ์ฌ์ฉํจ์ผ๋ก์จ ์ ์ฝ์ ๋๋ ๊ฒ์ด ๋ ์ข์ ์ค๊ณ๋ผ๋ ๊ฒ์ ํญ์ ์ผ๋ํด๋ฌ์ผํ๋ค.
DistributedLockAop.java
@Aspect
@Component
@RequiredArgsConstructor
@Sl4j
public class DistributedLockAop {
private static final String REDISSON_LOCK_PREFIX = "LOCK:";
private final RedissonClient redissonClient;
private final AopForTransaction aopForTransaction;
@Around("@annotation(com.dnd.matchup.aop.DistributedLock)")
public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
String key = REDISSON_LOCK_PREFIX + distributedLock.key();
RLock rLock = redissonClient.getLock(key);
try {
boolean available = rLock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());
if (!available) return false;
return aopForTransaction.proceed(joinPoint);
} catch (InterruptedException e) {
throw new InterruptedException();
} finally {
try {
rLock.unlock();
} catch (IllegalMonitorStateException e) {
log.info("Redisson Lock Already UnLock {}", key);
}
}
}
}
์ ์ฝ๋์์๋ ๋จผ์ `rLock`์ ๊ฐ์ ธ์จ ํ `tryLock`์ ํตํด ์ ๋ ธํ ์ด์ ์ ์ง์ ๋ waitTime ๊น์ง ๋ฝ ํญ๋์ ์๋ํ๋ค. ์ดํ ๋ฝ์ ํญ๋ํ ์ ์๋ค๋ฉด DistributedLock ์ ๋ ธํ ์ด์ ์ด ์ ์ธ๋์ด ์๋ ๋ฉ์๋๋ฅผ ๋ณ๋์ ํธ๋์ญ์ ์ผ๋ก ์คํํ๊ธฐ ์ํด ์๋์ ๊ฐ์ด ์ ํ ์ ๋ต์ `REQUIRES_NEW`๋ก ์ค์ ํ ํธ๋์ญ์ ์ด ์ ์ธ๋์ด ์๋ ๋ฉ์๋๋ฅผ ์คํํ๋ค.
AopForTransaction.java
@Component
public class AopForTransaction {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
}
์์ ๊ฐ์ด ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ ๊ฒฝ์ฐ ํ๋์ ๋ฌผ๋ฆฌ ํธ๋์ญ์ ๋ด์ ๋ ผ๋ฆฌ ํธ๋์ญ์ ์ด ์ถ๊ฐ๋๋ ํํ๊ฐ ์๋๋ผ, ๋ณ๊ฐ์ ๋ฌผ๋ฆฌ ํธ๋์ญ์ ์ด ์คํ๋๊ฒ ๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ๋ฉ์ธ ๋ก์ง์ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ค๊ณ ํด๋ ๋ฝ ํธ๋์ญ์ ์์๋ ํญ๋๊ณผ ๋ฐ๋ฉ์ด ์ ์์ ์ผ๋ก ์ด๋ฃจ์ด์ง๊ฒ๋์ด ํ๋์ ์ค๋ ๋๊ฐ ๋ฝ์ ์ ์ ํ๊ณ ์๋ ๊ฒฝ์ฐ๋ฅผ ๋ฐฉ์งํ ์ ์๋ค.
โ๏ธ ๊ฒฐ๋ก
๋ ๋์ค๋ฅผ ์ด์ฉํ ๋ถ์ฐ๋ฝ ๋ฐฉ์์ ๋ ๋์ค๊ฐ ์ด๋ฏธ ์ธํ๋ผ์ ํฌํจ๋์ด ์์ ๊ฒฝ์ฐ ์ข์ ์ ํ์ง๊ฐ ๋ ์ ์๋ค. ํ์ง๋ง ์ฑ๊ธ ์ค๋ ๋๋ก ์๋ํ๋ ๋ ๋์ค์ ํน์ฑ์ ๋จ์ผ ์ฅ์ ์ง์ ์ด ๋ ๊ฐ๋ฅ์ฑ์ด ์กด์ฌํ๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ด์ ๊ธ์์ ์๊ฐํ Replication์ด๋ Sentinel ์ํคํ ์ฒ์ ์ฌ์ฉ์ด ๊ถ์ฅ๋๋ค. ํด๋น ์ํคํ ์ฒ์์๋ ํน์ ์ํฉ์ ๋ฐ์ดํฐ ๋ถ์ผ์น ๋ฌธ์ ๋ ๊ฒฝ์ ์ํ๊ฐ ๋ฐ์ํ ์ ์๊ณ ์ด๋ ๋ ๋๋ฝ ์๊ณ ๋ฆฌ์ฆ์ ํตํด ํด๊ฒฐํ ์ ์๋ค