Skip to content

Commit

Permalink
Weekly/8/issue#144 동시성 제어 (#189) (#197)
Browse files Browse the repository at this point in the history
* Feat: RedissonConfig 설정

* Feat: Redisson Lock 구현(임시)

* Fix: RaceCondition 테스트 오류 수정

1. RedissonLock의 value로 hostId만 넘기기
2. RaceConditionTest를 SpringBootTest로 테스트
3. BeforeEach로 Redis 초기화

* Refactor: GroupMember 조회 fetch join 적용

* Refactor: random GroupMember 조회 fetch join 적용

* Refactor: 방문자 조회수 비동기 로직 추가

* Feat: 질문 지목 시 count 증가 동시성 제어

* Refactor: merge weekly9

* Refactor: 누락된 코드 추가

* Refactor: 누락된 코드 추가

* Refactor: 코드 수정

* Refactor: redissonClient 제거

* Refactor: redis 설정 변경

* Refactor: redis 설정 변경

* HotFix: 이미지 파일 resize 비율 조정

* Refactor: RedissonClient 사용 테스트 ActiveProfile 설정

* Refactor: RedissonClient 사용 테스트 Profile 설정

* Fix: Profile() 메서드에 적용

* Fix: ProfileIntegrationTest, RaceConditionTest 주석처리

* Fix: 충돌 해결

* Refactor: Random Question Response 수정

* Chore: 불필요한 Import 제거

* Import: 충돌 해결

---------

Co-authored-by: yso8296 <66588512+yso8296@users.noreply.github.com>
Co-authored-by: Kwon Da woon <82216606+momnpa333@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent 3bcf8d5 commit 286b95e
Show file tree
Hide file tree
Showing 19 changed files with 331 additions and 32 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.33.0'

// embedded redis - 테스트 시 적용
testImplementation('it.ozimov:embedded-redis:0.7.3') {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/supernova/whokie/global/annotation/RedissonLock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package supernova.whokie.global.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLock {
String value();
long waitTime() default 5000L;
long leaseTime() default 2000L;
}
55 changes: 55 additions & 0 deletions src/main/java/supernova/whokie/global/aop/RedissonAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package supernova.whokie.global.aop;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.client.RedisException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import supernova.whokie.global.annotation.RedissonLock;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@RequiredArgsConstructor
public class RedissonAspect {

private final RedissonClient redissonClient;

@Around("@annotation(supernova.whokie.global.annotation.RedissonLock)")
public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedissonLock annotation = method.getAnnotation(RedissonLock.class);
String lockKey = method.getName() + getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), annotation.value());
RLock lock = redissonClient.getLock(lockKey);
try {
boolean lockable = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), TimeUnit.MILLISECONDS);
if (!lockable) {
return false;
}
return joinPoint.proceed();
} catch (RedisException e) {
throw new Exception("Temporary errors failed to access the service");
} finally {
lock.unlock();
}
}

public Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

for(int i=0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
return parser.parseExpression(key).getValue(context, Object.class);
}
}
27 changes: 12 additions & 15 deletions src/main/java/supernova/whokie/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
package supernova.whokie.global.config;

import lombok.RequiredArgsConstructor;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.spring.data.connection.RedissonConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import supernova.whokie.global.property.RedisProperties;

@Profile("redis")
@Configuration
@RequiredArgsConstructor
public class RedisConfig {

private final RedisProperties redisProperties;
private static final String REDISSON_HOST_PREFIX = "redis://";

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.host(), redisProperties.port());
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisProperties.host() + ":" + redisProperties.port());
return Redisson.create(config);
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);

// Key는 String으로 저장
redisTemplate.setKeySerializer(new StringRedisSerializer());
// Value는 기본적으로 Jdk 직렬화로 저장
redisTemplate.setValueSerializer(new StringRedisSerializer());

