1. 실험 개요
1) 1000만건의 책데이터가 있고 각각의 책 데이터는 아래와 같은 칼럼과 내용을 가지고 있다.
- id(PK), 재고량, 출발년, 출판월, 가격, 출판사, 별점, 제목, 작가, 소분류,대분류, 이미지
2) 책의 재고량은 기본 20으로 설정되어 있고, 순간적으로 주문을 폭주시켜도 주문들이 정상 작동하는지 파악해보자.
3) 과정은 http request의 세션을 이용하여 어떤 유저가 주문을 했고, url을 통해 어떤책을 몇권 주문했는지 파악하고
재고량보다 같거나 적게 주문한 경우에만 주문이 정상작동하도록 설계했다.
4)book table 예시
5)Jumoon table 예시
2. 다양한 Lock의 종류와 Spring에서 해당 Lock들을 구현하는 방법
- 비관적 락 : 트랜잭션이 시작될 때 Shared Lock 또는 Exclusive Lock을 걸고 시작
- 낙관적 락 : DB에서 제공해주는 특징을 이용하는 것이 아닌 Application level에서 잡아주는 Lock, 트랜잭션 필요없다
- 트랜잭션에서 업데이트 하는 테이블이 2개이며 2번째 테이블에서 충돌이 발생한경우
- 비관적 락 : 트랜잭션 전체가 실패하고 rollback이 일어난다.
- 낙관적 락 : 코드 전체를 Transaction으로 잡지 않아서 바뀌는 곳만 바뀌고 충돌부는 안바뀐다. 수정이 안된 부분에 대한 책임은 application 단에서 지며, application에서 롤백을 수동으로 해주어야 한다.
1) @Version 을 이용하는방법
- Entity 자체에 @Version을 추가한다.
- Transation을 시작할 때 Version
- Optimistic Locking 방식 중 하나이다.
- 트랜잭션을 시작할 때와 커밋 시점에서 버전을 확인하여 동시성제어를 한다.
- 즉 시작할 때와 끝날 때 버전이 다르면(중간에 다른 스레드에서 수정하면) 커밋이 실패한다.
- 아래와 같이 엔티티 안에 간단히 @Version 칼럼을 추가해주면 설정이 끝이다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Long availableStock;
@Version
private Long version;
}
- @Version을 사용할 경우 모든 row의 version을 0으로 초기화 시켜줘야한다.(그냥 컬럼만 만들면 null)
- null인경우 기존 버전이 없어 기존 버전+1 로 새로운 버전을 만들지 못한다.
2)@Lock 어노테이션의 VALUE를 사용하는경우
- 비관적 락
- PESSIMISTIC_READ : 다른 트랜잭션이 읽을 수 있지만, 잠금으로 수정 불가
- PESSIMISTIC_WIRTE : 다른 트랜잭션이 읽을 수 없고, 잠금으로 인해 수정할 수 있음
- PESSIMISTIC_FORCE_INCEREMET : 다른 트랜잭션이 읽거나 수정할 수 없고, 잠금을 설정하여 버전 번호를 증가시킨다.
- 낙관적 락
- OPTIMISTIC : 먼저 읽어오고 트랜잭션이 끝나는 시점에 버전확인을 해서 변경 사항이 없을 때 DB 업데이트
- OPTIMISTIC_FORCE_INCEREMET : 읽어올 때 버전을 업데이트한다. 이러면 다른 트랜잭션이 동시에 엔티티를 변경하더라도 버전이 다르기 때문에 최종적으로 충돌이 발생하지 않는다.
아래와 같이 서비스의 Transaction 단위에 락을 걸고 어떤 방식일지 value를 통해 설정해준다.- 아래처럼 걸면 Lock이 안먹힌다. 엔티티 자체를 불러올 때 걸어야한다.
@Transactional
@Lock(value = LockModeType.PESSIMISTIC_READ)
public String meterbookorder(Long memberid, Long bookid, Integer quantity){
.
.
.
}
- 아니면 Entity를 찾는 부분에 Lock을 걸어줄 수 있다.
public class JumoonService {
private final BookRepository bookRepository;
private final MemberRepository memberRepository;
private final EntityManager entityManager;
@Transactional
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
public String meterbookorder(Long memberid, Long bookid, Integer quantity){
Member member = memberRepository.findById(memberid).orElseThrow(
()-> new CustomException(ErrorCode.MEMBER_NOT_FOUND)
);
Book book = entityManager.find(Book.class, bookid, LockModeType.PESSIMISTIC_WRITE);
Long inven = book.getInventory() - quantity;
if (inven<0){
new CustomException(ErrorCode.INVALIDATION_NOT_ENOUGH);
}else {
book.orderbook(inven);
jumoonRepository.save(new Jumoon(member,book,quantity));
return "success";
}
return null;
}
}
- 또는 JPA단위로 Lock을 걸어줄 수 있다.
public interface BookRepository extends JpaRepository<Book, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Book> findById(Long id);
}
3)Redission 분산 락
- 분산 서버의 동시성 제어를 위해 만들어졌다.
- 동일한 DB에 여러 서버가 접근한다고 했을 때 동시성 제어의 문제가 발생한다.
- Luttuce에서도 락을 제공하나 단점들로 인해 Redisson의 분산락을 사용한다.
- setnx메서드를 이용해 사용자가 직접 스핀락 형태로 구성하여 락 점유가 실패할 경우 계속 락 점유를 시도하게 된다. 이로 인해 레디스는 계속 부하를 받게 되며, 응답시간이 지연된다. 추가적으로 만료 시간을 제공하지 않아 락을 점유한 서버가 장애가 생기면 다른 서버들도 해당 락을 점유할 수 없는 상황이 연출된다.
- 사용하기 위한 의존성 설정
dependencies {
implementation 'org.redisson:redisson-spring-boot-starter:3.16.3'
}
- application.yml 또는 properties 설정
spring:
redis:
host: localhost
port: 6379
spring.redis.host=127.0.0.1
spring.redis.port=6379
- 서비스 로직 설정 예시
public void decrease(final String key, final int count){
final String lockName = key + ":lock";
final RLock lock = redissonClient.getLock(lockName);
final String worker = Thread.currentThread().getName();
try {
if(!lock.tryLock(1, 3, TimeUnit.SECONDS))
return;
final int stock = currentStock(key);
if(stock <= EMPTY){
log.info("[{}] 현재 남은 재고가 없습니다. ({}개)", worker, stock);
return;
}
log.info("현재 진행중인 사람 : {} & 현재 남은 재고 : {}개", worker, stock);
setStock(key, stock - count);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock != null && lock.isLocked()) {
lock.unlock();
}
}
}
- 주요기능인 RLock의 teyLock 메서드를 살펴보자
- 파라미터로 들어오는 leaseTime 시간 동안 락을 점유하는 시도합니다.
- 락을 사용할 수 있을 때까지 waitTime 시간까지 기다립니다.
- leaseTime시간이 지나면 자동으로 락이 해제됩니다.
- 정리하자면 선행 락 점유 스레드가 존재하면 waitTime동안 락 점유를 기다리며 leaseTime 시간 이후로는 자동으로 락이 해제되기 때문에 다른 스레드도 일정 시간이 지난 후 락을 점유할 수 있습니다.
3. http request 세션을 무시한 채 실험 진행
1) 세션을 읽고 누가 주문했는지 파악해야하나 아직 Jmeter를 숙달하지 못해 그부분이 어려워 아래와 같은 로직을 설계하고 진행.
- url : /meterbook/{memberid}/{bookid}/{quantity}
- 위 url로 누가 어떤책을 몇권 구매했는지 바로 요청해본다.
2) 별도의 Lock을 걸지않고 Transaction만으로 진행해본다.
ㄱ. URL 설정
- /meterbook/3/3/4
ㄴ. Thread Group 설정
- Number of Threads(users) : 20
- Ramp-up-period : 0.5
- Loop Count : 1
ㄷ. View Results Tree
- 재고 20개중 4개만 빼라고 요청했으니 5개만 성공한 것은 맞다
ㄹ. 주문 테이블 결과
- 주문도 5개 잘 들어갔다.
ㅁ. 책 테이블 결과
- 재고량은 8권이 되었다.
3) 앞선 실험들과 같이 그림으로 설명하지 않고 실험 내용들에 대하여 간결히 표로 작성해보자
5. 참조
1) 레디션 락 : https://velog.io/@hgs-study/redisson-distributed-lock