Post

동시성 제어

동시성 제어

동시성 이슈

동시성 이슈는 여러 스레드나 프로세스가 동일한 자원(예: 데이터베이스, 파일, 메모리)에 동시에 접근하여 작업을 수행할 때 발생하는 문제를 의미합니다.

이러한 상황에서 자원의 일관성(consistency)이나 무결성(integrity)이 훼손될 수 있으며, 결과적으로 서비스 오류나 성능 저하를 초래할 수 있습니다.

크게 아래와 같은 이슈가 발생할 수 있습니다.

Race Condition (경쟁 상태):

  • 여러 스레드가 동시에 공유 자원에 접근하여 값을 변경하려고 할 때 발생합니다. 스레드들이 자원의 상태를 예상하지 못하게 변경하여 예기치 않은 동작을 유발할 수 있습니다.
  • 예시: 두 스레드가 같은 변수에 값을 더하는 작업을 동시에 수행하면, 마지막 값이 두 값의 합이 아닌 다른 값이 될 수 있습니다.

Deadlock (교착 상태):

  • 두 개 이상의 스레드가 서로 상대방이 소유한 자원을 기다리면서, 영원히 진행되지 않는 상태입니다. 각 스레드는 다른 스레드가 점유한 자원을 요청하며, 서로 기다리면서 실행이 멈추게 됩니다.
  • 예시: 스레드 A는 자원 1을, 스레드 B는 자원 2를 점유하고, 두 스레드가 각각 다른 자원을 기다리면 교착 상태가 발생합니다.

Lost Update (갱신 손실):

  • 두 개 이상의 스레드가 동시에 동일한 데이터를 갱신하려고 할 때, 하나의 업데이트가 다른 업데이트에 의해 덮어씌워져 손실되는 현상입니다.
  • 예시: 두 사용자가 같은 계좌에 입금 작업을 동시에 수행할 때, 첫 번째 사용자의 입금 작업 결과가 두 번째 사용자의 입금 작업에 의해 덮어씌워질 수 있습니다.

Dirty Read (더티 리드):

  • 하나의 스레드가 다른 스레드가 아직 커밋하지 않은 데이터를 읽을 때 발생합니다. 읽은 데이터는 변경되거나 롤백될 수 있기 때문에 잘못된 결과를 초래할 수 있습니다.
  • 예시: 스레드 A가 데이터베이스에 데이터를 삽입하고 커밋하기 전에, 스레드 B가 그 데이터를 읽으면 더티 리드가 발생할 수 있습니다.

동시성이 발생할 수 있는 시나리오

위에서 정의한 동시성 이슈 관련 예시 시나리오 입니다.

본 프로젝트에서 동시성 이슈가 발생하는 시나리오는 5. 프로젝트에서의 적용 방안에 정의되어있습니다.

  1. 은행 계좌 이체
    • A 사용자가 자신의 계좌에서 100만 원을 출금하는 동안, B 사용자가 동시에 동일한 계좌에서 50만 원을 출금하려고 시도.
    • 동시성 제어가 제대로 이루어지지 않으면 계좌 잔고가 음수가 되는 문제가 발생할 수 있음.
  2. 재고 관리 시스템
    • 여러 고객이 동시에 특정 상품을 구매하려고 할 때, 재고 수량이 적절히 감소하지 않거나 초과 주문이 발생.
    • 이는 재고 부족 또는 잘못된 재고 정보로 이어질 수 있음.

