1. 문제 상황
같은 시간대 또는 겹치는 시간대에 여러 명이 동시에 예약을 하는 경우 어떻게 하면 정확히 한 명의 이용자만 예약할 수 있을지 고민이 되었습니다.
여러 개의 예약이 중복되거나 중첩되는 것을 방지하기 위해 한 번에 한 요청씩 DB에 접근, 조회 및 삽입하는 것이 목적입니다.
사용자 A가 오전 11시부터 오후 12시 30분까지 예약을 시도하고 사용자 B가 오전 11시 30분부터 오후 12시까지 예약을 시도하면 데이터베이스의 열 고유 속성으로 문제를 해결할 수 없습니다.
2. 문제 해결
먼저 경쟁 조건이란 무엇입니까?
둘 이상의 스레드가 동시에 공유 데이터에 액세스하고 동시에 수정하려고 시도할 때 발생하는 문제
첫 번째 시도 방법) 동기화(X)
큐) 트랜잭션이 발생하는 서비스 메소드에 synced 키워드를 추가하여 해결할 수 있습니까?
ㅏ) 미해결
왜?) @Transactional이 작동하는 방식 때문에그러나 Spring에서 @Transactional을 사용하면 메서드를 포함하는 클래스에 매핑되는 새 클래스가 생성되고 실행됩니다.
itemService에서 dim()을 호출하면 TransactionItemService에서 dim()을 실행하여 트랜잭션을 시작하고 정상적으로 종료되면 실제로 itemService.decrease(id, Quantity)를 호출하여 트랜잭션을 종료한다.
그리고 트랜잭션이 종료되면 데이터베이스에 반영됩니다.
하지만 여기서 문제가 발생합니다. 트랜잭션 간의 감소가 완료된 상태에서 감소가 가능하고 다음 수행할 작업을 다른 스레드에서 호출할 수 있기 때문에 동일한 문제가 발생합니다.
@Service
public class TransactionItemService {
private final ItemService itemService;
public TransactionItemService(ItemService itemService) {
this.itemService = itemService;
}
public void decrease(Long id, Long quantity) {
startTransaction();
itemService.decrease(id, quantity);
endTransaction();
}
public void startTransaction() {
}
public void endTransaction() {
}
}
Java 동기화는 하나의 프로세스만 보장합니다. 서버가 1대일 경우에는 1대만 데이터에 접근하기 때문에 상관없지만, 서버가 여러 대일 경우에는 동기화로 연결되어 있어도 여러 대의 서버가 동시에 데이터에 접근할 수 있다.
물론 단일 서버에 여러 프로세스가 있을 수 있지만 일반적으로 그렇지는 않습니다. 일반적인 애플리케이션은 서버당 하나의 프로세스를 사용합니다. 하드웨어 장애로 인해 서버를 사용할 수 없을 때 서버에서 여러 프로세스를 실행하면 서비스 장애가 발생하기 때문입니다.
따라서 여러 서버가 있는 경우 동기화는 사용되지 않습니다.
방법 2) 데이터베이스(Named Lock O)를 이용한 경쟁 조건 해결
급한 경우 (3) Named Lock만 활성화
(1) 낙관적 잠금
사실 잠금을 사용하는 대신 버전을 사용하여 일관성을 조정하는 방법입니다.
먼저 데이터를 읽고 업데이트 중에 현재 버전이 올바른지 확인하여 업데이트합니다.
내가 읽은 버전에 변경 사항이 있으면 응용 프로그램에서 다시 읽은 다음 작업을 수행해야 합니다.

서버 1과 2가 DB에서 버전 1 행을 읽고 있는 상황입니다.
Server1은 먼저 업데이트 요청을 합니다.
쿼리 조건을 보면 version=1 조건이 적용되는 곳이고, 집합을 보면 version+1 을 실행하는 태스크까지 있다.
따라서 업데이트 쿼리가 실행될 때 데이터베이스에는 버전 2의 행이 있습니다.
그런 다음 서버 2가 업데이트 요청을 보내면 버전이 일치하지 않고 업데이트 요청이 실패합니다.
따라서 데이터를 다시 읽어서 연산을 수행하는 코드를 포함해야 합니다.
@Entity
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
@Version
private Long version;
...
}
@Service
public class OptimisticLockItemService {
private ItemRepository itemRepository;
public OptimisticLockItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
@Transactional
public void decrease(Long id, Long quantity) {
Item item = itemRepository.findByIdWithOptimisticLock(id);
item.decrease(quantity);
itemRepository.saveAndFlush(stock);
}
}
@Service
public class OptimisticLockItemFacade {
private OptimisticLockItemService optimisticLockItemService;
public OptimisticLockItemFacade(OptimisticLockItemService optimisticLockItemService) {
this.optimisticLockItemService = optimisticLockItemService;
}
public void decrease(Long id, Long quantity) throws InterruptedException {
while (true) {
try {
optimisticLockItemService.decrease(id, quantity);
break;
} catch (Exception e) {
Thread.sleep(50);
}
}
}
}
버전을 추가하고 @Version을 추가합니다. 낙관적 잠금은 실패할 때 처리해야 하므로 OptimisticLockItemFacade 클래스는 이 문제를 해결합니다.
별도의 잠금장치가 없기 때문에 비관적 잠금에 비해 성능 이점있다
단점은 개발자가 직접 버그를 처리해야 한다는 것입니다.그리고 잦은 충돌이 예상되는 경우 Pessistic Lock을 사용하는 것이 성능에 더 좋을 수 있습니다.
(2) 비관적 잠금(배타적 잠금)
실제로 일관성을 보장하기 위해 데이터를 잠그는 방법입니다.
독점 잠금이 적용되면 다른 트랜잭션은 잠금이 해제될 때까지 데이터를 커밋할 수 없습니다.
이로 인해 교착 상태가 발생할 수 있으므로 주의하여 사용하십시오.
여러 서버가 있고 Server 1이 잠금을 보유하고 있는 경우 다른 서버는 Server 1이 잠금을 해제할 때까지 액세스할 수 없습니다.
잠금이 있는 스레드만 데이터에 액세스할 수 있습니다.
- 다른 트랜잭션이 특정 행에 대한 잠금을 획득하지 못하도록 합니다.
- 트랜잭션 A가 완료되기를 기다린 후 트랜잭션 B는 잠금을 획득합니다.
- 특정 행을 업데이트하거나 삭제할 수 있습니다.
- 일반 선택은 잠금이 없기 때문에 검색이 가능합니다.

