갯수 제한 이벤트시 순삭당하는 경우가 있습니다.
멀티 쓰레드 환경에서 sync하고 atomic하게 구현해야 정확하게 갯수에 맞춰서 사용자에게 서비스를 제공할 수 있습니다.
그러한 check를 db를 사용하기에는 너무 느리기에 redis를 사용하여 구현하였습니다.
일반 Spring boot에서 사용하는 Jedis 와 Lettuce는 lock 구현이 안되어 있어 redisson을 사용해야 합니다.
정확히는 스핀락을 사용하지 않는 pub/sub 기반으로 사용하려고 redisson을 사용합니다.
gradle
implementation 'org.redisson:redisson-spring-boot-starter:3.16.8'
application.properties
# Redis
spring.redis.host=localhost
spring.redis.port=6379
Config를 설정하여 Redisson Client를 bean에 등록합니다.
import java.io.IOException;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedissonConfig {
private final RedisProperties redisProperties;
@Profile({"local","prod","dev"}) // profile은 나누지 않았다면 제거해도 됨
@Bean(name = "redissonClient")
public RedissonClient redissonClientSingle() throws IOException {
RedissonClient redisson = null;
Config config = new Config();
final Codec codec = new StringCodec(); // redis-cli에서 보기 위해
config.setCodec(codec);
config.useSingleServer().
setAddress("redis://" + redisProperties.getHost()+":"+redisProperties.getPort()).
setConnectionPoolSize(100) // pool size는 custom
redisson = Redisson.create(config);
return redisson;
}
/* Cluster 구성 했을때.
@Profile({"prod","dev"})
@Bean(name = "redissonClient")
public RedissonClient redissonClientCluster() throws IOException {
String[] nodes = redisProperties.getUrl().split(",");
for (int i = 0; i < nodes.length; i++) {
nodes[i] = "redis://" + nodes[i];
}
RedissonClient redisson = null;
Config config = new Config();
final Codec codec = new StringCodec(); // redis-cli에서 보기 위해
config.setCodec(codec);
config.useClusterServers()
.setScanInterval(2000)
.addNodeAddress(nodes);
redisson = Redisson.create(config);
return redisson;
}
*/
}
Service로 모듈화를 시킴.
import java.util.concurrent.TimeUnit;
import org.redisson.api.RLock;
import org.redisson.api.RTransaction;
import org.springframework.transaction.TransactionStatus;
public interface MyRedis {
public RLock lock(String lockKey);
public RLock lock(String lockKey, long timeout);
public RLock lock(String lockKey, TimeUnit unit, long timeout);
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);
public void unlock(String lockKey);
public void unlock(RLock lock);
public Object getValue(String key);
public void setValue(String key, Object value);
public TransactionStatus startDBTransacton();
public void commitDB(TransactionStatus status);
public void rollbackDB(TransactionStatus status);
public RTransaction startRedisTransacton();
public void commitRedis(RTransaction transaction);
public void rollbackRedis(RTransaction transaction);
public boolean canUnlock(String lockKey);
}
import java.util.concurrent.TimeUnit;
import org.redisson.api.RLock;
import org.redisson.api.RTransaction;
import org.redisson.api.RedissonClient;
import org.redisson.api.TransactionOptions;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Component
@Getter
@RequiredArgsConstructor
public class MyRedisImpl implements MyRedis {
private final RedissonClient redissonClient;
private final PlatformTransactionManager transactionManager;
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
return lock;
}
@Override
public RLock lock(String lockKey, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
return lock;
}
@Override
public RLock lock(String lockKey, TimeUnit unit, long timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
@Override
public TransactionStatus startDBTransacton() {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return transactionManager.getTransaction(def);
}
@Override
public void commitDB(TransactionStatus status) {
transactionManager.commit(status);
}
@Override
public void rollbackDB(TransactionStatus status) {
transactionManager.rollback(status);
}
@Override
public RTransaction startRedisTransacton() {
return redissonClient.createTransaction(TransactionOptions.defaults());
}
@Override
public void commitRedis(RTransaction transaction) {
transaction.commit();
}
@Override
public void rollbackRedis(RTransaction transaction) {
transaction.rollback();
}
@Override
public Object getValue(String key)
{
return redissonClient.getBucket(key).get();
}
@Override
public void setValue(String key, Object value)
{
redissonClient.getBucket(key).set(value);
}
@Override
public boolean canUnlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
if(lock != null && lock.isLocked() && lock.isHeldByCurrentThread())
{
return true;
}
return false;
}
}
사용하는 부분
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import org.redisson.api.RBucket;
import org.redisson.api.RTransaction;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService
{
private final MyRedis myRedis;
@Override
public boolean temp() {
String lock = "lock_key"; // 해당 키에만 lock을 걸고 싶으면 value를 key로
boolean ret = false;
RTransaction transaction = myRedis.startRedisTransacton();
TransactionStatus status = myRedis.startDBTransacton();
try {
if (myRedis.tryLock(lock, TimeUnit.SECONDS, 5L, 10L)) {
// Redis 처리
RBucket<int[]> imageBucket = transaction.getBucket(/*key*/);
int imageList[] = imageBucket.get();
/* redis test
if(true)
{
throw new Exception()
}
*/
imageList[3]++;
imageBucket.set(imageList);
/* redis test2
if(true)
{
throw new Exception()
}
*/
RBucket<Integer> memberBucket = transaction.getBucket(/*key*/);
int savedBitmap = memberBucket.get();
memberBucket.set(++savedBitmap);
/*
DB 처리
*/
/* db,redis rollback test
if(true)
{
throw new Exception()
}
*/
myRedis.commitRedis(transaction);
myRedis.commitDB(status);
}
} catch (Exception e) {
myRedis.rollbackRedis(transaction);
myRedis.rollbackDB(status);
e.getStackTrace();
ret = false;
} finally {
if (myRedis.canUnlock(lock)) {
myRedis.unlock(lock);
}
}
return ret;
}
}
temp method에 @Transactional을 사용하면 위험합니다.
내부적으로 AOP를 사용하기 때문에 unlock이 된 후 commit이 되므로 동기화가 제대로 되지 않을 수도 있습니다.
그래서 redis에 lock을 잡고 db와 redis의 transaction 처리를 따로 해줘야 합니다.
redis transaction은 transaction처리가 가능하도록 transaction에서 제공하는 api로 set,get을 해야 합니다.
자세한 내용은 RTransaction을 보면 list,map등 여러가지 구현 할 수 있습니다.
감사합니다.
'개발 > Spring' 카테고리의 다른 글
Part.2 Spring boot 갯수 제한 이벤트 구현(Redis 이용) Redisson, Transaction, Sync(lock) 유저가 많이 몰릴때. (0) | 2022.04.06 |
---|---|
Spring @RequestParam @RequestBody @ModelAttribute 차이 (0) | 2022.03.20 |
Spring boot NHN Cloud api-alimtalk 2.2 사용하기 (0) | 2022.03.19 |
Spring boot s3 이미지 Upload, url 이미지 다운로드하기 (0) | 2022.03.19 |
Spring boot CORS 처리 (0) | 2022.03.17 |