Spring Boot'ta ödeme, sipariş ve rezervasyon gibi yazma işlemlerini idempotent tasarlayın. PostgreSQL benzersiz kısıtları, Spring MVC filtreleri ve Redis kilidiyle yeniden deneme kaynaklı çift kayıtları önleyin.
Spring Boot'ta İdempotent REST API: Yarış Koşulu ve Hata Yönetimi
Spring REST API için idempotency sözleşmesini doğru tanımlamak
Bir spring rest api içinde POST isteğinin idempotent olması, aynı HTTP gövdesine sahip her isteğin otomatik olarak aynı sonuç üretmesi demek değildir. İstemci her denemede rastgele yeni bir anahtar üretirse sunucu bunları farklı iş emirleri sayar. İstemci, mantıksal işlem başına UUID veya ödeme sağlayıcısının işlem kimliğini Idempotency-Key başlığında üretmeli ve ağ zaman aşımında aynı anahtarla yeniden denemelidir. Anahtarın kapsamını kullanıcı veya tenant ile birleştirin: tek başına anahtar global benzersiz değilse başka bir kullanıcının önceden ürettiği değer yanlış yanıt döndürebilir.
POST /orders HTTP/1.1
Idempotency-Key: 018f6f91-7c42-7d23-8e1a-68b0f764d530
Content-Type: application/json
{"customerId":"c-42","items":[{"sku":"A1","quantity":2}]}Sözleşmeye isteğin kanonik gövde karmasını da ekleyin. Aynı anahtar ile farklı payload gelirse eski yanıtı dönmek veri kaybını gizler; bunun yerine 409 Conflict üretin. Jackson ile ağaç yapısını sıralı alanlarla serileştirip SHA-256 hesaplamak, salt ham byte dizisini karmalamaktan daha dayanıklıdır; çünkü JSON alan sırası semantik olarak anlamlı değildir. Ancak sayı biçimi gibi iş kuralı açısından anlamlı dönüşümleri ayrıca normalize etmeden kanonikleştirmek, 1 ve 1.0 için beklenmeyen çakışmalara yol açabilir.
Spring MVC filtresi yerine veritabanı sahipliği ile yarış koşulunu kapatmak
Tek düğümde çalışan bir Spring MVC filtresindeki ConcurrentHashMap, iki eşzamanlı isteği yakalayabilir; fakat pod yeniden başladığında durum kaybolur ve yatayda her pod farklı harita görür. Asıl karar noktası PostgreSQL'deki benzersiz indeks olmalıdır. idempotency_record satırı işlemin sahipliğini temsil eder: ilk istek satırı ekler, yarışan istek benzersiz kısıta takılır ve tamamlanmış sonucu okur.
create table idempotency_record (
principal_id varchar(128) not null,
operation varchar(64) not null,
idempotency_key uuid not null,
request_hash char(64) not null,
status varchar(16) not null,
response_code integer,
response_body jsonb,
created_at timestamptz not null default now(),
completed_at timestamptz,
primary key (principal_id, operation, idempotency_key)
);
create index idempotency_record_cleanup_idx
on idempotency_record (completed_at)
where status = 'COMPLETED';Servis katmanında kayıt ekleme ile sipariş yazımını aynı transaction içinde yapın. Önce INSERT ... ON CONFLICT DO NOTHING çalıştırın; eklenen satır sayısı 1 ise istek sahibi sizsiniz. 0 ise satırı SELECT ... FOR UPDATE ile okuyun. Kilit, ilk transaction tamamlanana kadar ikinci isteği bekletir; ikinci istek daha sonra kaydedilmiş HTTP durumunu ve gövdesini döndürür. REQUIRES_NEW ile idempotency kaydını ana iş transaction'ından ayırmak cazip görünür, fakat iş transaction'ı rollback olunca kalıcı bir COMPLETED kaydı üretme riski taşır.
@Transactional
public OrderResponse create(CreateOrderCommand cmd, UUID key, String principal) {
int inserted = jdbc.update("""
insert into idempotency_record(principal_id, operation, idempotency_key,
request_hash, status)
values (?, 'CREATE_ORDER', ?, ?, 'PROCESSING')
on conflict do nothing
""", principal, key, cmd.hash());
if (inserted == 0) {
var record = repository.lock(principal, "CREATE_ORDER", key);
if (!record.requestHash().equals(cmd.hash())) throw new KeyReuseException();
if (record.status().equals("COMPLETED")) return record.toResponse();
throw new RequestInProgressException();
}
Order order = orderRepository.save(Order.from(cmd));
OrderResponse response = OrderResponse.from(order);
repository.complete(principal, "CREATE_ORDER", key, 201, response);
return response;
}Spring Security ile anahtar kapsamı ve hata yanıtlarının korunması
spring security tarafında anahtarı sadece header'dan almak yeterli değildir. Kayıt anahtarındaki principal_id, JWT'deki kararlı sub veya servis hesapları için istemci kimliği olmalıdır. E-posta gibi değişebilen bir claim kullanılırsa hesap güncellemesi aynı işleme tekrar erişimi bozabilir. Controller'a doğrulanmış kimliği geçirip tenant bilgisini de benzersiz anahtara dahil edin.
@PostMapping("/orders")
ResponseEntity<OrderResponse> create(
@RequestHeader("Idempotency-Key") UUID key,
@AuthenticationPrincipal Jwt jwt,
@RequestBody @Valid CreateOrderRequest request) {
String tenant = jwt.getClaimAsString("tenant_id");
String principal = tenant + ":" + jwt.getSubject();
var result = orderService.create(request.toCommand(), key, principal);
return ResponseEntity.status(result.created() ? 201 : 200).body(result.body());
}İdempotency kaydı yalnızca başarılı 2xx yanıtları saklamamalıdır. Örneğin ödeme sağlayıcısına çağrı yapıldıktan sonra bağlantı koptuysa, sonraki denemede sağlayıcıya ikinci tahsilat göndermemek için sağlayıcının hata kodu, dış işlem kimliği ve yanıt gövdesi de saklanmalıdır. Buna karşılık bean validation ile oluşan 400 yanıtlarını kalıcılaştırmak çoğu sistemde yanlış seçimdir: istemci hatayı düzelttikten sonra aynı anahtarla geçerli bir istek göndermek isteyebilir. Bu kuralı endpoint bazında açıkça belgeleyin.
Microservices mimarisi ve spring cloud ortamında sınırlar
microservices mimarisi içinde gateway'nin retry politikası, istemci timeout'u ve mesaj tüketicisinin yeniden teslimi aynı iş emrini çoğaltabilir. Spring Cloud Gateway'de POST için körlemesine retry açmak, upstream isteği işledikten sonra 502 döndüğünde ikinci yazmayı tetikler. Yalnızca bağlantı kurulamadığı bilinen hataları yeniden deneyin veya downstream servise Idempotency-Key başlığını taşıyın. Gateway'de header'ın varsayılan olarak iletilmesi, downstream kodunun anahtarı sakladığı anlamına gelmez; iki katmanı ayrı test edin.
spring:
cloud:
gateway:
routes:
- id: order-service
uri: http://order-service
predicates:
- Path=/orders/**
filters:
- name: Retry
args:
retries: 2
methods: GET
statuses: BAD_GATEWAY,GATEWAY_TIMEOUTRedis ile SET key value NX PX 30000 ilk isteği hızlıca ayırmak için kullanılabilir, ancak SQL kaydının yerine geçmez. Redis TTL'i iş süresinden önce biterse ikinci istek kilidi alabilir; failover sırasında henüz replike edilmemiş kilit kaybolabilir. Redis'i yalnızca 'işlem sürüyor' yanıtını hızlı üretmek veya kısa süreli load shedding için kullanın, kesin sonuç kaydını PostgreSQL'de tutun. Bu ayrım, bir spring framework eğitimi veya spring boot eğitimi sırasında genellikle atlanan dağıtık tutarlılık ayrıntısıdır.
Spring Boot eğitimi bağlamında ölçüm, yük testi ve saklama süresi
Bu tasarımı doğrulamak için önce değiştirmeden önceki davranışı ölçün: k6 ile aynı anahtara sahip 100 paralel POST gönderin, veritabanında select count(*) from orders where external_key = ... sorgusunu çalıştırın ve 201/200/409 dağılımını kaydedin. Değişiklikten sonra hedef, iş kuralına göre tam bir sipariş satırı, tam bir 201 ve geri kalanların aynı gövdeli 200 veya açıkça tanımlanmış 409 döndürmesidir. Sadece ortalama gecikmeye bakmak yeterli değildir; kilit bekleyen isteklerin p95 ve p99 değerlerini ayrı izleyin.
import http from 'k6/http';
import { check } from 'k6';
export const options = { vus: 100, iterations: 100 };
const key = '018f6f91-7c42-7d23-8e1a-68b0f764d530';
export default function () {
const r = http.post('http://localhost:8080/orders',
JSON.stringify({ customerId: 'c-42', items: [{ sku: 'A1', quantity: 2 }] }),
{ headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key } });
check(r, { 'status is replay-safe': x => [200, 201, 409].includes(x.status) });
}Micrometer ile idempotency.replay, idempotency.in_progress ve idempotency.hash_mismatch sayaçlarını endpoint etiketiyle yayınlayın; idempotency key veya kullanıcı kimliğini tag yapmayın, çünkü sınırsız cardinality metrik depolamasını büyütür. Tamamlanmış kayıtlar için örneğin iş gereksinimine göre 24 saatlik retention belirleyip batch silme uygulayın. Bu pratik, bir spring boot kursu ya da java spring eğitimi içeriğinde kodun ötesindeki operasyonel maliyeti görünür kılar.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Java Spring Boot ReactJS FullStack Eğitimi (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Spring Boot'ta Idempotency-Key için Redis yeterli mi?
Hayır. Redis SET NX PX, eşzamanlı ilk çağrıyı azaltır ama TTL bitişi ve failover sonrası kilit kaybolabilir. Sipariş veya ödeme gibi kalıcı sonuçlar için PostgreSQL benzersiz kısıtıyla sahiplik kaydı oluşturun; Redis'i sadece kısa süreli in-progress kontrolü olarak kullanın.
Spring MVC POST endpoint'inde aynı idempotency key ile farklı body gelirse ne yapılmalı?
İlk isteğin kanonik JSON SHA-256 karmasını kaydedin. Aynı principal, operation ve key için sonraki isteğin karması farklıysa 409 Conflict döndürün. Eski başarılı yanıtı dönmek, istemcinin yanlış payload gönderdiğini sessizce gizler.
spring security JWT kullanıcı bilgisi idempotency kaydında nasıl kullanılmalı?
Benzersiz kaydı JWT sub ve gerekiyorsa tenant_id ile kapsamlayın. Anahtar olarak e-posta veya görünen kullanıcı adı kullanmayın; bunlar değişebilir. Kayıt anahtarı örneği: tenant_id + ':' + sub + ':' + operation + ':' + idempotency_key.
spring cloud gateway retry ayarı POST isteklerini neden çift çalıştırır?
Gateway, upstream servis veritabanına commit attıktan sonra ağ hatası veya timeout görebilir. POST retry bu durumda aynı yazmayı yeniden yollar. Retry'ı GET gibi güvenli metodlarla sınırlayın veya downstream servisin Idempotency-Key ile kalıcı deduplikasyon yaptığını doğrulayın.
AI / LLM Discovery
Bu makale Opendart Akademi Spring Framework eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