충돌이 빈번한 경우 비관적 잠금을 사용하는 것이 낙관적 잠금보다 더 나은 성능을 발휘할 수 있습니다..
업데이트는 잠금에 의해 제어되기 때문에 데이터 일관성에 적합합니다. 하지만 별도의 잠금장치를 사용하기 때문에 성능 저하가 있을 수 있습니다.
또한 단점으로 잠금을 보유한 트랜잭션이 오래 걸리면 연결 수가 충분하지 않을 수 있습니다.
이 경우 데이터베이스 쿼리 속도가 느려지고 서비스 속도가 느려집니다.
반면에 Redis를 사용하면 연결이 기다리지 않기 때문에 이러한 문제를 피할 수 있습니다.
(3) 명명된 잠금 사용(따라서 해결됨)
이름이 있는 메타데이터 잠금입니다. 이름으로 잠금을 획득한 후에는 잠금이 해제될 때까지 다른 세션에서 잠금을 획득할 수 없습니다. 트랜잭션이 종료될 때 잠금이 자동으로 해제되지 않는다는 점에 유의하십시오. 별도의 명령으로 또는 선점 시간이 만료된 후에만 해제됩니다.
- 이름으로 자물쇠를 얻으십시오. 다른 세션에서 잠금을 획득하거나 해제할 수 없습니다.
- mysql에서는 get_lock()으로 잠금을 얻고 release_lock()이라는 명령으로 명명된 잠금을 해제할 수 있습니다.

이전의 비관적 및 낙관적 잠금이 항목을 잠근 경우 명명된 잠금은 별도의 범위를 잠급니다.
이름이 “1”인 Session1이 잠기면 Session1이 잠금 해제된 후 Session2가 잠금 “1”을 얻을 수 있습니다.
따라서 데이터를 삽입할 때 일관성이 필요한 경우 명명된 잠금을 사용할 수 있습니다.
NamedLockItemFacade는 getLock 및 releaseLock이 실제 실행 전후에 실행되어야 하므로 정의됩니다.
@Component
public class NamedLockItemFacade {
private LockRepository lockRepository;
private ItemService itemService;
public NamedLockItemFacade(LockRepository lockRepository, ItemService itemService) {
this.lockRepository = lockRepository;
this.itemService = itemService;
}
@Transactional
public void decrease(Long id, Long quantity) {
try {
lockRepository.getLock(id.toString());
itemService.decrease(id, quantity);
} finally {
lockRepository.releaseLock(id.toString());
}
}
}
@Service
public class ItemService {
private final ItemRepository itemRepository;
public ItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decrease(Long id, Long quantity) {
Item item = itemRepository.findById(id).orElseThrow();
item.decrease(quantity);
itemRepository.saveAndFlush(item);
}
}
ItemService.decrease()는 상위 트랜잭션, 즉 전파와 별도로 실행되어야 합니다. REQUIRES_NEW세트
타임아웃 시간을 짧게 지정하면 잠금이 해제될 수 있으며, 타임아웃 시간을 너무 길게 지정하면 해제가 되지 않을 경우 서비스 속도가 저하될 수 있습니다. 일반적으로 2~5초가 적당합니다.
비관적 잠금과 명명된 잠금의 차이점
이 둘은 유사하지만 Perssimistic Lock은 테이블 또는 행별로 잠그지만 Named Lock은 메타데이터를 잠그고 행 또는 테이블별로 잠그지 않습니다.
결론) 예약이 중복되거나 중복되지 않도록 하려면 어떻게 해야 하나요?
비관적 잠금으로 방지할 수 없습니다. 위에서 말했듯이 Named Lock으로 해결해야 합니다. (Redis로도 가능합니다.)
비관주의는 기존 데이터의 일관성을 방해하는 것이기 때문입니다.
동시 이중 저장을 방지하려면 named lock 또는 redis를 사용하여 잠금을 획득한 후 동일한 데이터가 저장되지 않도록 해야 합니다.
예를 들어 id 1이라는 데이터의 중복 저장을 방지하려면 id(1)을 키로 유지하고 저장하기 전에 잠금을 생성해야 합니다.