동시성 제어의 필요성

  • 왜 동시성 제어가 필요한가?
    여러 스레드 또는 프로세스가 동시에 같은 자원(예: 데이터베이스, 파일)에 접근할 때 발생할 수 있는 문제(경쟁 상태, 데이터 불일치)를 방지하기 위함입니다.
    예를 들어 하나의 자원에 여러 사용자가 동시에 쓰기 작업을 수행하는 경우 데이터 무결성이 깨질 수 있고, 동일한 작업이 중복으로 수행되어 잘못된 결과가 저장될 수 있습니다.
    예를 들어서 포인트 충전 및 이용에 대해서 순차처리를 해야한다면 동시성 제어가 필요합니다.

    순차처리를 해야한다면 큐를 사용해도 되지 않나요??
    순차처리를 해야한다면 큐를 사용해도 된다. 그렇지만, 큐를 사용하게 된다면, application 성능 저하가 발생한다.
    예를 들어서 포인트 충전을 순차처리하기 위해서 큐를 사용한다면 모든 유저의 포인트 충전은 단일 큐에 몰리고 결국 순차처리는 되지만, 성능 저하가 발생한다.
    그렇다면 유저별로 순차처리를 해야한다면? 유저별로 큐가 존재해야하는가?를 생각해보자.
    “락”을 건다면 락을 획득한 순서대로 처리가 된다.
    순차처리라는 단어에 매몰되서 큐만 떠올리지 말자..

동시성 제어 방안

동시성 제어 방안에 대해서 락(비관적락, 낙관적락, 분산락, 네임드락)과 CAS, MVCC에 대해 설명합니다.

1. 비관적 락(Pessimistic Locking)

비관적 락은 자원에 접근하려는 트랜잭션이 충돌을 피하기 위해 자원을 잠금 상태로 유지하는 방식입니다. 이 접근 방식에서는 자원이 잠겨 있는 동안 다른 트랜잭션이 동일한 자원에 접근하지 못합니다.

충돌이 발생할 가능성이 높다고 가정하고, 먼저 락을 획득하여 다른 작업의 접근을 제한합니다.

트랜잭션 범위 내에서만 유효합니다.

일반적으로 InnoDB 스토리지 엔진의 트랜잭션에서 사용되며, SELECT … FOR UPDATE 나 LOCK IN SHARE MODE 구문으로 구현.

LockModeType.PESSIMISTIC_WRITE을 사용해 쓰기 락을 걸어 다른 트랜잭션이 읽거나 쓰는 것을 방지합니다.

  • 예시 : SQL의 SELECT ... FOR UPDATE 문은 데이터 조회 시 락을 설정하여 다른 트랜잭션이 동일한 데이터에 접근하거나 변경하지 못하도록 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ProductRepository : CrudRepository<Product, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE) // 비관적 락 적용
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    fun findByIdWithLock(id: Long): Product?
}

@Service
class ProductService(private val productRepository: ProductRepository) {
    @Transactional
    fun updateProductWithLock(productId: Long, updatedQuantity: Int) {
        val product = productRepository.findByIdWithLock(productId)
            ?: throw IllegalArgumentException("Product not found")
        product.quantity = updatedQuantity
        // 트랜잭션 종료 시 락 해제
    }
}
  • 장점
    • 정합성 보장: 충돌을 방지하므로 데이터 정합성을 강력하게 유지할 수 있습니다.
    • 단순한 구조: 구현과 사용 방법이 직관적입니다.
  • 단점
    • 성능 저하: 트랜잭션이 락을 유지하는 동안 다른 트랜잭션은 대기해야 하므로 시스템 성능이 저하될 수 있습니다.
    • 데드락 발생 위험: 여러 트랜잭션이 서로의 자원을 기다리며 데드락이 발생할 수 있습니다.

비관적 락을 걸면 아예 조회가 안되나요??
비관적락 거는 경우 select * from for update는 안되지만 select * from 은 조회가 가능합니다.(이전 버전을 읽음)
“이전버전을 읽는다”는 말은 이전 버전 읽기를 지원한다는 것이며, mvcc를 지원하는 DB를 의미합니다.
update 구문은 배타락이 default기 때문에 동시성이 발생하지 않으나 조회 후 업데이트인 경우(갱신손실문제인 경우)는 발생할 수 있습니다. ex) 좋아요 이런것들

주의사항
비괁거 락의 경우는 인덱스 갯수 자체가 6개 이상이면 체감상 매우 성능이 낮아집니다.
여기서 말하는 인덱스 갯수는 컬럼 6개를 하나의 인덱스로 묶는 개념이 아니라 테이블에 걸려있는 인덱스의 총 갯수를 의미합니다.
(카디널리티가 높다 => 중복도가 낮다.)

