Skip to content

Commit

Permalink
♻️ 쿠폰 발행 - lua script를 활용한 atomic연산 방식으로 변경
Browse files Browse the repository at this point in the history
  • Loading branch information
qwerty1434 committed Jan 5, 2024
1 parent fd32118 commit 0704515
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 29 deletions.
46 changes: 17 additions & 29 deletions src/main/java/kr/bb/store/domain/coupon/handler/CouponIssuer.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,43 @@
import kr.bb.store.domain.coupon.exception.ExpiredCouponException;
import kr.bb.store.domain.coupon.repository.IssuedCouponRepository;
import kr.bb.store.util.RedisOperation;
import lombok.RequiredArgsConstructor;
import kr.bb.store.util.luascript.CouponLockExecutor;
import kr.bb.store.util.luascript.RedisLuaScriptExecutor;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.List;
import java.util.function.Predicate;

import static kr.bb.store.util.RedisUtils.makeRedisKey;
import static kr.bb.store.util.luascript.LockScript.script;

@RequiredArgsConstructor
@Component
public class CouponIssuer {
private final IssuedCouponRepository issuedCouponRepository;
private final RedisLuaScriptExecutor redisLuaScriptExecutor;
private final RedisOperation redisOperation;

public CouponIssuer(IssuedCouponRepository issuedCouponRepository, CouponLockExecutor couponLockExecutor, RedisOperation redisOperation) {
this.issuedCouponRepository = issuedCouponRepository;
this.redisLuaScriptExecutor = couponLockExecutor;
this.redisOperation = redisOperation;
}

public IssuedCoupon issueCoupon(Coupon coupon, Long userId, String nickname, String phoneNumber, LocalDate issueDate) {
if(coupon.getIsDeleted()) throw new DeletedCouponException();
if(coupon.isExpired(issueDate)) throw new ExpiredCouponException();

String redisKey = makeRedisKey(coupon);
String redisValue = userId.toString();
Integer limitCnt = coupon.getLimitCount();
if(isDuplicated(redisKey, redisValue)) throw new AlreadyIssuedCouponException();

List<Long> result = (List) redisOperation.countAndSet(redisKey, redisValue);

Integer limitCnt = coupon.getLimitCount();
Long issueCount = result.get(0);
if(isExhausted(limitCnt,issueCount)) {
redisOperation.remove(redisKey, redisValue);
throw new CouponOutOfStockException();
boolean issuable = (Boolean)redisLuaScriptExecutor.execute(script, redisKey, redisValue, limitCnt);
if(issuable) {
return issuedCouponRepository.save(makeIssuedCoupon(coupon,userId,nickname,phoneNumber));
}

return issuedCouponRepository.save(makeIssuedCoupon(coupon,userId,nickname,phoneNumber));
throw new CouponOutOfStockException();
}

public void issuePossibleCoupons(List<Coupon> coupons, Long userId, String nickname, String phoneNumber, LocalDate issueDate) {
Expand All @@ -56,16 +60,9 @@ public void issuePossibleCoupons(List<Coupon> coupons, Long userId, String nickn
}))
.filter(coupon -> {
String redisKey = makeRedisKey(coupon);

List<Long> result = (List) redisOperation.countAndSet(redisKey, redisValue);

Integer limitCnt = coupon.getLimitCount();
Long issueCount = result.get(0);
if(isExhausted(limitCnt,issueCount)) {
redisOperation.remove(redisKey, redisValue);
return false;
}
return true;
boolean issuable = (Boolean)redisLuaScriptExecutor.execute(script, redisKey, redisValue, limitCnt);
return issuable;
})
.forEach(coupon -> issuedCouponRepository.save(makeIssuedCoupon(coupon,userId,nickname,phoneNumber)));
}
Expand All @@ -86,15 +83,6 @@ private IssuedCouponId makeIssuedCouponId(Long couponId, Long userId) {
.build();
}

/*
* 모든 쿠폰은 생성시 expirationDate 설정을 위해 DUMMY_DATA를 넣어 redis에 등록됩니다.
* 하나의 데이터가 더 들어있기 때문에 이를 고려해 '<=' 가 아닌 '<'로 개수를 비교해야
* 쿠폰에 등록한 limitCount만큼 발급이 가능합니다.
*/
private boolean isExhausted(Integer limitCount, Long issueCount) {
return limitCount < issueCount;
}

private boolean isDuplicated(String redisKey, String value) {
return redisOperation.contains(redisKey, value);
}
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/kr/bb/store/util/luascript/CouponLockExecutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.bb.store.util.luascript;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;

@Component
@RequiredArgsConstructor
public class CouponLockExecutor implements RedisLuaScriptExecutor{

private final RedisTemplate<String,String> redisTemplate;

@Override
public Boolean execute(String script, String key, Object... args) {
RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
return redisTemplate.execute(redisScript, Collections.singletonList(key), args[0], String.valueOf(args[1]));
}

}
19 changes: 19 additions & 0 deletions src/main/java/kr/bb/store/util/luascript/LockScript.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kr.bb.store.util.luascript;

public class LockScript {
/*
* 모든 쿠폰은 expirationDate 설정을 위해 생성 시점에 DUMMY_DATA를 넣어 redis에 등록됩니다.
* 결과적으로 하나의 데이터(DUMMY_DATA)가 더 들어있기 때문에 이를 고려해 '<='가 아닌 '<'로 개수를 비교해야
* 쿠폰을 생성할 때 설정한 limitCnt수 만큼 발급받게 할 수 있습니다.
*/
public static final String script = "local key = KEYS[1]\n" +
"local value = ARGV[1]\n" +
"local limitCnt = tonumber(ARGV[2])\n" +
"local currentCnt = redis.call('SCARD', key)\n" +
"if currentCnt <= limitCnt then\n" +
" redis.call('SADD', key, value)\n" +
" return true\n" +
"else\n" +
" return false\n" +
"end";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kr.bb.store.util.luascript;

public interface RedisLuaScriptExecutor {
Object execute(String script, String key, Object... args);
}

0 comments on commit 0704515

Please sign in to comment.