Pessimistic Locking in JPA
- PESSIMISTIC_READ allows us to obtain a shared lock and prevent the data from being updated or deleted.
- PESSIMISTIC_WRITE allows us to obtain an exclusive lock and prevent the data from being read, updated or deleted.
- PESSIMISTIC_FORCE_INCREMENT works like PESSIMISTIC_WRITE, and it additionally increments a version attribute of a versioned entity.
PESSIMISTIC_READ
Whenever we want to just read data and don’t encounter dirty reads, we could use PESSIMISTIC_READ (shared lock). We won’t be able to make any updates or deletes, though.
PESSIMISTIC_WRITE
Any transaction that needs to acquire a lock on data and make changes to it should obtain the PESSIMISTIC_WRITE lock. According to the JPA specification, holding PESSIMISTIC_WRITE lock will prevent other transactions from reading, updating or deleting the data.
PESSIMISTIC_FORCE_INCREMENT
Any updates of versioned entities could be preceded with obtaining the PESSIMISTIC_FORCE_INCREMENT lock.
Acquiring that lock results in updating the version column.
PESSIMISTIC_WRITE 예제
CouponGroup 엔티티 생성을 하고 CouponGroup issueCount를 1씩 증가 되도록 하고자 한다.
CouponGroup Entity
@Entity
@Table(name = "coupon_group")
class CouponGroup {
constructor(
name: String,
useStartAt: Instant,
useEndAt: Instant,
totalCount: Int,
) {
this.name = name
this.useStartAt = useStartAt
this.useEndAt = useEndAt
this.totalCount = totalCount
this.issueCount = 0
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
var id: Long? = null
@NotNull
@Column(name = "name", nullable = false)
var name: String = ""
@NotNull
@Column(name = "is_active", nullable = false)
var isActive: Boolean = true
@NotNull
@Column(name = "use_start_at", nullable = false)
var useStartAt: Instant = Instant.now()
@NotNull
@Column(name = "use_end_at", nullable = false)
var useEndAt: Instant = Instant.now()
@NotNull
@Column(name = "total_count", nullable = false)
var totalCount: Int = 0
@NotNull
@Column(name = "issue_count", nullable = false)
var issueCount: Int = 0
@OneToMany(mappedBy = "couponGroup", fetch = FetchType.LAZY)
var coupons: MutableList<Coupon> = mutableListOf()
fun incrIssueCount() {
this.issueCount += 1
}
}
CouponGroup Repository
interface CouponGroupRepository : JpaRepository<CouponGroup, Long>, CouponGroupQueryDslRepository
interface CouponGroupQueryDslRepository {
@Lock(LockModeType.PESSIMISTIC_WRITE)
fun findAvailableById(id: Long): CouponGroup?
}
class CouponGroupQueryDslRepositoryImpl(
private val queryFactory: JPAQueryFactory,
) : CouponGroupQueryDslRepository {
private val couponGroup = QCouponGroup.couponGroup
override fun findAvailableById(id: Long): CouponGroup? {
return queryFactory
.selectFrom(couponGroup)
.where(couponGroup.id.eq(id))
.fetchFirst() ?: null
}
}
CouponGroup Service
interface CouponService {
fun publishCouponByCouponGroupId(couponGroupId: Long): CouponGroup
}
@Service
class BasicCouponService(
private val couponGroupRepository: CouponGroupRepository,
private val couponRepository: CouponRepository,
) : CouponService {
@Transactional
override fun publishCouponByCouponGroupId(couponGroupId: Long): CouponGroup {
return couponGroupRepository.findAvailableById(couponGroupId)!!.let {
it.incrIssueCount()
couponGroupRepository.save(it)
}
}
}
Test
@SpringBootTest
class CouponServiceTest(
private val couponGroupRepository: CouponGroupRepository,
private val couponRepository: CouponRepository,
private val couponService: CouponService,
) : FunSpec(
{
val logger = LoggerFactory.getLogger(this::class.java)
beforeEach {
listOf(
couponRepository,
couponGroupRepository
).forEach {
it.deleteAllInBatch()
}
}
fun givenCouponGroup(totalCount: Int): CouponGroup {
val now = Instant.now()
return couponGroupRepository.save(
CouponGroup(
name = "test",
useStartAt = Instant.now(),
useEndAt = now.plus(7, ChronoUnit.DAYS),
totalCount = totalCount,
)
)
}
test("Pessimistic Write Lock Demo") {
// given
val totalCount = 20
val latchSize = 15
val couponGroup = givenCouponGroup(totalCount)
// when
val threadPool = Executors.newFixedThreadPool(5)
val latch = CountDownLatch(latchSize)
for (i in 1..latchSize) {
threadPool.execute {
try {
couponService.publishCouponByCouponGroupId(couponGroup.id!!)
} catch (e: PessimisticLockingFailureException) {
logger.error("@@ Pessimistic Lock Failure", e)
} catch (e: Exception) {
logger.error("@@ exception", e)
}
}
latch.countDown()
}
latch.await()
// then
Thread.sleep(1000)
couponGroupRepository.findByIdOrNull(couponGroup.id!!)!!.issueCount shouldBe latchSize
}
},
)
Ref
https://www.baeldung.com/jpa-pessimistic-locking
'Kotlin & Java > Spring' 카테고리의 다른 글
Kotlin + Spring Boot 에서 data class 구현으로 Validation 로직 작성하기 (3) | 2024.07.28 |
---|