2. 낙관적 락(Optimistic Locking)

낙관적 락은 충돌이 드물다고 가정하고, 자원 사용 후 충돌 여부를 검사하여 처리하는 방식입니다. 충돌이 감지되면 롤백하는 방식입니다.

일반적으로 버전 번호나 타임스탬프를 이용하여 데이터 변경 시점을 관리합니다.

  • 예시 : JPA에서 @Version 어노테이션을 사용하여 엔티티에 버전 필드를 추가하면, 데이터 업데이트 시 자동으로 버전 검증을 수행합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
data class Product(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val name: String,
    var quantity: Int,
    @Version // 버전 필드를 사용하여 낙관적 락 적용. 업데이트 시 버전 충돌이 발생하면 예외가 던져집니다.
    val version: Long? = null
)

@Service
class ProductService(private val productRepository: ProductRepository) {
    @Transactional
    fun updateProduct(productId: Long, updatedQuantity: Int) {
        val product = productRepository.findById(productId)
            ?: throw IllegalArgumentException("Product not found")
        product.quantity = updatedQuantity
        productRepository.save(product) // JPA가 자동으로 version 필드를 검사
    }
}
  • 장점
    • 높은 동시성 처리량: 락을 사용하지 않으므로 자원 경합 없이 높은 동시성을 처리할 수 있습니다.
    • 데드락 없음: 락을 설정하지 않으므로 데드락이 발생하지 않습니다.
  • 단점
    • 충돌 발생 시 롤백 필요: 데이터 충돌이 발생하면 작업을 중단하고 롤백해야 합니다.
    • 재시도 로직 구현 필요: 충돌 발생 시 데이터 갱신을 재시도하기 위한 추가 로직이 필요합니다.

3. 분산 락 (Distributed Lock)

분산 락은 여러 노드에서 동일한 자원에 동시 접근을 제어하기 위한 메커니즘입니다. Redis, ZooKeeper, Etcd 등의 도구를 이용해 구현할 수 있습니다.

  • 예시 : Redis의 Redisson을 사용하여 특정 키를 기반으로 분산 락을 설정하고, 노드 간의 동기화를 보장합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// redis 사용 예시
@Service
class RedisLockService(private val redisTemplate: StringRedisTemplate) {
    fun acquireLock(lockKey: String, leaseTime: Long): Boolean {
        val result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "locked", Duration.ofSeconds(leaseTime))
        return result == true
    }

    fun releaseLock(lockKey: String) {
        redisTemplate.delete(lockKey)
    }
}

@Service
class ProductService(
    private val redisLockService: RedisLockService,
    private val productRepository: ProductRepository
) {
    private val lockKeyPrefix = "lock:product:"

    fun updateProductWithDistributedLock(productId: Long, updatedQuantity: Int) {
        val lockKey = "$lockKeyPrefix$productId"
        // 락 획득
        val acquired = redisLockService.acquireLock(lockKey, leaseTime = 10)

        if (!acquired) {
            throw IllegalStateException("Failed to acquire lock for product $productId")
        }

        try {
            val product = productRepository.findById(productId)
                ?: throw IllegalArgumentException("Product not found")

            product.quantity = updatedQuantity
            productRepository.save(product)
        } finally {
            // 락 해제
            redisLockService.releaseLock(lockKey)
        }
    }
}
  • 장점
    • 분산 환경에서 정합성 보장: 여러 서버에서 자원 접근을 효과적으로 관리할 수 있습니다.
    • 다양한 구현체 지원: Redis, ZooKeeper 등 다양한 오픈소스 라이브러리를 활용할 수 있습니다.
  • 단점
    • 네트워크 지연 및 장애 위험: 네트워크 문제나 서버 장애로 인해 락이 예상대로 해제되지 않을 수 있습니다.
    • 복잡한 구현: 단일 서버의 락보다 구현이 복잡하며, 추가적인 설계와 관리가 필요합니다.

4. 네임드 락(Named Lock)

네임드 락은 데이터베이스에서 제공하는 이름 기반의 락을 사용하여 동시성을 제어하는 방식입니다.

