격리수준과 잠금
2022.12.06 - [Spring/JPA] - [Lock] 동시성 제어를 위한 JPA Lock 1. 격리수준과 잠금(락)
참고
https://hackernoon.com/optimistic-and-pessimistic-locking-in-jpa
[Real MySQL 8.0] https://link.coupang.com/a/GW7Yc
https://zzang9ha.tistory.com/381
https://www.baeldung.com/jpa-optimistic-locking
https://www.baeldung.com/jpa-pessimistic-locking
https://www.baeldung.com/java-jpa-transaction-locks
잠금(락)은 낙관적 락과 비관적 락 두 종류의 잠금 방식이 있습니다.
낙관적 락(Optimistic Lock)
낙관적 락은 어플리케이션에서 제공하는 락입니다.
트랜잭션 대부분이 충돌이 일어나지 않을 것이라고 가정하고 수행합니다.
JPA에서는 @Version 어노테이션을 이용하여 낙관적 락을 거는 엔티티의 값을 설정합니다.
- 엔티티 객체의 업데이트 수행 시 @Version 어노테이션 설정된 값의 버전을 체크하고 버전이 맞지 않으면 OptimisticLockException을 던집니다.
- 버전이 맞다면 트랜잭션은 업데이트를 커밋하고, @Version의 값을 증가시킵니다.
- @Version 어노테이션이 설정된 값은 증가할 수 있는 값이 필요하므로 'updated_at' 등을 설정하면 좋습니다.
- 적용 가능 타입 : Long, Integer, Short, java.sql.Timestamp
- 하나의 엔티티에는 하나의 @Version만 설정 가능합니다.
- 연관관계의 테이블인 경우 기본(주된) 테이블에 선언되어야 합니다.
특징
- 읽기를 많이 수행하는 어플리케이션 동작에 유용하게 동작할 수 있습니다.
- 일정 시간 동안 잠금을 유지할 수 없는 상황에서도 사용 가능합니다. (비관적락을 걸었을 때 많은 지연시간이 필요한 경우)
- 단점으로는 충돌이 발생한 경우 예외가 발생하는데, 예외에 대한 처리를 해주어야 합니다.
JPA에서 제공하는 잠금 모드
- @Version이 설정된 값을 기준으로 읽기/쓰기 잠금 옵션을 추가할 수 있습니다.
- @Lock 어노테이션은 쿼리에 설정하며, 트랜잭션에서 읽기동작만 있는 경우에도 트랜잭션이 종료될때 버전을 체크합니다.
- @Lock(LockModeType.OPTIMISTIC) (읽기) : @Version 값이 포함된 모든 엔티티의 읽기 잠금
- @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) (쓰기): 모든 엔티티의 읽기를 잠그지만, 버전의 값은 증가 시킴
실습
DB
Entity
@Entity
@Table(name = "goods")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Goods {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 재고
private int stock;
@CreatedDate
private Timestamp createdAt;
@LastModifiedDate
private Timestamp updatedAt;
// 재고 감소 메소드
public Goods decreaseGoodsStock(int count){
this.stock -= count;
return this;
}
}
@Entity
@Table(name = "orders")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Orders {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@CreatedDate
private Timestamp createdAt;
@LastModifiedDate
private Timestamp updatedAt;
}
Service
@Service
@RequiredArgsConstructor
@Slf4j
public class OrdersService {
private final UserRepository userRepository;
private final OrdersRepository ordersRepository;
private final GoodsRepository goodsRepository;
// 새로운 주문 요청 시 id 1번의 상품을 1씩 감소한다고 가정
@Transactional
public Runnable createOrder(Long userId) throws Exception{
User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("Not Found"));
String name = user.getName();
String orderNumber = this.orderNumber(name);
Set<Goods> setGoods = new HashSet<>();
Goods goodsA = goodsRepository.findByIdWithVersionLock(1L).orElseThrow(() -> new RuntimeException("Not Found"));
if(goodsA.getStock() > 0) {
setGoods.add(goodsA);
Orders newOrder = Orders.builder()
.orderNumber(orderNumber)
.user(user)
.build();
Goods goods = goodsA.decreaseGoodsStock(1);
goodsRepository.save(goods);
ordersRepository.save(newOrder);
}
else {
log.info("재고 부족");
}
return null;
}
private String orderNumber(String userName){
int randomNumber = (int) (Math.random() * 100000);
StringBuilder sb = new StringBuilder();
sb.append(userName.substring(1, 2));
sb.append(randomNumber);
return sb.toString();
}
}
Test
- 상품(Goods) A의 재고는 총 2개
- 3개의 쓰레드를 생성하여 상품의 재고를 1씩 감소시키고 주문정보를 생성하도록 테스트 코드를 작성합니다.
@SpringBootTest
public class ConcurrencyTest {
@Autowired
private OrdersService ordersService;
@Test
void test() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 1; i < 4; i++) {
int finalI = i;
executorService.execute(() -> {
Long userId = Long.valueOf(finalI);
try {
System.out.println("userId: " + userId);
ordersService.createOrder(userId);
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
}
}
1. 락을 주지 않았을 경우
repository
public interface GoodsRepository extends JpaRepository<Goods, Long> {
@Query(value = "SELECT g FROM Goods g WHERE g.id = :id")
Optional<Goods> findByIdWithVersionLock(@Param("id") Long id);
}
- 락을 주지 않았을 경우 모든 쓰레드가 하나의 데이터를 이용하기 때문에 재고는 1만 감소합니다.
- 주문정보는 쓰레드 수가 3개이기 때문에 3개의 쓰레드가 생성됩니다. => 재고는 2개인데 3개의 주문이 생성되므로 동시성문제가 발생합니다.
2. 낙관적 락 설정
@Version 설정
Goods 엔티티의 updated_at 값을 @Version으로 설정해 줍니다.
@Entity
@Table(name = "goods")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Goods {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 재고
private int stock;
@CreatedDate
private Timestamp createdAt;
@Version
@LastModifiedDate
private Timestamp updatedAt;
// 재고 감소 메소드
public Goods decreaseGoodsStock(int count){
this.stock -= count;
return this;
}
}
업데이트 실행 시 Hibernate의 쿼리문 where 절에 @Version으로 설정된 값(updated_at)이 추가됩니다.
Hibernate: update goods set created_at=?, name=?, stock=?, updated_at=? where id=? and updated_at=?
@Version 처리를 실행했을 경우, 모든 쓰레드에서 동일한 상품을 읽으려고 시도하지만 이미 다른 트랜잭션에서 해당 값을 읽고있음을 알 수 있게되어 ObjectOptimisticLockingFailureException 예외를 발생합니다.
업데이트 수행 시 @Version 어노테이션 설정된 값의 버전을 체크하고 업데이트를 진행하기 때문에 동시성을 제어할 수 있습니다.
- 상품의 재고는 1만큼 감소하고 주문 정보도 1개 생성됨을 알 수 있습니다.
- @LOCK(LockModeType.NONE)
락 모드 타입
@Version 설정이 된 엔티티의 값을 기준으로 락의 모드를 설정하는 옵션을 가질 수 있습니다.
- @Lock(LockModeType.NONE)
@Version만 설정한 경우 기본적으로 NONE으로 옵션이 설정됩니다.
- @Lock(LockModeType.OPTIMISTIC)
읽기 모드로 @Version 값이 포함된 모든 엔티티의 읽기 실행에 락을 겁니다.
읽기자체에 락을 걸기 때문에 다른 쓰레드에서 읽기 시도를 했을 때 락이 걸려 아무런 동작을 취하지 못하고 예외를 발생시킵니다.
- @LOCK(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
쓰기 모드로 모든 엔티티의 읽기에 락을 걸지만, 버전의 값은 강제로 증가시킵니다.
@Version의 값은 강제적으로 올리는 역할이 추가되며, 현재 엔티티가 업데이트 하는 동안 다른 엔티티를 잠그려는 동작에서 사용될 수 있습니다.
참고 :
https://www.logicbig.com/tutorials/java-ee-tutorial/jpa/optimistic-lock-force-increment-use-case.htmlhttps://vladmihalcea.com/hibernate-locking-patterns-how-does-optimistic_force_increment-lock-mode-work/
파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음
'Spring > JPA' 카테고리의 다른 글
[Lock] 동시성 제어를 위한 JPA Lock 3. 비관적 락(Pessimistic Lock) (0) | 2022.12.08 |
---|---|
[Lock] 동시성 제어를 위한 JPA Lock 1. 격리수준과 잠금(락) (0) | 2022.12.06 |
식별 관계 비식별 관계 (0) | 2021.07.13 |
[JPA 연관관계매핑] 단방향, 양방향 연관 관계 매핑 (0) | 2021.04.11 |
[JPA] Hibernate의 ddl-auto (0) | 2021.04.09 |