๐Ÿ” ์žฌ๊ณ  ์‹œ์Šคํ…œ์˜ ๋™์‹œ์„ฑ ์ œ์–ด์™€ ๋ฝ ์ฒ˜๋ฆฌ ์‹คํ—˜๊ธฐ

์ด๋ช…๊ทœยท2024๋…„ 9์›” 1์ผ
0

๋ฌธ์ œ ์ธ์‹

ํ˜„์žฌ ์„œ๋น„์Šค์—์„œ๋Š” ์žฌ๊ณ  ์ˆ˜๋Ÿ‰์„ ๊ด€๋ฆฌํ•˜๋Š” ๋กœ์ง์ด ์กด์žฌํ•˜๋ฉฐ,
์—ฌ๋Ÿฌ ์š”์ฒญ์ด ๋™์‹œ์— ๋“ค์–ด์˜ฌ ๊ฒฝ์šฐ ๋™์‹œ์„ฑ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ตฌ์กฐ๋‹ค.

์ด๋ฅผ ์ง์ ‘ ๋ˆˆ์œผ๋กœ ํ™•์ธํ•˜๊ณ  ์‹ถ์–ด์„œ,
์žฌ๊ณ  ์ˆ˜๋Ÿ‰์„ ๋‹จ์ˆœํžˆ 1 ๊ฐ์†Œ์‹œํ‚ค๋Š” API๋ฅผ ์ž‘์„ฑํ•˜๊ณ  K6๋ฅผ ํ™œ์šฉํ•ด ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค.


๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ with K6

ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ

  • ์•„๋ž˜ ๊ฐ€์ƒ ์œ ์ € 10๋ช…์ด ๋™์‹œ์— ์š”์ฒญํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ
import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  scenarios: {
    simultaneous_requests: {
      executor: 'per-vu-iterations', // ๊ฐ VU๊ฐ€ ๋™์‹œ์— ์‹œ์ž‘
      vus: 10, // VU ์ˆ˜
      iterations: 1, // ๊ฐ VU๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰
      maxDuration: '1s', // ์ „์ฒด ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ๊ฐ„
    },
  },
};


export default function() {
	http.post('http://localhost:8080/inventories/test');
}

k6 ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰

# k6 run script.js

์‹คํ–‰ ๊ฒฐ๊ณผ

ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ, 10๋ช…์˜ ์š”์ฒญ ์ค‘ 1๊ฑด๋งŒ ๋ฐ˜์˜

์˜ˆ์ƒ๋Œ€๋กœ Lost Update ํ˜„์ƒ ๋ฐœ์ƒ
โ†’ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์ปค๋ฐ‹๋œ ์š”์ฒญ๋งŒ ๋ฐ˜์˜๋˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ๋ชจ๋‘ ๋ฌด์‹œ๋จ

๐Ÿค” ๊ฐ„๋‹จํ•œ synchronized? โ†’ ์‹คํŒจ

@Transactional ํ™˜๊ฒฝ์—์„œ ๋‹จ์ˆœ synchronized ๋ธ”๋ก์„ ์‚ฌ์šฉํ•ด ๋ณด์•˜์ง€๋งŒ,
์Šค๋ ˆ๋“œ ๋ฝ์€ ํ•œ ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ๋งŒ ์œ ํšจํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ€ํ‹ฐ ํ”„๋กœ์„ธ์Šค/๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฌด์˜๋ฏธํ–ˆ๋‹ค.

๋˜ํ•œ, ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์—์„œ ๋ฝ์„ ๊ฑธ๋”๋ผ๋„,
์ปค๋ฐ‹ ์‹œ์  ์ด์ „์— ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ๊ณผ๊ฑฐ ๊ฐ’์„ ์ฝ๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊น€ โ†’ ์‹คํŒจ