특정 이름(키)을 기준으로 락을 설정하여 동일한 자원에 대한 동시 접근을 차단합니다.

  • 예시 : MySQL의 GET_LOCKRELEASE_LOCK 함수를 사용하여 특정 작업에 대한 락을 설정.
1
2
3
4
5
-- 락 획득
SELECT GET_LOCK('lock_name', timeout_seconds);

-- 락 해제
SELECT RELEASE_LOCK('lock_name');
  • 예시 : Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class NamedLockService(private val jdbcTemplate: JdbcTemplate) {

    @Transactional
    fun executeWithLock(lockName: String, action: () -> Unit) {
        val acquired = jdbcTemplate.queryForObject(
            "SELECT GET_LOCK(?, 10)", Boolean::class.java, lockName
        )

        if (acquired == true) {
            try {
                action()
            } finally {
                jdbcTemplate.update("SELECT RELEASE_LOCK(?)", lockName)
            }
        } else {
            throw IllegalStateException("Failed to acquire lock: $lockName")
        }
    }
}

@Service
class BusinessService(private val namedLockService: NamedLockService) {
    fun updateSharedResource() {
        namedLockService.executeWithLock("shared_resource_lock") {
            // 공유 자원 업데이트 로직
        }
    }
}

5. CAS (Compare-And-Swap)

CAS는 값을 변경하기 전에 현재 값과 예상 값이 동일한지 확인 후 처리하는 방식입니다. 락을 사용하지 않고 동시성을 제어하기 때문에 높은 성능을 보장합니다.

CAS는 아래와 같은 흐름으로 원자성을 보장합니다.

  1. 현재 값(기대 값, expectedValue)을 읽어옴
  2. 메모리에서 실제 값(currentValue)과 기대 값(expectedValue)이 같은지 비교
  3. 같으면 새로운 값으로 변경하고 성공을 반환
  4. 다르면 실패하고, 다시 읽어와 재시도(반복적으로 실행)
  • 예시: Java의 AtomicInteger를 활용한 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.atomic.AtomicInteger

class CASExample {
    private val counter = AtomicInteger(0)

    fun increment() {
        var current: Int
        do {
            current = counter.get()
        } while (!counter.compareAndSet(current, current + 1))
    }

    fun getCounter(): Int {
        return counter.get()
    }
}

fun main() {
    val casExample = CASExample()

    // Increment the counter
    casExample.increment()
    println("Counter after increment: ${casExample.getCounter()}")
}
  • 장점:
    • 락 없이 구현 가능.
    • 높은 성능.
  • 단점:
    • 충돌 발생 시 재시도가 필요.
    • 구현 복잡성 증가.

6. 멀티버전 동시성 제어 (MVCC)

MVCC는 읽기 작업이 쓰기 작업을 차단하지 않도록 데이터를 다중 버전으로 유지하여 동시성을 보장합니다. 주로 데이터베이스에서 사용됩니다.

  • 작동 원리:
    • 쓰기 작업이 발생할 때마다 새로운 버전을 생성하여 기존 데이터를 유지.
    • 읽기 작업은 특정 시점의 데이터를 참조하여 동시성을 보장.
  • 장점:
    • 읽기-쓰기 동시성 보장.
    • 높은 성능.
  • 단점:
    • 데이터 저장소가 더 많은 공간 필요.
    • 쓰기 충돌 시 롤백 발생 가능.
  • 예시: PostgreSQL의 MVCC 사용 방식
1
2
3
4
5
6
7
8
9
10
11
-- 트랜잭션 시작
BEGIN;

-- 데이터 읽기
SELECT * FROM accounts WHERE id = 1;

-- 데이터 쓰기
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- 트랜잭션 종료
COMMIT;
  • 코드 예시 (Kotlin):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.transaction.annotation.Transactional
import org.springframework.stereotype.Service

@Service
class AccountService(private val accountRepository: AccountRepository) {

