ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] Optimistic, Pessimistic Lock
    JAVA/SPRING 2024. 8. 10. 16:06
    728x90

    낙관적 락(Optimistic Lock)

    낙관적 락은 사전에 테이블 로우에 락을 거는 방식이 아닌 충돌이 발생했을 경우에 대비하는 방식입니다.
    테이블에 특정 컬럼을 추가하여 조회 시점의 값과 저장하려는 시점의 값이 동일한지 확인하여 충돌을 방지하도록 동작합니다.
    이는 충돌 발생 빈도수가 낮은 상황에 적합하며 지속적인 락으로 인한 성능 저하를 막을 수 있습니다.

    아래는 Spring boot, JPA에서 낙관적 락을 적용하는 예시입니다.

    Entity

    @Entity
    class UserEntity(
        id: Long,
        name: String,
        version: Int,
    ) {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        var id: Long = id
            protected set
    
        var name: String = name
            protected set
    
        @Version
        var version: Int = version
            protected set
    
        fun updateName(name: String) {
            this.name = name
        }
    }

    Service

    @Service
    class UserService(
        private val userEntityRepository: UserEntityRepository
    ) {
        @Transactional
        fun update(id: Long, name: String) {
            val user = userEntityRepository.findById(id)
                .orElseThrow { RuntimeException("User not found") }
            user.updateName(name)
        }
    }

    테스트 케이스

    class UserServiceTest @Autowired constructor(
        private val userService: UserService
    ) {
        @Test
        fun `10명 동시접근시 낙관락 테스트`() {
            val executorService = Executors.newFixedThreadPool(10)
            val latch = CountDownLatch(10)
            for (i in 1..10) {
                executorService.submit {
                    val name = (1..100).random().toString()
                    try {
                        userService.update(1, name)
                    } catch (e: Exception) {
                        println(e)
                    } finally {
                        latch.countDown()
                    }
                }
            }
    
            latch.await()
        }
    }

    위 테스트 케이스는 10명의 사용자가 동시에 접근하여 리소스를 수정하려는 상황을 연출했습니다.
    테스트를 실행시 의도한대로 총 9번의 ObjectOptimisticLockingFailureException이 발생했습니다

    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]
    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]
    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value ma
    pping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]
    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]
    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]
    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]
    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]
    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]
    org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.side.jobiss.persistence.entity.UserEntity#1]

    비관적 락(Pessimistic Lock)

    비관적 락은 변경하려는 테이블 로우에 락을 걸어 다른 트랜잭션에서 접근을 하지 못하도록 막아 충돌을 방지하는 방식입니다.
    낙관적 락에 비해 설정에 따라 데이터에 접근조차 하지 못하여 성능상에서는 좋지 않습니다.
    JPA에서 제공하는 비관적 락은 PESSIMISTIC_READ, PESSIMISTIC_WRITE가 있습니다.

    • PESSIMISTIC_READ: 공유락으로 락이 걸린 데이터 로우를 읽은 수 있지만 쓰기 작업은 불가능합니다.
    • PESSIMISTIC_READ: 배타락으로 락이 걸린 데이터에 읽기, 쓰기 작업이 모두 불가능합니다.

    아래는 비관적 락의 예시 코드입니다.

    Entity

    @Entity
    class UserEntity(
        id: Long,
        name: String,
    ) {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        var id: Long = id
            protected set
    
        var name: String = name
            protected set
    
        fun updateName(name: String) {
            this.name = name
        }
    }

    Repository

    공유락을 적용

    interface UserEntityRepository : JpaRepository<UserEntity, Long> {
        @Lock(LockModeType.PESSIMISTIC_READ)
        @QueryHints(QueryHint(name = "jakarta.persistence.lock.timeout", value = "10000"))
        fun findWithPessimisticLockById(id: Long): Optional<UserEntity>
    }

    Service

    @Service
    class UserService(
        private val userEntityRepository: UserEntityRepository
    ) {
        @Transactional
        fun update(id: Long, name: String) {
            val user = userEntityRepository.findWithPessimisticLockById(id)
                .orElseThrow { RuntimeException("User not found") }
            user.updateName(name)
        }
    }

    테스트 케이스

    class UserServiceTest @Autowired constructor(
        private val userService: UserService
    ) {
        @Test
        fun `10명 동시접근시 비관락 테스트`() {
            val executorService = Executors.newFixedThreadPool(10)
            val latch = CountDownLatch(10)
            for (i in 1..10) {
                executorService.submit {
                    val name = (1..100).random().toString()
                    try {
                        userService.update(1, name)
                    } catch (e: Exception) {
                        println("$e")
                    } finally {
                        latch.countDown()
                    }
                }
            }
    
            latch.await()
        }
    }

    낙관적 락과 동일하게 10명의 사용자가 동시에 접근하는 상황을 가정하였습니다.
    기대했던 대로 10건중 9건의 요청은 CannotAcquireLockException이 발생하였습니다.

    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update user_entity set name=?,version=? where id=?]; SQL [update user_entity set name=?,version=? where id=?]
    728x90
Designed by Tistory.