synchronized๋Š” JVM ๋ ˆ๋ฒจ์˜ ๋ฝ
synchronized ํ‚ค์›Œ๋“œ๋Š” Java ๊ฐ์ฒด ์ˆ˜์ค€์—์„œ ๋ฝ์„ ๊ฑฐ๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๋ฉฐ
์ด ๋ฝ์€ JVM ๋‚ด๋ถ€์—์„œ ๊ด€๋ฆฌ๋˜๋ฉฐ, JVM ํ”„๋กœ์„ธ์Šค ์•ˆ์— ์žˆ๋Š” ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ ๊ฐ„์˜ ๋™์‹œ ์ ‘๊ทผ์„ ๋ง‰๋Š” ์šฉ๋„๋กœ ๋™์ž‘
๋”ฐ๋ผ์„œ JVM ๋ฐ–, ์ฆ‰ ๋‹ค๋ฅธ ํ”„๋กœ์„ธ์Šค์—์„œ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ์™€๋Š” ๋ฝ์ด ๊ณต์œ ๋˜์ง€ ์•Š์•„ ์ „ํ˜€ ์ œ์–ดํ•  ์ˆ˜ ์—†์Œ.
synchronized๋Š” JVM ๋‚ด๋ถ€(= ๋‹จ์ผ ํ”„๋กœ์„ธ์Šค ๋‚ด)์—์„œ๋งŒ ์œ ํšจํ•œ ๋ฝ์ด๋‹ค.
์ฆ‰, ๋ฉ€ํ‹ฐ ํ”„๋กœ์„ธ์Šค ํ™˜๊ฒฝ์ด๋‚˜ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์—์„œ๋Š” ๋™์‹œ์„ฑ ๋ณด์žฅ์ด ๋˜์ง€ ์•Š๋Š”๋‹ค.

@Transactional
fun updateQty(id: Long) {
     synchronized(lock) {
            // synchronized ๋ธ”๋ก ์•ˆ์˜ ์ฝ”๋“œ๋Š” ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
            ...
        }
}

๋™์‹œ์„ฑ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ํƒ์ƒ‰

์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ๊ฐ์†Œ ๋กœ์ง์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€ํ‘œ์ ์ธ ๋™์‹œ์„ฑ ์ œ์–ด ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค

  • Pessimistic Lock (๋น„๊ด€์  ๋ฝ)
  • Optimistic Lock (๋‚™๊ด€์  ๋ฝ)
  • Named Lock (DB ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฝ)
  • Redis ๊ธฐ๋ฐ˜ ๋ถ„์‚ฐ ๋ฝ (Lettuce / Redisson)

1. Pessimistic Lock

  • ์‹ค์ œ๋กœ DB ์— Lock ์„ ๊ฑฐ๋Š” ๋ฐฉ๋ฒ•
  • ๋ฐฐํƒ€์  Lock, ์ฆ‰ Lock ์„ ๊ฐ€์ ธ์˜จ ์ดํ›„ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ํ•ด๋‹น Lock ์ด ํ•ด์ œ๋˜๊ธฐ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๊ฒŒ๋จ.

์กฐํšŒ์‹œ Lock ์„ ๊ฑธ๊ณ  ํ…Œ์ŠคํŠธ.

@Repository
interface InventoryDetailRepository :
    JpaRepository<InventoryDetail, Long>,
    InventoryDetailRepositoryDSL {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT i FROM InventoryDetail i WHERE i.id = :id")
    fun findByIdWithLock(id: Long): Optional<InventoryDetail>
}

๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ์ž˜ ์ ์šฉ๋œ ๊ฒƒ ๊ฐ™๋‹ค.

	select
    ...
    id1_0.updated_at 
    from
        inventory_detail id1_0 
    where
        (
            id1_0.deleted_at IS NULL
        ) 
        and id1_0.id=? for update

ํ•˜์ง€๋งŒ ๋ฝ์„ ํš๋“ํ•  ๋•Œ๊นŒ์ง€ ํŠธ๋žœ์žญ์…˜์ด ๋Œ€๊ธฐํ•˜๊ธฐ ๋•Œ๋ฌธ์—,
์„ฑ๋Šฅ์ƒ ๋ณ‘๋ชฉ์ด ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๊ณ  ์ผ๋ฐ˜์ ์œผ๋กœ ์ถ”์ฒœ๋˜์ง€๋Š” ์•Š๋Š”๋‹ค.
(์ถฉ๋Œ์ด ๋งŽ์ด ๋ฐœ์ƒํ•˜๋Š” DB ํ…Œ์ด๋ธ”์— ํ•œํ•ด์„œ๋Š” ๊ถŒ์žฅํ•œ๋‹ค๊ณ  ํ•œ๋‹ค)


2. Optimistic Lock

  • Lock ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋ฒ„์ „์„ ๋”ฐ๋กœ ๋ช…์‹œํ•จ์œผ๋กœ์จ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๋งž์ถ”๋Š” ๋ฐฉ๋ฒ•
  • ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์€ ํ›„ update ํ•  ๋•Œ ํ˜„์žฌ ๋ฒ„์ „์ด ๋งž๋Š”์ง€ ํ™•์ธ โ†’ ์ฆ‰ ์žฌ์‹œ๋„ ๋กœ์ง์ด ํ•„์š”ํ•จ