    @Transactional
    fun transferFunds(fromAccountId: Long, toAccountId: Long, amount: Double) {
        val fromAccount = accountRepository.findById(fromAccountId).orElseThrow()
        val toAccount = accountRepository.findById(toAccountId).orElseThrow()

        if (fromAccount.balance >= amount) {
            fromAccount.balance -= amount
            toAccount.balance += amount
            accountRepository.save(fromAccount)
            accountRepository.save(toAccount)
        } else {
            throw IllegalArgumentException("Insufficient funds")
        }
    }
}

락 전략 비교

락 종류특징사용 사례
낙관적 락데이터 충돌 시점에 검증. 성능이 중요하고 충돌 가능성이 낮은 경우 사용.(실패해도 됨.)게시판 수정, 수강신청
비관적 락데이터 읽기 시점부터 락을 걸어 충돌 방지. 정확성이 중요하고 충돌 가능성이 높은 경우 사용.(반드시 성공하길 원함.)재고 관리, 은행 계좌 이체
MySQL Named Lock사용자 정의 락으로 특정 리소스에 대해 락 적용. 트랜잭션 외부 리소스 동시성 제어 가능.리소스 단위의 고유 작업 실행
분산 락여러 노드에서 동시 접근 제어. Redis, ZooKeeper 등을 사용.분산 시스템의 주문 처리, 파일 생성 관리

동시성 제어시 고려사항

락을 걸시에 아래의 사항을 고려해야합니다.

  • 데드락 (Deadlock) 방지
    여러 락을 사용할 경우 락 획득 순서를 정하거나 타임아웃을 설정해 데드락을 방지해야 합니다.
    ex) tryLock 사용 시 시간 제한을 두어 데드락 방지.

    1
    2
    3
    4
    5
    6
    7
    
      if (lock.tryLock(5, TimeUnit.SECONDS)) { // 락 획득까지 대기하는 시간 설정
          try {
              // 작업 수행
          } finally {
              lock.unlock()
          }
      }
    
  • 락 타임아웃 (Timeout)
    락 획득 시 타임아웃과 임대 시간(leaseTime)을 적절히 설정해 불필요한 락 점유를 방지합니다.

  • 분산 환경에서의 락 보장
    분산 락을 사용할 때는 네트워크 장애, 서버 재시작 등의 상황에서 락이 정상적으로 해제될 수 있도록 보장해야 합니다.

임시

5. 구현 방식 설명

DB, 외부 infra를 사용 하지 않고, application level에서 동시성을 제어 하기 위해서는 임계영역전에 락을 획득합니다.

synchronized를 사용한다면 타임아웃을 설정할 수 없어 스레드가 무한정 락이 해제될 때까지 대기하는 데드락이 발생할 수 있기에 타임아웃을 지정할 수 있는 java.util.concurrent.locks.ReentrantLock 패키지에서 제공하는 ReentrantLock를 사용하여 구현하였습니다.

  • synchronizedReentrantLock 비교
특징synchronizedReentrantLock
락 획득/해제자동 관리명시적 관리 (lock()/unlock())
타임아웃지원하지 않음tryLock()으로 지원
인터럽트 처리지원하지 않음lockInterruptibly() 지원
공정성지원하지 않음공정성 설정 가능
조건 변수wait()/notify() 사용newCondition() 사용
재진입성지원지원
성능간단한 경우 성능 유리복잡한 상황에서 유리
  • 공정성 : 먼저 대기한 스레드가 반드시 먼저 락을 획득하는 것인지에 대한 여부
    ReentrantLock는 생성자에 true를 전달하면 FIFO 순서로 락을 획득하지만 synchronized는 먼저 대기한 스레드가 반드시 먼저 락을 획득하는 것은 아님(JVM이 관리)
1
val fairLock = ReentrantLock(true)
  • 재진입성 : 동일한 스레드가 이미 획득한 락을 다시 획득할 수 있는지에 대한 여부

5.1 구현 방식 코드

락을 수행하는 LockManager, @SyncLock 어노테이션을 통해 메서드 단위에서 락을 제어하는 방식으로 구현하였습니다.

또한 fun <T> lock(key: String, action: () -> T): T를 사용하여 메소드 단위가 아닌 특정 구간의 lock도 가능합니다.

