@Component @RequiredArgsConstructor public class StockLockService { private final RedisTemplate<String, String> redisTemplate; private final StockService stockService; public void decrease(Long id) throws InterruptedException { // Redis 명령어에서 'SET [key] [value] NX EX [seconds]'와 동일 // key : stock_lock:{id} // value : lock // timeout : 3초 (락을 획득하고 3초 뒤에는 자동으로 소멸 -> 데드락 방지) String key = "stock_lock:" + id; // 락 획득 시도 (락 획득에 실패하면 100ms 대기 후 재시도) while (!tryLock(key)) { Thread.sleep(100); } try { stockService.decrease(id); } finally { // 락 해제 redisTemplate.delete(key); } } // 락 획득 시도 (성공시 true, 실패시 false를 return) // (Redis 명령어에서 'SET [key] [value] NX EX [seconds]'와 동일) // (key : stock_lock:{id}) // (value : lock) // (timeout : 3초 => 락을 획득하고 3초 뒤에는 자동으로 소멸함으로써 데드락 방지) private boolean tryLock(String key) { return redisTemplate .opsForValue() .setIfAbsent(key, "lock", Duration.ofMillis(3_000)); } }
@RestController @RequestMapping("/stocks") @RequiredArgsConstructor public class StockController { private final StockService stockService; private final StockLockService stockLockService; ... @PostMapping("/{id}/decrease/redis") public void decreaseWithRedis( @PathVariable Long id ) throws InterruptedException { stockLockService.decrease(id); } }


import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { // 가상 유저(VUs) 100명으로 설정 vus: 100, // 테스트를 10초 동안 진행 duration: '10s', }; export default function () { // 재고 차감 API const url = 'http://localhost:8080/stocks/1/decrease/redis'; const params = { headers: { 'Content-Type': 'application/json', }, }; // POST 요청 전송 const res = http.post(url, null, params); // 1초 간격으로 요청 발송 sleep(1); // 응답 상태 코드 확인 (200 OK가 왔는 지 체크) check(res, { 'status is 200': (r) => r.status === 200, }); }
$ k6 run scripts/script_2-2.js