@Version ์ถ”๊ฐ€

@Entity
@Table(
    name = "inventory_detail",
)
class InventoryDetail : AutoIncrementIdEntity() {
    ...
    
    @Version
    var version: Long? = null
}

๊ทธ๋ฆฌ๊ณ  ๊ธฐ์กด DB ํ…Œ์ด๋ธ”์˜ ๋ฒ„์ „์„ 0์œผ๋กœ ์ดˆ๊ธฐ๊ฐ’ ์„ธํŒ…ํ•˜๊ณ  ํ•œ๋ฒˆ ์š”์ฒญํ•ด๋ณด์ž.


ํ™•์ธํ•ด๋ณด๋‹ˆ ์ถฉ๋Œ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ๋ถ€๋ถ„์„ ๋”ฐ๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฉด ObjectOptimisticLockingFailureException ์ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๊ณ  ํ•œ๋‹ค

2024-09-01T21:46:41.349+09:00 ERROR 50605 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.~~.InventoryDetail#1]] with root cause

์ดํ›„ ์ข€ ๋” ๋ฐฉ์•ˆ์„ ์ฐพ์•„๋ณด๊ณ  ์žฌ์‹œ๋„ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค


    @Lock(LockModeType.OPTIMISTIC)
    @Query("SELECT i FROM InventoryDetail i WHERE i.id = :id")
    fun findByIdWithLock(id: Long): Optional<InventoryDetail>

    @Retryable(
        value = [Exception::class],
        maxAttempts = 50,
        backoff = Backoff(delay = 1000),
    )
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateQty(id: Long) {
        try {
            val inventoryDetail =
                inventoryDetailRepository.findByIdWithLock(id).orElseThrow {
                    throw GeneralException.with(GeneralMsgType.NOT_FOUND_INVENTORY)
                }

            inventoryDetail.updateQty(inventoryDetail.getQty() - 1)

            inventoryDetailRepository.saveAndFlush(inventoryDetail)
        } catch (e: ObjectOptimisticLockingFailureException) {
            log.error("Optimistic locking ์ถฉ๋Œ ๋ฐœ์ƒ !! : ${e.message}")
            throw e 
        } catch (e: PersistenceException) {
            log.error("PersistenceException ๋ฐœ์ƒ: ${e.message}")
            throw RuntimeException("PersistenceException ๋ฐœ์ƒ", e)
        } catch (e: Exception) {
            log.error("๊ธฐํƒ€ ์˜ˆ์™ธ ๋ฐœ์ƒ: ${e.message}", e)
            throw e
        }
    }

ํ•˜์ง€๋งŒ ์‹ค์ œ ํ…Œ์ŠคํŠธ์—์„œ๋Š” StaleObjectStateException ์˜ˆ์™ธ๊ฐ€ ๊ณ„์† ๋ฐœ์ƒํ–ˆ๊ณ ,
์˜ˆ์™ธ๊ฐ€ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋ฐ–์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„ catch๋กœ ํฌ์ฐฉ๋˜์ง€ ์•Š์•˜๋‹ค.

โ†’ Spring Retry, ์˜ˆ์™ธ ํ•ธ๋“ค๋ง ๋“ฑ์„ ๋ณต์žกํ•˜๊ฒŒ ๊ตฌ์„ฑํ–ˆ์ง€๋งŒ ๊ฒฐ๊ตญ ์‹คํŒจ๋กœ ๊ฒฐ๋ก ์ง€์—ˆ๋‹ค.

