From 0f341b2f1d4ec0963fcbd0a24e1fb81a0e6a3782 Mon Sep 17 00:00:00 2001 From: LeeDongHoon Date: Mon, 9 Mar 2026 15:06:01 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EB=B6=84=EC=82=B0=20=EB=9D=BD=20?= =?UTF-8?q?=ED=99=80=EB=94=A9=20=EC=8B=9C=EA=B0=84=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=B9=84=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EB=9D=BD=20=EB=B9=84=EA=B5=90=20API=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DistributedLockService에 [LOCK_HELD], [LOCK_TIMEOUT], [LOCK_INTERRUPTED] 구조화 로그 추가 - 비관적 락 비교 테스트를 위한 POST /api/reservations/pessimistic 엔드포인트 추가 - k6 스트레스 테스트 스크립트에 429 상태코드 정상 거부 처리 반영 - 분산 락 도입 Before/After 스트레스 테스트 리포트 추가 - Logback + Loki 로깅 시스템 구축 계획 문서 추가 Co-Authored-By: Claude Opus 4.6 --- docs/logging-system-plan.md | 258 ++++++++++++++++++ ...s-test-report-distributed-lock-20260309.md | 177 ++++++++++++ k6/reservation-stress-test.js | 6 +- .../repository/EventScheduleRepository.java | 10 + .../controller/ReservationController.java | 6 + .../service/ReservationService.java | 19 ++ .../global/lock/DistributedLockService.java | 8 +- 7 files changed, 480 insertions(+), 4 deletions(-) create mode 100644 docs/logging-system-plan.md create mode 100644 docs/reports/stress-test-report-distributed-lock-20260309.md diff --git a/docs/logging-system-plan.md b/docs/logging-system-plan.md new file mode 100644 index 0000000..ef5387e --- /dev/null +++ b/docs/logging-system-plan.md @@ -0,0 +1,258 @@ +# Logback + Loki 로깅 시스템 구축 계획 + +> 작성일: 2026-03-09 +> 상태: 계획 + +--- + +## 1. 목표 + +애플리케이션 로그를 중앙 수집하여 Grafana에서 **메트릭(Prometheus)과 로그(Loki)를 통합 조회**할 수 있는 환경을 구축한다. + +### 현재 + +``` +Spring Boot → Logback → 파일/콘솔 출력 (서버 접속해서 tail -f로 확인) +Prometheus → Grafana (메트릭만 시각화) +``` + +### 목표 + +``` +Spring Boot → Logback → loki-logback-appender → Loki → Grafana (로그 시각화) +Spring Boot → Micrometer → Prometheus → Grafana (메트릭 시각화) +``` + +--- + +## 2. 기술 스택 + +| 구성 요소 | 역할 | 비고 | +|-----------|------|------| +| **Logback** | 애플리케이션 로깅 프레임워크 | Spring Boot 기본 내장 | +| **loki-logback-appender** | Logback → Loki 전송 | Grafana 공식 라이브러리 | +| **Loki** | 로그 수집·저장·검색 엔진 | Prometheus와 유사한 라벨 기반 | +| **Grafana** | 통합 대시보드 | 기존 Prometheus + Loki 데이터소스 추가 | + +--- + +## 3. 인프라 구성 + +``` +EC2 ① - 애플리케이션 서버 (t3.small) +├── Nginx (:80, :443) → 리버스 프록시, SSL 종료 +├── Spring Boot (:8080) → 애플리케이션 +│ ├─ Micrometer ──────────────────→ Prometheus (메트릭 scrape) +│ └─ loki-logback-appender ───────→ Loki (로그 HTTP push) +└── Redis (:6379) → 분산 락 + +EC2 ② - 모니터링 서버 +├── Prometheus (:9090) → 메트릭 수집·저장 +├── Loki (:3100) → 로그 수집·저장 +└── Grafana (:3000) → 통합 대시보드 + ├─ Prometheus 데이터소스 ← 메트릭 시각화 + └─ Loki 데이터소스 ← 로그 시각화 + +RDS MySQL (:3306) → 데이터 저장 +``` + +- 애플리케이션 서버 → 모니터링 서버: 내부 네트워크(VPC) 통신 +- Spring Boot → Loki: HTTP POST (loki-logback-appender) +- Prometheus → Spring Boot: HTTP GET scrape (/actuator/prometheus) + +--- + +## 4. 구현 단계 + +### Phase 1: Loki 설치 (모니터링 서버) + +모니터링 서버(EC2 ②)에 Loki 바이너리 설치 및 systemd 서비스 등록. + +```bash +# 바이너리 다운로드 +curl -O -L "https://github.com/grafana/loki/releases/latest/download/loki-linux-amd64.zip" +unzip loki-linux-amd64.zip + +# 설정 파일 생성 +# /etc/loki/loki-config.yml + +# systemd 서비스 등록 +sudo systemctl enable loki +sudo systemctl start loki +``` + +Loki 기본 설정: +- 포트: 3100 +- 스토리지: 로컬 파일시스템 (`/var/loki/`) +- 보존 기간: 7일 (디스크 절약) + +### Phase 2: Spring Boot 연동 + +**build.gradle 의존성 추가:** + +```groovy +implementation 'com.github.loki4j:loki-logback-appender:1.5.1' +``` + +**logback-spring.xml 생성:** + +```xml + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + http://{모니터링서버IP}:3100/loki/api/v1/push + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + +``` + +핵심 라벨: +- `app`: 애플리케이션 이름 +- `host`: 서버 호스트명 +- `level`: 로그 레벨 (INFO, WARN, ERROR) + +### Phase 3: 구조화된 로그 적용 + +주요 이벤트에 구조화된 로그를 추가하여 Loki에서 검색·필터링할 수 있도록 한다. + +**DistributedLockService — 락 실패 로그:** + +```java +log.warn("[LOCK_TIMEOUT] key={}, waitTime={}s", lockKey, WAIT_TIME_SECONDS); +``` + +**ReservationService — 예약 성공 로그:** + +```java +log.info("[RESERVATION_CREATED] scheduleId={}, reservationId={}", scheduleId, reservationId); +``` + +**GlobalExceptionHandler — 비즈니스 예외 로그:** + +```java +log.warn("[BIZ_EXCEPTION] type={}, status={}, message={}", + e.getClass().getSimpleName(), e.getHttpStatus(), e.getMessage()); +``` + +로그 태그 규칙: +- `[LOCK_TIMEOUT]` — 분산 락 대기 초과 +- `[LOCK_INTERRUPTED]` — 락 획득 중 인터럽트 +- `[RESERVATION_CREATED]` — 예약 생성 성공 +- `[RESERVATION_CANCELLED]` — 예약 취소 +- `[BIZ_EXCEPTION]` — 비즈니스 예외 (400, 409, 429 등) +- `[UNEXPECTED_ERROR]` — 예상치 못한 서버 에러 (500) + +### Phase 4: Grafana 대시보드 구성 + +**Loki 데이터소스 추가:** + +``` +Grafana → Configuration → Data Sources → Add → Loki +URL: http://{모니터링서버IP}:3100 +``` + +**대시보드 패널 구성:** + +| 패널 | LogQL 쿼리 | 용도 | +|------|-----------|------| +| 429 발생 추이 | `rate({app="form-pass"} \|= "[LOCK_TIMEOUT]" [5m])` | 락 실패 빈도 | +| 에러 로그 | `{app="form-pass", level="ERROR"}` | 서버 에러 실시간 확인 | +| 예약 성공 추이 | `rate({app="form-pass"} \|= "[RESERVATION_CREATED]" [5m])` | 예약 처리량 | +| 비즈니스 예외 | `{app="form-pass"} \|= "[BIZ_EXCEPTION]"` | 400/409/429 상세 | + +**메트릭 + 로그 연동 예시:** + +``` +Grafana 대시보드 +├── 상단: Prometheus — HTTP 요청 응답시간 그래프 +├── 중단: Prometheus — DB 커넥션 풀 사용량 +└── 하단: Loki — 같은 시간대의 에러 로그 +→ "p95가 튄 시점"을 클릭하면 해당 시간대 로그가 바로 보임 +``` + +### Phase 5: 알림 설정 + +**Grafana Alert Rules:** + +| 알림 조건 | 기준 | 알림 채널 | +|-----------|------|----------| +| 429 급증 | 5분간 [LOCK_TIMEOUT] 50건 초과 | Slack / Email | +| 5xx 발생 | 1분간 ERROR 로그 10건 초과 | Slack / Email | +| 로그 수집 중단 | 5분간 로그 0건 | Slack / Email | + +--- + +## 5. 리소스 예상 + +**애플리케이션 서버 (EC2 ①):** + +| 구성 요소 | 예상 메모리 | +|-----------|-----------| +| loki-logback-appender | ~5MB (Spring Boot 내) | + +애플리케이션 서버에는 appender만 추가되므로 부담 없음. + +**모니터링 서버 (EC2 ②):** + +| 구성 요소 | 예상 메모리 | +|-----------|-----------| +| Loki | 50~100MB | + +Loki 로그 보존 기간을 7일로 설정하여 디스크 사용도 제한. + +--- + +## 6. 파일 변경 요약 + +| 작업 | 파일 | +|------|------| +| CREATE | `src/main/resources/logback-spring.xml` | +| MODIFY | `build.gradle` (loki-logback-appender 의존성) | +| MODIFY | `DistributedLockService.java` (구조화된 로그 추가) | +| MODIFY | `GlobalExceptionHandler.java` (구조화된 로그 추가) | +| MODIFY | `ReservationService.java` (예약 성공/취소 로그 추가) | +| CREATE | 모니터링 서버: `/etc/loki/loki-config.yml` | +| CREATE | 모니터링 서버: `/etc/systemd/system/loki.service` | +| MODIFY | Grafana: Loki 데이터소스 추가 + 대시보드 패널 | + +--- + +## 7. 검증 + +```bash +# 1. Loki 동작 확인 +curl http://{모니터링서버IP}:3100/ready +# → ready + +# 2. Spring Boot 실행 후 로그 수집 확인 +curl -G "http://{모니터링서버IP}:3100/loki/api/v1/query" \ + --data-urlencode 'query={app="form-pass"}' | jq + +# 3. Grafana에서 Loki 쿼리 실행 +# Explore → Loki → {app="form-pass", level="WARN"} + +# 4. 부하 테스트 후 429 로그 확인 +# {app="form-pass"} |= "[LOCK_TIMEOUT]" +``` diff --git a/docs/reports/stress-test-report-distributed-lock-20260309.md b/docs/reports/stress-test-report-distributed-lock-20260309.md new file mode 100644 index 0000000..6fa5f64 --- /dev/null +++ b/docs/reports/stress-test-report-distributed-lock-20260309.md @@ -0,0 +1,177 @@ +# 스트레스 테스트 리포트 — 분산 락 도입 Before/After 비교 + +> 테스트일: 2026-03-09 +> 환경: EC2 t3.small (2vCPU, 2GB) + Redis (같은 인스턴스) +> 관련: ADR-003 + +--- + +## 1. 테스트 목적 + +비관적 락(`SELECT FOR UPDATE`)을 Redisson 분산 락으로 전환한 후, +500 VU 스트레스 테스트에서 **서버 에러율과 응답시간이 개선되었는지** 검증한다. + +--- + +## 2. 테스트 조건 + +| 항목 | 값 | +|------|-----| +| 스크립트 | `k6/reservation-stress-test.js` | +| VU 단계 | 100 → 200 → 300 → 500 (각 30초 유지) | +| 총 시간 | 2분 20초 | +| 대상 API | `POST /api/reservations` | +| 대상 스케줄 | 단일 스케줄 (scheduleId: 1, maxCapacity: 10,000+) | +| sleep | 0.5초 (요청 간 간격) | + +### 성공/실패 판정 기준 + +| HTTP 상태 | 분류 | 의미 | +|-----------|------|------| +| 201 | 성공 | 예약 생성 완료 | +| 400, 409 | 정상 거부 | 중복 예약, 정원 초과 등 비즈니스 규칙에 의한 거부 | +| 429 | 정상 거부 | 분산 락 대기 초과 (5초 내 락 획득 실패) | +| 5xx | **서버 에러** | DB 커넥션 풀 고갈, 타임아웃 등 시스템 장애 | + +--- + +## 3. Before/After 비교 + +### 환경 차이 + +| | Before | After | +|---|---|---| +| 인스턴스 | t2.micro (1vCPU, 1GB) | t3.small (2vCPU, 2GB) | +| 락 방식 | 비관적 락 (SELECT FOR UPDATE) | Redisson 분산 락 (Redis) | +| 커넥션 풀 | HikariCP 30 | HikariCP 30 | +| Redis | 없음 | localhost:6379 (maxmemory 50mb) | + +### 핵심 지표 비교 + +| 지표 | Before (비관적 락) | After (분산 락) | 변화 | +|------|-------------------|----------------|------| +| **서버 에러율 (5xx)** | **19.09%** | **0.00%** | **완전 해소** | +| 예약 성공 | 4,970건 | 6,509건 | +30.9% | +| 정상 거부 (409/429) | - | 209건 (3.11%) | - | +| 처리량 | ~36 req/s | 44.14 req/s | +22.6% | +| 총 요청 | 6,143건 | 6,719건 | +9.3% | + +### 응답시간 비교 + +| 지표 | Before | After | 변화 | +|------|--------|-------|------| +| 평균 | 3.14s | 5.42s | +2.28s | +| p90 | - | 9.91s | - | +| p95 | 8.21s | 10.65s | +2.44s | +| p99 | 11.72s | 10.72s | -1.00s | +| 최대 | 30.00s (타임아웃) | 11.44s | **-18.56s** | + +--- + +## 4. 분석 + +### 4-1. 서버 에러율: 19.09% → 0.00% + +**가장 중요한 개선.** 비관적 락에서 발생하던 5xx 에러가 완전히 사라졌다. + +원인: +- 비관적 락: 락 대기 중 DB 커넥션 점유 → 커넥션 풀 고갈 → 타임아웃 → 5xx +- 분산 락: 락 대기를 Redis가 담당 → DB 커넥션은 트랜잭션 처리 시간만 점유 + +``` +[Before — 비관적 락] +500 VU 동시 요청 → 30개 커넥션 점유 + 470개 대기 → 커넥션 풀 고갈 → 5xx + +[After — 분산 락] +500 VU 동시 요청 → Redis에서 순차 대기 → 락 획득한 요청만 DB 커넥션 사용 +→ 커넥션 풀 고갈 없음 → 5xx 0건 +``` + +### 4-2. 예약 성공: 4,970건 → 6,509건 (+30.9%) + +5xx로 실패하던 요청이 정상 처리되면서 성공 건수가 증가했다. + +### 4-3. 정상 거부 209건 (3.11%) + +429(락 대기 초과)와 409(중복 예약/정원 초과)를 합산한 수치. +시스템이 과부하 상황에서 **에러 대신 명확한 거부 응답**을 반환하는 것이므로 정상 동작이다. + +### 4-4. p95 응답시간: 8.21s → 10.65s + +p95가 다소 증가했으나, 이는 다음 요인에 의한 것: + +1. **단일 스케줄 집중**: 500 VU가 같은 스케줄 1개에 몰리면 락이 순차 처리됨 +2. **Before에서는 5xx로 빠르게 실패**: 커넥션 풀 고갈 시 타임아웃으로 즉시 실패 → 응답시간 통계에서 제외 +3. **After에서는 모든 요청이 처리됨**: 락 대기 후 성공까지 가므로 전체 응답시간이 길어짐 + +즉, **Before의 낮은 p95는 "빠르게 실패한 것"이지 "빠르게 처리한 것"이 아니다.** +After에서 p95가 높아진 것은 **이전에 실패하던 요청까지 성공적으로 처리**하기 때문이다. + +### 4-5. 실제 운영 환경에서의 예상 성능 + +이 테스트는 **최악의 시나리오**(500명이 동일한 스케줄 1개에 동시 예약)를 재현한 것이다. +실제 운영에서는: + +- 여러 이벤트, 여러 스케줄에 트래픽이 분산됨 +- 스케줄별로 별도의 락 키(`schedule:{id}`)를 사용하므로 락 경합이 줄어듦 +- p95 응답시간은 이보다 크게 낮아질 것으로 예상 + +--- + +## 5. Before 상세 데이터 (비관적 락) + +> 출처: `docs/reports/stress-test-report-20260307.md` + +| 지표 | 커넥션 풀 10 | 커넥션 풀 30 | +|------|-------------|-------------| +| 에러율 | 13.82% | 19.09% | +| p95 응답시간 | 11.31s | 8.21s | +| 예약 성공 | 4,691건 | 4,970건 | +| 총 요청 | 5,445건 | 6,143건 | + +--- + +## 6. After 상세 데이터 (분산 락) + +``` +CUSTOM +reservation_created...........: 6509 42.76/s +reservation_duration..........: avg=5426ms min=32ms med=5449ms max=11442ms + p(90)=9911ms p(95)=10654ms p(99)=10728ms +reservation_rejected..........: 209 1.37/s +server_error_rate.............: 0.00% 0 out of 6718 + +HTTP +http_req_duration.............: avg=5.42s min=31ms med=5.44s max=11.44s + p(90)=9.91s p(95)=10.65s p(99)=10.72s +http_req_failed...............: 3.11% 209 out of 6719 +http_reqs.....................: 6719 44.14/s + +EXECUTION +iterations....................: 6718 44.14/s +vus_max.......................: 500 +``` + +--- + +## 7. 결론 + +| 목표 | 결과 | +|------|------| +| 서버 에러율 해소 | **달성** — 19.09% → 0.00% | +| 초과 예약 방지 | **달성** — 정합성 유지 | +| 처리량 유지/개선 | **달성** — 36 → 44 req/s (+22.6%) | + +분산 락 도입으로 **DB 커넥션 풀 고갈 문제가 근본적으로 해결**되었다. +500 VU 환경에서 서버 에러 없이 안정적으로 예약을 처리할 수 있게 되었다. + +--- + +## 8. 후속 과제 + +| 과제 | 우선순위 | 설명 | +|------|---------|------| +| p95 응답시간 최적화 | 중 | 대기열 시스템 도입으로 락 대기 시간 단축 검토 | +| Redis 고가용성 | 낮 | 현재 단일 Redis → 서비스 확장 시 Sentinel 또는 ElastiCache 검토 | +| 다중 스케줄 부하테스트 | 중 | 실제 운영과 유사한 분산 트래픽 패턴에서 성능 검증 | +| ADR-001 상태 업데이트 | 낮 | `Superseded by ADR-003`으로 변경 | diff --git a/k6/reservation-stress-test.js b/k6/reservation-stress-test.js index 775c89e..ff15a1d 100644 --- a/k6/reservation-stress-test.js +++ b/k6/reservation-stress-test.js @@ -107,12 +107,12 @@ export default function () { if (res.status === 201) { reservationCreated.add(1); serverErrorRate.add(0); - } else if (res.status === 409 || res.status === 400) { - // 정원 초과, 중복 예약 — 시스템이 정상적으로 거부 + } else if (res.status === 409 || res.status === 400 || res.status === 429) { + // 정원 초과, 중복 예약, 락 대기 초과 — 시스템이 정상적으로 거부 reservationRejected.add(1); serverErrorRate.add(0); } else { - // 5xx, 타임아웃 — 비관적 락 병목 + // 5xx, 타임아웃 — 서버 병목 serverError.add(1); serverErrorRate.add(1); console.log(`[VU ${__VU}] SERVER ERROR: ${res.status} - ${res.body}`); diff --git a/src/main/java/com/example/reservation_solution/api/event/repository/EventScheduleRepository.java b/src/main/java/com/example/reservation_solution/api/event/repository/EventScheduleRepository.java index 50cdf4b..493aec1 100644 --- a/src/main/java/com/example/reservation_solution/api/event/repository/EventScheduleRepository.java +++ b/src/main/java/com/example/reservation_solution/api/event/repository/EventScheduleRepository.java @@ -1,7 +1,17 @@ package com.example.reservation_solution.api.event.repository; import com.example.reservation_solution.api.event.domain.EventSchedule; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; public interface EventScheduleRepository extends JpaRepository, EventScheduleRepositoryCustom { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT es FROM EventSchedule es WHERE es.id = :id") + Optional findByIdWithLock(@Param("id") Long id); } diff --git a/src/main/java/com/example/reservation_solution/api/reservation/controller/ReservationController.java b/src/main/java/com/example/reservation_solution/api/reservation/controller/ReservationController.java index e81af86..625903d 100644 --- a/src/main/java/com/example/reservation_solution/api/reservation/controller/ReservationController.java +++ b/src/main/java/com/example/reservation_solution/api/reservation/controller/ReservationController.java @@ -32,6 +32,12 @@ public ResponseEntity createReservation(@Valid @RequestBody return ResponseEntity.status(HttpStatus.CREATED).body(response); } + @PostMapping("/pessimistic") + public ResponseEntity createReservationWithPessimisticLock(@Valid @RequestBody ReservationRequest request) { + ReservationResponse response = reservationService.createReservationWithPessimisticLock(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + @GetReservationDocs @GetMapping("/{id}") public ResponseEntity getReservation(@PathVariable Long id) { diff --git a/src/main/java/com/example/reservation_solution/api/reservation/service/ReservationService.java b/src/main/java/com/example/reservation_solution/api/reservation/service/ReservationService.java index d3d416d..a06ea58 100644 --- a/src/main/java/com/example/reservation_solution/api/reservation/service/ReservationService.java +++ b/src/main/java/com/example/reservation_solution/api/reservation/service/ReservationService.java @@ -26,6 +26,25 @@ public class ReservationService { private final FormQuestionRepository formQuestionRepository; private final EncryptionUtils encryptionUtils; + @Transactional + public ReservationResponse createReservationWithPessimisticLock(ReservationRequest request) { + EventSchedule schedule = eventScheduleRepository.findByIdWithLock(request.getScheduleId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 스케줄입니다.")); + String encryptedPhoneNumber = encryptionUtils.encrypt(request.getGuestPhoneNumber()); + checkDuplicateReservation(request.getScheduleId(), encryptedPhoneNumber); + schedule.incrementReservedCount(request.getTicketCount()); + Reservation reservation = Reservation.create( + schedule, + request.getGuestName(), + encryptedPhoneNumber, + request.getTicketCount() + ); + processFormAnswers(request, reservation); + validateRequiredQuestions(reservation, schedule); + Reservation savedReservation = reservationRepository.save(reservation); + return ReservationResponse.from(savedReservation, encryptionUtils); + } + @Transactional public ReservationResponse createReservation(ReservationRequest request) { EventSchedule schedule = loadEventScheduleOrThrow(request.getScheduleId()); diff --git a/src/main/java/com/example/reservation_solution/global/lock/DistributedLockService.java b/src/main/java/com/example/reservation_solution/global/lock/DistributedLockService.java index a5b5fd8..e16940f 100644 --- a/src/main/java/com/example/reservation_solution/global/lock/DistributedLockService.java +++ b/src/main/java/com/example/reservation_solution/global/lock/DistributedLockService.java @@ -24,11 +24,17 @@ public T executeWithLock(String lockKey, Supplier supplier) { try { boolean acquired = lock.tryLock(WAIT_TIME_SECONDS, TimeUnit.SECONDS); if (!acquired) { + log.warn("[LOCK_TIMEOUT] key={}, waitTime={}s", lockKey, WAIT_TIME_SECONDS); throw LockAcquisitionException.timeout(); } - return supplier.get(); + long start = System.currentTimeMillis(); + T result = supplier.get(); + long elapsed = System.currentTimeMillis() - start; + log.info("[LOCK_HELD] key={}, duration={}ms", lockKey, elapsed); + return result; } catch (InterruptedException e) { Thread.currentThread().interrupt(); + log.warn("[LOCK_INTERRUPTED] key={}", lockKey); throw LockAcquisitionException.interrupted(); } finally { if (lock.isHeldByCurrentThread()) {