deadlock을 방지하기위해 lock 획득 waitTime을 받아서 trylock을 시도합니다.

  • LockManager : 락을 획득하고 해제하는 manager 구현. userId를 키로 가지고 있는 ConcurrentHashMap를 활용하여 user별 로 관리.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
class LockManager {
    private val locks: MutableMap<String, ReentrantLock> = ConcurrentHashMap()

    fun <T> lock(key: String, timeout: Long, unit: TimeUnit, action: () -> T): T {
        val lock = locks.computeIfAbsent(key) { ReentrantLock() }
        val acquired = lock.tryLock(timeout, unit)
        if (!acquired) {
            throw IllegalStateException("Failed to acquire lock for key: $key within $timeout ${unit.name}")
        }
        try {
            return action()
        } finally {
            lock.unlock()
        }
    }

    fun lock(key: String, time: Long, unit: TimeUnit): Boolean {
        val lock = locks.computeIfAbsent(key) { ReentrantLock() }
        return lock.tryLock(time, unit)
    }

    fun unlock(key: String) {
        val lock = locks[key]
        if (lock != null && lock.isHeldByCurrentThread) {
            lock.unlock()
        }
    }
}
  • aspect : SyncLock 어노테이션에 대한 aspect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Around("@annotation(syncLock)")
fun aroundDistributedLock(joinPoint: ProceedingJoinPoint, syncLock: SyncLock): Any? {
    val lockKey = resolveLockKey(syncLock.key, joinPoint)

    val acquired = lockManager.lock(lockKey, syncLock.waitTime, syncLock.timeUnit)

    if (!acquired) {
        logger.error("lock 획득 실패: $lockKey")
        throw IllegalStateException("Failed to acquire lock: $lockKey within ${syncLock.waitTime} ${syncLock.timeUnit}")
    }

    return try {
        logger.info("lock 획득 성공: $lockKey")
        joinPoint.proceed()
    } finally {
        lockManager.unlock(lockKey)
        logger.info("lock 해제 성공: $lockKey")
    }
}
  • 실제 적용 코드 (PointCommand)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    @SyncLock(key = "#userId")
    fun chargePoint(userId: Long, amount: Long): UserPoint {
      val userPoint = userPointTable.selectById(userId)
      val updatedUserPoint = userPoint.increasePoints(amount)
    
      val savedUserPoint = userPointTable.insertOrUpdate(userId, updatedUserPoint.point)
      pointHistoryTable.insert(updatedUserPoint.id, amount, TransactionType.CHARGE, System.currentTimeMillis())
    
      return savedUserPoint
    }
    

6. 테스트 및 검증

아래의 시나리오대로 동시성 테스트를 진행하였습니다.

스레드를 만들고 지정된 횟수만큼 수행하여 성공횟수와 실패 횟수 및 포인트 조회를 통해 검증하는 절차로 진행하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
val latch = CountDownLatch(threadCount)
val executor: ExecutorService = Executors.newFixedThreadPool(10)
val successCount = AtomicInteger(0) // 성공 횟수 추적
val failureCount = AtomicInteger(0) // 실패 횟수 추적

// when
for (i in 1..threadCount) {
    executor.submit {
        try {
            pointCommand.usePoint(userId, amount)
            successCount.incrementAndGet()
        } catch (e: Exception) {
            failureCount.incrementAndGet()
        } finally {
            latch.countDown()
        }
    }
}

latch.await()
executor.shutdown()

executor: ExecutorService = Executors.newFixedThreadPool(10)

alt text

  1. 포인트 충전 동시성 테스트
  2. 포인트 사용 동시성 테스트
  3. 포인트 사용을 100건만 처리 할 수 있을 때, 포인트 사용 요청이 101건 들어오면 마지막 요청은 실패한다.
  4. 포인트 충전을 100건만 처리 할 수 있을 때(최대잔고에 도달할 경우), 포인트 사용 적립 101건 들어오면 마지막 요청은 실패한다.
This post is licensed under CC BY 4.0 by the author.