(https://developer.jboss.org/thread/131217)
(https://stackoverflow.com/questions/30236145/not-able-to-catch-org-hibernate-staleobjectstateexception)


3. Named Lock

MySQL์˜ GET_LOCK, RELEASE_LOCK์„ ํ™œ์šฉํ•˜์—ฌ
์ด๋ฆ„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฝ์„ ํš๋“ํ•˜๊ณ  ํ•ด์ œํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
์ด๋ฆ„์„ ๊ฐ€์ง„ Lock ์„ ํš๋“ํ•œ ํ›„ ํ•ด์ œํ•  ๋•Œ๊นŒ์ง€ ๋‹ค๋ฅธ ์„ธ์…˜์€ ์ด Lock ์„ ํš๋“ํ•  ์ˆ˜ ์—†์Œ

  • ์ฃผ์˜. ํŠธ๋žœ์žญ์…˜์ด ์ข…๋ฃŒ๋  ๋•Œ Lock ์ด ์ž๋™์œผ๋กœ ํ•ด์ œ๋˜์ง€ ์•Š์œผ๋ฉฐ ๋ณ„๋„์˜ ๋ช…๋ น์–ด๋กœ ํ•ด์ œ๋ฅผ ์ˆ˜ํ–‰ํ•ด์ฃผ์–ด์•ผ ํ•จ
  • Pessimistic Lock ๊ณผ ๋น„์Šทํ•˜์ง€๋งŒ Pessimistic Lock ์€ ํ…Œ์ด๋ธ”์˜ Row, Table ๋‹จ์œ„๋กœ Lock ์„ ๊ฑฐ๋Š” ๊ฒƒ์ด๋ฉฐ Named Lock ์€ metadata ์— Lock ์„ ๊ฑฐ๋Š” ๋ฐฉ๋ฒ• ์ฆ‰, ๊ณต์œ ์ž์› (Name) ์— ๋Œ€ํ•œ Lock ์„ ๊ฑฐ๋Š” ๊ฒƒ

์ด ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์„œ๋กœ ๋‹ค๋ฅธ ๊ฒƒ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ข‹๋‹ค๊ณ  ์ด์•ผ๊ธฐํ•œ๋‹ค
์ด์œ ๋Š” ์ปค๋„ฅ์…˜ ํ’€์ด ๋ถ€์กฑํ•ด์ง€๋Š” ์ด์Šˆ๊ฐ€ ์ƒ๊ธด๋‹ค

๊ตฌ์กฐ ์„ค๊ณ„

  • InventoryWithNamedLockService: ๋ฝ ํš๋“/ํ•ด์ œ ๋‹ด๋‹น
  • InventoryUpdater: ์‹ค์ œ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ๊ฐ์†Œ ์ฒ˜๋ฆฌ

๋‘๊ฐœ์˜ ์„œ๋น„์Šค๋กœ ๊ตฌํ˜„
(ํŠธ๋žœ์žญ์…˜์˜ ๊ฒฝ๊ณ„์™€ ๋ฝ์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๋ช…ํ™•ํžˆ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•จ)

๋ถ€๋ชจ-์ž์‹ ๊ตฌ์กฐ๋กœ ๊ตฌํ˜„

  • ๋ถ€๋ชจ : ๋ฝ์„ ํš๋“ ๋ฐ ํ•ด์ œํ•˜๋Š” ์ฑ…์ž„์„ ๊ฐ€์ง, ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ๋„˜์–ด ๋ฝ์„ ๊ด€๋ฆฌ
  • ์ž์‹ : ์‹ค์ œ ํŠธ๋žœ์žญ์…˜์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ
interface InventoryDetailRepository :
    JpaRepository<InventoryDetail, Long>,
    InventoryDetailRepositoryDSL {
    @Query(
        value = "select get_lock(:key, 3000)",
        nativeQuery = true,
    )
    fun getLock(key: String)

    @Query(
        value = "select release_lock(:key)",
        nativeQuery = true,
    )
    fun releaseLock(key: String)
}

@Service
class InventoryWithNamedLockService(
    private val inventoryDetailRepository: InventoryDetailRepository,
    private val inventoryUpdater: InventoryUpdater,
) {
    private val log = logger()

    @Transactional
    fun updateQty(id: Long) {
        try {
            inventoryDetailRepository.getLock(id.toString())
            inventoryUpdater.updateQty(id)
        } finally {
            inventoryDetailRepository.releaseLock(id.toString())
        }
    }
}

@Service
class InventoryUpdater(
    private val inventoryDetailRepository: InventoryDetailRepository,
) {
    private val log = logger()

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateQty(id: Long) {
        val inventoryDetail =
            inventoryDetailRepository.findById(id).orElseThrow {
                throw GeneralException.with(GeneralMsgType.NOT_FOUND_INVENTORY)
            }

        inventoryDetail.updateQty(
            inventoryDetail.getQty() - 1,
        )
        log.info("qty : {}", inventoryDetail.getQty())
    }
}

๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ. ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘.
๊ทธ๋Ÿฌ๋‚˜, ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ ์‹œ ๋ฝ์ด ์ž๋™ ํ•ด์ œ๋˜์ง€ ์•Š์Œ โ†’ ์ง์ ‘ ํ•ด์ œ ํ•„์š”


4. Redis ๋ถ„์‚ฐ ๋ฝ

๋Œ€ํ‘œ์ ์œผ๋กœ ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์ด ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค

  1. Lettuce
    • setnx ๋ช…๋ น์–ด๋กœ ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„, spin lock ๋ฐฉ์‹ (๊ณ„์† lock์„ ํ™•์ธ)
    • ๋ณ„๋„์˜ ์žฌ์‹œ๋„ ๋กœ์ง ํ•„์š”
  2. Redisson
    • pub-sub ๊ธฐ๋ฐ˜์œผ๋กœ lock ๊ตฌํ˜„
    • ์ฑ„๋„์„ ๋งŒ๋“ค๊ณ  lock ์„ ํš๋“ํ•˜๋ ค๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ๊ตฌ๋…ํ•˜์—ฌ lock ์„ ํ•ด์ œํ•˜๋ ค๋Š” ์Šค๋ ˆ๋“œ ์ชฝ์—์„œ ์•Œ๋ ค์ฃผ๋ฉด ์•ˆ๋‚ด๋ฅผ ๋ฐ›์€ ์Šค๋ ˆ๋“œ๊ฐ€ lock ์„ ํš๋“ํ•˜๋Š” ๋ฐฉ์‹
    • ๋ณ„๋„์˜ ์žฌ์‹œ๋„ ๋กœ์ง ํ•„์š”ํ•˜์ง€ ์•Š์Œ

4-1. Lettuce

์ด๊ฒƒ๋„ Named Lock ๊ณผ ๋น„์Šทํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋œ๋‹ค ๋‹จ์ง€ Redis ๋ฅผ ํ™œ์šฉํ•  ๋ฟ.

์ผ๋‹จ Redis setnx ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด key ์™€ value ๋ฅผ ์„ค์ •ํ•ด์ฃผ๋Š” RedisRepository ๋ฅผ ๊ตฌํ˜„ํ•˜์ž

@Component
class RedisLockRepository(
    private val redisTemplate: RedisTemplate<String, String>,
) {
    fun lock(key: Long): Boolean? =
        redisTemplate
            .opsForValue()
            .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3000))

    fun unlock(key: Long): Boolean = redisTemplate.delete(generateKey(key))

    fun generateKey(key: Long) = key.toString()
}

