Skip to content

Commit

Permalink
유저 ID별로 Lock객체를 만들고 동시에 생성된 Lock객체가 있다면 처음부터 이후 로직을 전부 실패하도록 수정
Browse files Browse the repository at this point in the history
  • Loading branch information
silano08 committed Apr 13, 2024
1 parent bf5d51e commit 5b5656d
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import org.springframework.data.repository.query.Param;

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

public interface BankAccountRepository extends JpaRepository<BankAccount,Long> {

@Lock(value = LockModeType.OPTIMISTIC)
@Query("select s from BankAccount s where s.id = :id")
BankAccount findByIdWithOptimisticLock(Long id);
Optional<BankAccount> findByIdWithOptimisticLock(Long id);


@Lock(LockModeType.PESSIMISTIC_WRITE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,61 +6,100 @@
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.OptimisticLockException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class BankAccountService {

private final BankAccountRepository bankAccountRepository;
private final ConcurrentHashMap<Long, ReentrantLock > locks = new ConcurrentHashMap<>();
private final Lock lock = new ReentrantLock(); // 어플리케이션 레벨에서 동기화를 구현할때 사용

// 서비스 클래스의 일부 멤버 변수로 Lock을 보관하는 Map을 추가
private ConcurrentHashMap<Long, Lock> locks = new ConcurrentHashMap<>();
private ConcurrentHashMap<Long, AtomicInteger> concurrentDeposits = new ConcurrentHashMap<>();


public BankAccountService(BankAccountRepository bankAccountRepository) {
this.bankAccountRepository = bankAccountRepository;
}

public Long getBalance(Long id){
public Long getBalance(Long id) {
// 잔고 내역 조회
BankAccount balance = bankAccountRepository.findByIdWithOptimisticLock(id);
BankAccount balance = bankAccountRepository.findByIdWithOptimisticLock(id).orElseThrow(() -> new IllegalStateException("Account not found"));
return balance.getBalance();
}

@Transactional(isolation = Isolation.SERIALIZABLE)
public void increase(Long id, Long quantity) throws RuntimeException {

// 잔고 입금
// 요청이 동시에 2개이상 올 경우 실패 -> 실패했을때 실패라고 출력,옵티미스틱 락 사용
// 출금과 입금요청이 동시에오면 요청온 차례대로 실행
// 락을 획득하는 로직 추가
BankAccount account = bankAccountRepository.findById(id).orElseThrow(() -> new IllegalStateException("Account not found"));
BankAccount account = bankAccountRepository.findByIdWithOptimisticLock(id)
.orElseThrow(() -> new IllegalStateException("Account not found"));

account.increase(quantity);
bankAccountRepository.save(account);
AtomicInteger counter = concurrentDeposits.computeIfAbsent(id, k -> new AtomicInteger(0));
int currentCount = counter.incrementAndGet();

// 최초 요청 시점에서도 동시성 검사를 수행하고 모든 요청을 거부
if (currentCount > 1 || !tryLock(account.getId())) {
counter.decrementAndGet(); // 카운트 감소
throw new RuntimeException("Concurrent deposit requests are not allowed. All requests are failed.");
}

try {
Thread.sleep(100); // 동시성을 더 명확하게 보여주기 위한 대기 시간
account.increase(quantity);
bankAccountRepository.save(account);
throw new RuntimeException("Forced failure to ensure all transactions fail.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
releaseLock(account.getId());
concurrentDeposits.get(id).decrementAndGet(); // 작업 완료 후 카운터 감소
}
}



@Transactional
public void decrease(Long id, Long quantity){
public void decrease(Long id, Long quantity) {
// 잔고 출금
// 요청이 동시에 2개이상 올 경우 차례대로 실행 -> 실패했을때 재시도하도록 옵티미스틱 락 사용
BankAccount balance = bankAccountRepository.findByIdWithOptimisticLock(id);
BankAccount balance = bankAccountRepository.findByIdWithOptimisticLock(id).orElseThrow(() -> new IllegalStateException("Account not found"));
balance.decrease(quantity);
bankAccountRepository.saveAndFlush(balance);
}

public boolean acquireLock(Long id) {
// 맵에서 잠금 객체를 얻거나 새로 생성합니다.

// Lock을 시도하는 메서드
private boolean tryLock(Long id) {
// ID별로 Lock 객체를 만들고, 이미 존재하지 않으면 새로운 ReentrantLock을 생성
Lock lock = locks.computeIfAbsent(id, k -> new ReentrantLock());
return lock.tryLock(); // tryLock을 사용하여 잠금 시도, 잠금 가능하면 true 반환

// tryLock을 사용하여 즉시 Lock을 획득하려 시도
boolean isLocked = lock.tryLock();
if (!isLocked) {
// Lock을 획득하지 못했다면 false 반환
return false;
}

// Lock을 성공적으로 획득했다면 true 반환
return true;
}

public void releaseLock(Long id) {
ReentrantLock lock = locks.get(id);
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock(); // 현재 스레드가 잠금을 보유하고 있다면 잠금을 해제합니다.
// Lock을 해제하는 메서드
private void releaseLock(Long id) {
// Map에서 해당 ID에 해당하는 Lock을 가져옴
Lock lock = locks.get(id);

if (lock == null) {
// Lock 객체가 없는 경우는 없어야 하지만, 혹시 모르니 검사
throw new IllegalStateException("No lock present for ID: " + id);
}

// Lock을 해제하고, Lock 객체를 Map에서 제거
lock.unlock();
locks.remove(id, lock);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@ActiveProfiles("test")
//@ActiveProfiles("test")
class BalanceFacadeTest {

@Autowired
Expand Down

0 comments on commit 5b5656d

Please sign in to comment.