return redisTemplate;
public RedisConnectionFactory redisConnectionFactory(RedissonClient redissonClient) {
return new RedissonConnectionFactory(redissonClient);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public interface GroupMemberRepository extends JpaRepository<GroupMember, Long>
@Query("SELECT g FROM GroupMember g WHERE g.group.id = :groupId")
Page<GroupMember> findAllByGroupId(Pageable pageable, Long groupId);

@Query("SELECT g FROM GroupMember g WHERE g.user.id != :userId AND g.group.id = :groupId ORDER BY function('RAND')")
List<GroupMember> getRandomGroupMember(@Param("userId") Long userId,
@Query("SELECT g FROM GroupMember g JOIN FETCH g.user WHERE g.user.id != :userId AND g.group.id = :groupId ORDER BY function('RAND')")
List<GroupMember> getRandomGroupMemberJoinFetch(@Param("userId") Long userId,
@Param("groupId") Long groupId, Pageable pageable);

Boolean existsByUserIdAndGroupId(Long userId, Long groupId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public Page<GroupMember> getGroupMembers(Pageable pageable, Long userId, Long gr
@Transactional(readOnly = true)
public List<GroupMember> getRandomGroupMembersByGroupId(Long userId, Long groupId,
Pageable pageable) {
return groupMemberRepository.getRandomGroupMember(userId,
return groupMemberRepository.getRandomGroupMemberJoinFetch(userId,
groupId, pageable);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package supernova.whokie.ranking.infrastructure.repoistory;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import supernova.whokie.group.Groups;
import supernova.whokie.ranking.Ranking;
import supernova.whokie.user.Users;
Expand All @@ -17,4 +19,8 @@ public interface RankingRepository extends JpaRepository<Ranking, Long> {
List<Ranking> findAllByGroupIdFetchJoinUsers(Long groupId);

Optional<Ranking> findByUsersAndQuestionAndGroups(Users users, String question, Groups groups);

@Modifying
@Query("UPDATE Ranking r SET r.count = r.count + 1 WHERE r.id = :id")
void incrementCount(@Param("id") Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public Ranking createRanking(Users user, String question, Groups groups) {
public void increaseRankingCountByUserAndQuestionAndGroups(Users user, String question, Groups group) {
Ranking ranking = rankingRepository.findByUsersAndQuestionAndGroups(user, question, group)
.orElseGet(() -> createRanking(user, question, group));
ranking.increaseCount();
rankingRepository.incrementCount(ranking.getId());
rankingRepository.save(ranking);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package supernova.whokie.redis.service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import supernova.whokie.global.annotation.RedissonLock;
import supernova.whokie.profile.service.ProfileVisitReadService;
import supernova.whokie.redis.entity.RedisVisitCount;
import supernova.whokie.redis.entity.RedisVisitor;
Expand All @@ -10,17 +14,14 @@
import supernova.whokie.redis.service.dto.RedisCommand;
import supernova.whokie.redis.util.RedisUtil;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class RedisVisitService {
private final RedisVisitorRepository redisVisitorRepository;
private final RedisVisitCountRepository redisVisitCountRepository;
private final ProfileVisitReadService profileVisitReadService;

@RedissonLock(value = "#hostId")
public RedisVisitCount visitProfile(Long hostId, String visitorIp) {
RedisVisitCount redisVisitCount = findVisitCountByHostId(hostId);
if(!checkVisited(hostId, visitorIp)) {
Expand Down Expand Up @@ -80,5 +81,4 @@ public void deleteAllVisitors(List<RedisVisitor> visitors) {
List<String> ids = visitors.stream().map(RedisVisitor::getId).toList();
redisVisitorRepository.deleteAllById(ids);
}

}
19 changes: 19 additions & 0 deletions src/test/java/supernova/config/EmbeddedRedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import redis.embedded.RedisServer;
import redis.embedded.RedisServerBuilder;

Expand All @@ -11,6 +15,7 @@
@TestConfiguration
public class EmbeddedRedisConfig {
private static final int REDIS_PORT = 6379;
private static final String REDIS_HOST = "localhost";
private RedisServer redisServer;

@PostConstruct
Expand All @@ -20,12 +25,26 @@ public void startRedis() throws IOException {
.setting("maxmemory 128M")
.build();
redisServer.start();
try {
redisServer.start();
} catch (Exception e) {
e.printStackTrace();
}
}


@PreDestroy
public void stopRedisServer() {
if (redisServer != null) {
redisServer.stop();
}
}

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + REDIS_HOST + ":" + REDIS_PORT);
return Redisson.create(config);
}
}
3 changes: 2 additions & 1 deletion src/test/java/supernova/whokie/WhokieApplicationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.awspring.cloud.s3.S3Template;
import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
Expand All @@ -13,7 +14,7 @@
"jwt.secret=abcd"

})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class})
class WhokieApplicationTests {

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
Expand Down Expand Up @@ -43,7 +44,7 @@
"jwt.secret=abcd",
"spring.sql.init.mode=never"
})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class AnswerIntegrationTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
Expand All @@ -29,7 +30,7 @@
"jwt.secret=abcd",
"spring.sql.init.mode=never"
})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class FriendRepositoryTest {
@Autowired
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
Expand Down Expand Up @@ -43,7 +44,7 @@
"jwt.secret=abcd",
"spring.sql.init.mode=never"
})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class GroupMemberIntegrationTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
Expand Down Expand Up @@ -32,10 +33,10 @@

@SpringBootTest
@AutoConfigureMockMvc
@Import(EmbeddedRedisConfig.class)
@TestPropertySource(properties = {
"jwt.secret=abcd"
})
@Import(EmbeddedRedisConfig.class)
@MockBean({S3Client.class, S3Template.class, S3Presigner.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class ProfileIntegrationTest {
Expand All @@ -55,11 +56,15 @@ public class ProfileIntegrationTest {
@Autowired
private MockMvc mockMvc;

@Autowired
private RedissonClient redissonClient;

private Users user;
private Profile profile;

@BeforeEach
void setUp() {
redissonClient.getKeys().flushall();
user = createUser();
profile = createProfile();
ProfileVisitCount profileVisitCount = createProfileVisitCount();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
Expand Down Expand Up @@ -40,7 +41,7 @@
"jwt.secret=abcd",
"spring.sql.init.mode=never"
})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class})
@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class QuestionIntegrationTest {

Expand Down
Loading

0 comments on commit 286b95e

Please sign in to comment.