์ดํ›„ ๋ถ€๋ชจ-์ž์‹ ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด
๋ถ€๋ชจ ๋ ˆ์ด์–ด๋Š” Redis ์˜ key ์— ๋Œ€ํ•œ Lock ํ•ด์ œ ๋ฐ ํš๋“์„ ๊ตฌํ˜„ (spin lock)

์ž์‹ ๋ ˆ์ด์–ด๋Š” ๊ธฐ์กด ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ์ฒ˜๋ฆฌ ๋กœ์ง

@Service
class InventoryLettuceLockService(
    private val redisLockRepository: RedisLockRepository,
    private val inventoryUpdater: InventoryUpdater,
) {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateQty(id: Long) {
        while (!redisLockRepository.lock(id)!!) {
            Thread.sleep(100)
        }

        try {
            inventoryUpdater.updateQty(id)
        } finally {
            redisLockRepository.unlock(id)
        }
    }
}

์ดํ›„ Redis io ๊ด€๋ จ ๋กœ๊ทธ

2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] o.s.d.redis.core.RedisConnectionUtils    : Fetching Redis Connection from RedisConnectionFactory
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] io.lettuce.core.RedisChannelHandler      : dispatching command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] i.lettuce.core.protocol.DefaultEndpoint  : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] i.lettuce.core.protocol.DefaultEndpoint  : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1] write() done
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] write(ctx, AsyncCommand [type=DEL, output=IntegerOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], promise)
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandEncoder   : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379] writing command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Received: 4 bytes, 1 commands in the stack
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Stack contains: 1 commands
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.l.core.protocol.RedisStateMachine      : Decode done, empty stack: true
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Completing command AsyncCommand [type=DEL, output=IntegerOutput [output=1, error='null'], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] o.s.d.redis.core.RedisConnectionUtils    : Closing Redis Connection

