Skip to content

Commit

Permalink
test: Prevent like concurrency by pessimistic lock
Browse files Browse the repository at this point in the history
  • Loading branch information
Minuooooo committed Jul 11, 2024
1 parent 9d350fe commit ca1ab1e
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,4 @@ public class PostLike extends AuditEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "liker_id", nullable = false)
private Member liker;
@Version
private int version;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package greeny.backend.domain.community.repository;

import greeny.backend.domain.community.entity.PostLike;
import greeny.backend.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;

import javax.persistence.LockModeType;
import java.util.Optional;

public interface PostLikeRepository extends JpaRepository<PostLike, Long> {
Optional<PostLike> findByPostIdAndLikerId(Long postId, Long likerId);
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<PostLike> findByPostIdAndLiker(Long postId, Member liker);
boolean existsByPostIdAndLikerId(Long postId, Long likerId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,58 @@
import greeny.backend.exception.situation.PostNotFoundException;
import greeny.backend.exception.situation.SelfLikeNotAllowedException;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.OptimisticLockingFailureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
@Slf4j
public class PostLikeService {
private final PostLikeRepository postLikeRepository;
private final PostRepository postRepository;

@Transactional
public void like(Long postId, Member liker) {
boolean retry;
do {
try {
retry = false;
Optional<PostLike> foundPostLike = findPostLike(postId, liker.getId());
toggle(postId, liker, foundPostLike);
}
catch (OptimisticLockingFailureException e) {
retry = true;
}
} while (retry);
log.info("like");
toggle(postId, liker, findPostLike(postId, liker));
}

public Optional<PostLike> findPostLike(Long postId, Member liker) {
log.info("find post like");
return postLikeRepository.findByPostIdAndLiker(postId, liker);
}

public void toggle(Long postId, Member liker, Optional<PostLike> postLike) {
log.info("toggle");
if (postLike.isEmpty()) {
create(postId, liker);
return;
}
log.info("delete");
delete(postLike.get());
}

public void create(Long postId, Member liker) {
log.info("create");
Post post = getPost(postId);
if (post.getWriter().getId().equals(liker.getId()))
throw new SelfLikeNotAllowedException();
log.info("save post like");
postLikeRepository.save(PostLike.builder()
.post(post)
.liker(liker)
.build());
}

public Post getPost(Long postId) {
return postRepository.findByIdWithWriter(postId).orElseThrow(PostNotFoundException::new);
}

public Optional<PostLike> findPostLike(Long postId, Long likerId) {
return postLikeRepository.findByPostIdAndLikerId(postId, likerId);
}

private void delete(PostLike postLike) {
postLikeRepository.delete(postLike);
}

private void toggle(Long postId, Member liker, Optional<PostLike> postLike) {
if (postLike.isEmpty()) {
create(postId, liker);
return;
}
delete(postLike.get());
public Post getPost(Long postId) {
log.info("get post");
return postRepository.findByIdWithWriter(postId).orElseThrow(PostNotFoundException::new);
}
}
10 changes: 5 additions & 5 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ spring:
# check-location: false
# baseline-on-migration: true

sql:
init:
mode: always
data-locations: classpath:sql/data.sql
# sql:
# init:
# mode: always
# data-locations: classpath:sql/data.sql

jpa:
open-in-view: false
Expand All @@ -32,7 +32,7 @@ spring:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
database: mysql
database-platform: org.hibernate.dialect.MySQL8Dialect
defer-datasource-initialization: true
# defer-datasource-initialization: true

logging:
level:
Expand Down
77 changes: 77 additions & 0 deletions src/test/java/greeny/backend/service/PostLikeServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package greeny.backend.service;

import greeny.backend.domain.community.entity.Post;
import greeny.backend.domain.community.repository.PostLikeRepository;
import greeny.backend.domain.community.repository.PostRepository;
import greeny.backend.domain.community.service.PostLikeService;
import greeny.backend.domain.member.entity.Member;
import greeny.backend.domain.member.entity.Role;
import greeny.backend.domain.member.repository.MemberRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Slf4j
class PostLikeServiceTest {
@Autowired
PostLikeService postLikeService;
@Autowired
MemberRepository memberRepository;
@Autowired
PostRepository postRepository;
@Autowired
PostLikeRepository postLikeRepository;

@Test
void likeConcurrency() throws InterruptedException {
// Given
log.info("test start");
Member savedWriter = memberRepository.save(createMember("[email protected]"));
Member savedLiker = memberRepository.save(createMember("[email protected]"));
Post savedPost = postRepository.save(createPost(savedWriter));

// When
int numberOfThread = 3;
ExecutorService executorService = Executors.newFixedThreadPool(numberOfThread);
CountDownLatch countDownLatch = new CountDownLatch(numberOfThread);
for (int i = 0; i < numberOfThread; i++) {
executorService.submit(() -> {
log.info("Current thread");
postLikeService.like(savedPost.getId(), savedLiker);
countDownLatch.countDown();
});
}
countDownLatch.await();

// Then
assertThat(postLikeRepository.existsByPostIdAndLikerId(savedPost.getId(), savedLiker.getId())).isTrue();
memberRepository.deleteAll();
postRepository.deleteAll();
postLikeRepository.deleteAll();
log.info("test end");
}

Member createMember(String email) {
return Member.builder()
.email(email)
.role(Role.ROLE_USER)
.build();
}

Post createPost(Member writer) {
return Post.builder()
.writer(writer)
.title("안녕")
.content("반가워!")
.hits(0)
.build();
}
}

0 comments on commit ca1ab1e

Please sign in to comment.