๊ทธ์น˜๋งŒ ํ•ด๋‹น ๋ฐฉ์‹์€ ์˜ˆ์ƒํ•˜๋“ฏ spin lock ๋ฐฉ์‹์ด๋ฏ€๋กœ redis ์˜ ๋ถ€ํ•˜๋ฅผ ์ค„ ์ˆ˜ ์žˆ์Œ
์ถ”์ฒœํ•˜์ง€ ์•Š์Œ.


4.2 Redisson

๊ธฐ์กด Observer ํŒจํ„ด๊ณผ ๋น„์Šทํ•˜๊ฒŒ ์ฑ„๋„์„ ๊ตฌ๋…ํ•œ ์ดํ›„ ๋‹ค๋ฅธ ์„ธ์…˜์ด Lock ์„ ํ•ด์ œํ•  ๊ฒฝ์šฐ ๊ด€๋ จ ์ด๋ฒคํŠธ์— ๋Œ€ํ•œ ์•Œ๋ฆผ์„ ์ฃผ๊ณ ,
๊ตฌ๋…ํ•œ ๋‹ค๋ฅธ ์„ธ์…˜์ด ํ•ด๋‹น ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„์„œ Lock ์„ ํš๋“ํ•˜๋Š” ๋ฐฉ์‹

Redis ๋ช…๋ น์–ด๋กœ ์•Œ์•„๋ณด์ž.

๊ตฌ๋…
127.0.0.1:6379> subscribe ch1
1) "subscribe"
2) "ch1"
3) (integer) 1

publish
127.0.0.1:6379> publish ch1 hello
(integer) 1
127.0.0.1:6379>

๊ตฌ๋…
1) "message"
2) "ch1"
3) "hello"

pub_sub ๊ธฐ๋ฐ˜์ด๋ฏ€๋กœ Lettuce ๋ณด๋‹ค Redis ๋ถ€ํ•˜๊ฐ€ ์ค„์–ด๋“ ๋‹ค, Redisson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ด๋ฏธ Lock ํš๋“ ๋ฐ ํ•ด์ œ๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” RedisRepository ๋Š” ํ•„์š” ์—†๋‹ค

@Service
class InventoryRedissonLockService(
    private val redissonClient: RedissonClient,
    private val inventoryUpdater: InventoryUpdater,
) {
    private val log = logger()

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateQty(id: Long) {
        val lock = redissonClient.getLock(id.toString())

        try {
            val enabled = lock.tryLock(10, 1, TimeUnit.SECONDS)

            if (!enabled) {
                log.info("Redis Lock ํš๋“ ์‹คํŒจ Key : {}", id)
                return
            }

            inventoryUpdater.updateQty(id)
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        } finally {
            lock.unlock()
        }
    }
}

๋น„๊ต์  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด๋‚˜ ๊ด€๋ จ๋œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์—†์–ด์„œ ๊ฐ„๋‹จํ•˜๋‹ค.
ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋Š” ์ •์ƒ ์ž‘๋™ํ•˜๋ฉฐ ๊ตฌํ˜„ ๋˜ํ•œ ๊ฐ„๋‹จํ•˜๋‹ค

๊ทธ๋Ÿฌ๋‚˜ ๋ฝ ํ•ด์ œ ์‹คํŒจ ๋ฌธ์ œ, ๋ฝ์ด ํš๋“ํ•˜์ง€ ๋ชปํ–ˆ์„ ๊ฒฝ์šฐ ์ž๋™ ํ•ด์ œ ์‹œ๊ฐ„ ์„ค์ • ๋“ฑ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์š”์ธ๋“ค์„ ์ƒ๊ฐํ•ด์•ผ ํ•œ๋‹ค ๊ทธ๋ฆฌ๊ณ  Redis ๋ผ๋Š” ์™ธ๋ถ€ ์ž์›์„ ํ™œ์šฉํ•˜๋Š” ๋งŒํผ ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•˜๋Š” ์ด์Šˆ๋“ค์ด ์ƒ๊ฒจ๋‚  ์ˆ˜๋„ ์žˆ๋‹ค

์ด์™ธ์—๋„ ๋ฉ”์‹œ์ง€ ํ๋ฅผ ํ™œ์šฉํ•œ ๋™์‹œ์„ฑ ์ œ์–ด ๋“ฑ์ด ์กด์žฌํ•œ๋‹ค

์ถ”๊ฐ€๋กœ K6 ๋ฅผ ํ™œ์šฉํ•˜๋ฉด์„œ ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๋Š”๋ฐ ์ข€ ์ฐพ์•„๋ณด๋‹ˆ TPS (Transaction Per Seconds) ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋„ ๊ฐ€๋Šฅํ•ด ๋ณด์ธ๋‹ค

  • ๋™์‹œ ์š”์ฒญ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋Ÿ‰ ์ธก์ •

๊ฐ„๋‹จํžˆ ์—ฌํƒœ๊นŒ์ง€์˜ ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋Œ๋ ค์„œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด,

execution: local
        script: script.js
        output: -

     scenarios: (100.00%) 1 scenario, 10 max VUs, 40s max duration (incl. graceful stop):
              * simultaneous_requests: 1 iterations for each of 10 VUs (maxDuration: 10s, gracefulStop: 30s)


     data_received..................: 1.6 kB 1.3 kB/s
     data_sent......................: 1.2 kB 934 B/s
     http_req_blocked...............: avg=1.87ms   min=1.82ms   med=1.85ms   max=2.02ms p(90)=1.92ms   p(95)=1.97ms
     http_req_connecting............: avg=618.8ยตs  min=553ยตs    med=615.49ยตs max=704ยตs  p(90)=675.2ยตs  p(95)=689.6ยตs
     http_req_duration..............: avg=766.22ms min=292.99ms med=761.23ms max=1.23s  p(90)=1.14s    p(95)=1.18s
       { expected_response:true }...: avg=766.22ms min=292.99ms med=761.23ms max=1.23s  p(90)=1.14s    p(95)=1.18s
     http_req_failed................: 0.00%  โœ“ 0        โœ— 10
     http_req_receiving.............: avg=77.89ยตs  min=59ยตs     med=68.5ยตs   max=131ยตs  p(90)=110.3ยตs  p(95)=120.65ยตs
     http_req_sending...............: avg=293.3ยตs  min=177ยตs    med=316ยตs    max=366ยตs  p(90)=346.19ยตs p(95)=356.1ยตs
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s     p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=765.85ms min=292.64ms med=760.96ms max=1.23s  p(90)=1.14s    p(95)=1.18s
     http_reqs......................: 10     8.050875/s
     iteration_duration.............: avg=770.42ms min=297.84ms med=765.39ms max=1.24s  p(90)=1.14s    p(95)=1.19s
     iterations.....................: 10     8.050875/s
     vus............................: 3      min=3      max=3
     vus_max........................: 10     min=10     max=10

์—ฌ๊ธฐ์˜ http_reqs ๋ฅผ ๋ณด๋ฉด 10๊ฐœ์˜ ์š”์ฒญ์ด ๋“ค์–ด๊ฐ”๊ณ  1์ดˆ์— 8๋ฒˆ์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค


ํšŒ๊ณ  ๋ฐ ์ •๋ฆฌ

  • Pessimistic Lock : ๊ตฌํ˜„์€ ๊ฐ„๋‹จํ•˜๋ฉฐ ๊ฐ•๋ ฅํ•œ ๋ฝ์„ ํ†ตํ•ด ๋™์‹œ์„ฑ ์ œ์–ด ๊ทธ๋Ÿฌ๋‚˜ ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฐ ๋ณ‘๋ชฉํ˜„์ƒ ๋ฐœ์ƒ
  • Optimistic Lock : ํŠธ๋žœ์žญ์…˜ ๋ณ‘๋ ฌ์„ฑ ํ™•๋ณด ๋ฐ ์ถฉ๋Œ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ๋ณต์žกํ•˜๋‹ค
  • Named Lock : ๊ฐ„๋‹จํ•œ DB ๋ฝ์„ ํ†ตํ•œ ๊ตฌํ˜„ ๋ฝ ์ˆ˜๋™ํ•ด์ œ ํ•„์š” ๋ฐ ์ปค๋„ฅ์…˜์„ ์ ์œ ํ•จ
  • Redis Lettuce : ์‰ฌ์šด ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„, Redis ๋ถ€ํ•˜ ๋ฐ SpinLock ๊ตฌ์กฐ
  • Redis Redisson : ์•ˆ์ •์ ์ธ ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„, ์™ธ๋ถ€ ์˜์กด์„ฑ ๊ณผ ์„ค์ •์ด ํ•„์š”
profile
๊ฐœ๋ฐœ์ž

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด