Spring Data JPA ve Hibernate ORM ile stok gibi yüksek çekişmeli kayıtları kayıpsız güncellemek için optimistic locking, koşullu SQL, retry sınırları ve k6-JFR tabanlı ölçüm akışını uygulayın.
Spring Data JPA ile Yüksek Çekişmeli Stok Güncelleme Tasarımı
Spring Data JPA ile kayıp güncellemeyi yeniden üretin
Yüksek çekişme hatasını önce deterministik biçimde üretin. PostgreSQL'de ürün satırını tek kayıtla başlatın ve 100 paralel isteğin toplam 100 adet düşmesini bekleyin. java backend geliştirme ekiplerinde sık görülen hata, her isteğin önce available değerini okuyup sonra Java tarafında azaltarak kaydetmesidir. İki transaction aynı değeri okuduğunda, son yazan transaction ilkini ezer; HTTP 200 dönmesi bu kaybı görünmez kılar.
create table product (
id bigint primary key,
available integer not null check (available >= 0),
version bigint not null
);
insert into product(id, available, version) values (42, 100, 0);
@Entity
class Product {
@Id Long id;
int available;
@Version long version;
}Bu davranışı k6 ile CI'da da çalıştırılabilecek küçük bir yük testiyle görünür yapın. Test sonunda GET /products/42 sonucu 0 değilse veya başarılı rezervasyon sayısı ile başlangıç stoku arasındaki ilişki tutarsızsa, kayıp güncelleme vardır. Aynı testi 1, 10 ve 100 VU ile çalıştırmak, hatanın yalnızca üretim trafiğinde ortaya çıkıyor gibi görünmesini engeller.
import http from 'k6/http';
import { check } from 'k6';
export const options = { vus: 100, iterations: 100 };
export default function () {
const r = http.post('http://localhost:8080/products/42/reservations',
JSON.stringify({ quantity: 1 }),
{ headers: { 'Content-Type': 'application/json' } });
check(r, { '200 veya 409': x => x.status === 200 || x.status === 409 });
}Bir spring framework uygulamasında controller, transaction sınırını değil yalnızca HTTP sözleşmesini taşımalıdır. Reservation işlemini servis katmanına koyun; aksi halde testte doğrudan servis çağrısı yapan bir işçi ile HTTP çağrısının transaction davranışı ayrışır. Bu ayrım, java microservices içinde aynı rezervasyon mantığının REST, mesaj tüketicisi ve zamanlanmış iş tarafından çağrılabilmesini sağlar.
Hibernate ORM optimistic locking ve kontrollü retry sınırı
Hibernate ORM, @Version alanını güncelleme SQL'inin where bölümüne ekler. Bir transaction ürünü version 7 ile okuduysa Hibernate fiilen update product set available=?, version=8 where id=? and version=7 çalıştırır. Etkilenen satır sayısı 0 olduğunda Spring, çoğunlukla ObjectOptimisticLockingFailureException fırlatır. Bu mekanizma negatif stoku tek başına önlemez; sadece eski kopyayla yazmayı reddeder.
Retry denemesinin yeni bir transaction içinde başladığından emin olun. @Retryable ve @Transactional anotasyonlarını aynı metoda eklemek, proxy sırası ve self-invocation nedeniyle beklenmeyen bir sınır üretebilir. Aşağıdaki yaklaşımda RetryTemplate her denemede TransactionTemplate çağırır; başarısız denemenin persistence context'i sonraki denemeye taşınmaz.
@Service
class ReservationService {
private final ProductRepository products;
private final TransactionTemplate tx;
private final RetryTemplate retry = RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(10, 2.0, 80)
.retryOn(ObjectOptimisticLockingFailureException.class)
.build();
Reservation reserve(long id, int quantity) {
return retry.execute(ctx -> tx.execute(status -> {
Product p = products.findById(id).orElseThrow();
if (p.getAvailable() < quantity) throw new OutOfStockException(id);
p.setAvailable(p.getAvailable() - quantity);
return new Reservation(id, quantity, p.getVersion() + 1);
}));
}
}Retry yalnızca saf, yeniden yürütülebilir state değişiminde güvenlidir. Transaction içinde kart tahsilatı, e-posta gönderimi veya Kafka'ya doğrudan publish varsa ikinci deneme bu yan etkileri çoğaltabilir. Bu durumda rezervasyon kaydını ve outbox olayını aynı transaction'a yazın; yayıncı outbox kaydını commit sonrasında teslim etsin. Deneme sayısını metrik olarak kaydedin: 3 denemeyi tüketen istekler 409 veya 503 olarak ayrıştırılmalıdır, sonsuz retry kuyruk gecikmesini büyütür.
Spring Data JPA ile tek SQL'de koşullu stok düşürme
Tek satır üzerindeki çekişme sürekli ise önce oku-sonra yaz akışı gereksiz round trip ve retry üretir. Stok kuralını doğrudan SQL'in atomik koşuluna taşıyın: available >= :quantity kontrolü ile azaltma aynı statement içinde gerçekleşir. PostgreSQL ve InnoDB, aynı satıra gelen güncellemeleri kilit sırasına koyar; ikinci statement ilk commit sonrasındaki güncel satır sürümünde koşulu tekrar değerlendirir.
public interface ProductRepository extends JpaRepository<Product, Long> {
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("""
update Product p
set p.available = p.available - :quantity,
p.version = p.version + 1
where p.id = :id
and p.available >= :quantity
""")
int decrementIfAvailable(long id, int quantity);
}
@Transactional
public void reserve(long id, int quantity) {
if (quantity <= 0) throw new IllegalArgumentException("quantity");
if (products.decrementIfAvailable(id, quantity) != 1) {
throw new OutOfStockException(id);
}
}Bulk JPQL update, yönetilen Product nesnesinin alanlarını Hibernate'in birinci seviye cache'inde otomatik değiştirmez. Bu yüzden aynı transaction'da ürünü daha önce yüklediyseniz eski available değerini görürsünüz. Örnekteki clearAutomatically = true persistence context'i temizler; daha karmaşık akışlarda bulk update'i ayrı bir transaction'a almak daha güvenlidir. Ayrıca bulk update version alanını otomatik artırmaz, bu nedenle sorguda açıkça artırmak gerekir.
Bu seçim optimistic locking'i tamamen terk etmek anlamına gelmez. Koşullu SQL, sayısal sayaç ve kapasite ayırma için iyi bir araçtır; birden çok alanın kullanıcı tarafından düzenlendiği aggregate'lerde @Version çatışmayı kullanıcıya göstermek için daha uygundur. spring data jpa repository metodunun dönüşündeki satır sayısını iş kuralına bağlamak kritik ayrıntıdır: 0, hem ürün yok hem de stok yetersiz anlamına gelebilir. Dış API bu ikisini ayıracaksa önce varlık kontrolü yapın veya SQL'den durum döndüren bir PostgreSQL returning sorgusu kullanın.
JFR ve k6 ile önce-sonra karşılaştırmasını yapın
Koşullu SQL'e geçişi sadece ortalama süreyle değerlendirmeyin. Aynı veri seti, aynı connection pool boyutu ve aynı k6 senaryosuyla önce optimistic locking plus retry, sonra tek SQL sürümünü çalıştırın. Karşılaştırmada p95 gecikme, 409 oranı, başarılı rezervasyon sayısı, retry sayısı ve veritabanı statement sayısını birlikte kaydedin. Örneğin 100 stok için 100 paralel istek testinde doğru sonuç, başarılı istek sayısının tam olarak 100 olmasıdır; daha düşük sayı gereksiz çatışma, daha yüksek sayı ise invariant ihlalidir.
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20Yük testi sırasında JVM kaydını Java Flight Recorder ile alın: jcmd $PID JFR.start name=stock settings=profile duration=120s filename=stock.jfr. JDK Mission Control'da Java Monitor Blocked, Socket Read, allocation flame graph ve thread park sürelerine bakın. JDBC pool beklemesi artıyorsa JFR'de çok sayıda park olmuş HTTP iş parçacığı ve Micrometer'da hikaricp.connections.pending görülür; bu durumda pool'u rastgele büyütmek yerine PostgreSQL'deki aktif sorgu ve lock beklemelerini de inceleyin.
PostgreSQL tarafında yük testi penceresinde aşağıdaki sorguyu alın. wait_event_type = 'Lock' satırları uygulama kilidi değil veritabanı kilidi beklediğini gösterir. Koşullu tek SQL'e geçtikten sonra lock beklemesi tamamen yok olmak zorunda değildir, çünkü aynı ürün satırı fiziksel olarak seri güncellenir; beklenen değişim retry kaynaklı ek statement ve uygulama katmanı CPU'sunun azalmasıdır.
select pid, state, wait_event_type, wait_event, query
from pg_stat_activity
where datname = current_database()
and state <> 'idle';Spring AI, Spring MCP ve model context protocol araçlarında rezervasyon semantiği
spring ai ile bir asistana stok aracı sunduğunuzda modelin doğal dil üretimi transaction kararı olmamalıdır. spring mcp üzerinden model context protocol aracı çağrısı, normal HTTP istemcisi gibi timeout sonrası tekrar deneyebilir. Bu nedenle araç parametresine istemcinin ürettiği bir idempotency anahtarı ekleyin ve bunu rezervasyon tablosunda unique index ile saklayın. Aynı anahtar ikinci kez geldiğinde yeni stok düşmek yerine ilk sonucu döndürün.
@Tool(description = "Reserve stock for a confirmed order")
public ReservationResult reserveStock(long productId, int quantity,
String idempotencyKey) {
if (!idempotencyKey.matches("[A-Za-z0-9_-]{16,80}")) {
throw new IllegalArgumentException("invalid idempotency key");
}
return reservationApplication.reserve(productId, quantity, idempotencyKey);
}
create unique index uq_reservation_idempotency
on reservation(idempotency_key);Araç açıklamasına serbest metin yerine birim, üst sınır ve hata türlerini koyun. Örneğin quantity için 1-20 sınırı uygulama servisinde doğrulanmalı, modelin tool schema'sına güvenilmemelidir. Bu yaklaşım, model zaman aşımından sonra aynı çağrıyı yeniden yaptığında stok invariant'ını korur ve gözlemlenebilirlikte idempotency anahtarıyla tek bir iş isteğini izlemeyi mümkün kılar.
Java eğitimi için uygulanabilir kontrol listesi
Bir java eğitimi veya java programlama eğitimi kapsamında bu konuyu öğretmenin ölçülebilir yolu, öğrenciye önce hatalı read-modify-write endpoint'ini verip k6 ile 100 paralel istekteki sonuç farkını göstermektir. Ardından aynı endpoint'i önce @Version, sonra koşullu JPQL update ile uygulatın; her aşamada available, başarı sayısı ve p95 değerini CSV olarak kaydedin. Bu laboratuvar, yalnızca annotation ezberi yerine transaction isolation ve SQL etki sayısının ilişkisinin kurulmasını sağlar.
Bir java kursu veya java fullstack eğitimi projesinde frontend, 409 yanıtını 'stok başka bir istek tarafından tüketildi' mesajıyla ele almalı ve aynı idempotency anahtarını yeni kullanıcı niyeti oluşana kadar tekrar kullanmalıdır. Bir spring boot eğitimi modülünde ise Actuator Prometheus endpoint'i üzerinden http_server_requests_seconds_bucket ve Hikari metriklerini Grafana paneline bağlamak, kod değişikliğinin veritabanı beklemesine etkisini doğrulamak için somut bir ödevdir.
İ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 Data JPA ile stok düşürürken @Version mı koşullu update mi kullanılmalı?
Tek sayaç veya kapasite düşürme işleminde repository metodunun döndürdüğü satır sayısını kontrol eden koşullu update genellikle daha az retry üretir. Birden fazla alanın kullanıcı düzenlemesini çatışma olarak sunmanız gerekiyorsa @Version kullanın. Bulk JPQL update kullanırsanız persistence context'in eski kalmasını önlemek için clearAutomatically=true ekleyin veya işlemi ayrı transaction'a taşıyın.
Java eğitimi ve java programlama eğitimi projelerinde optimistic locking nasıl test edilir?
Başlangıç stokunu 100 yapın, k6 ile 100 paralel rezervasyon gönderin ve son stok ile başarılı yanıt sayısını doğrulayın. Test sırasında jcmd ile 120 saniyelik JFR kaydı alın. 100 başarıdan az sonuç retry sınırını veya lock çatışmasını, 100'den fazla sonuç ise negatif stok ya da kayıp güncelleme hatasını gösterir.
Spring Boot eğitimi içinde Hibernate ORM bulk update neden eski veri gösterir?
JPQL bulk update doğrudan veritabanında çalışır ve Hibernate'in birinci seviye cache'indeki yönetilen entity alanlarını değiştirmez. Aynı transaction içinde daha önce yüklenmiş Product nesnesi varsa eski available değeri okunabilir. @Modifying(flushAutomatically=true, clearAutomatically=true) kullanın veya bulk update sonrasında EntityManager.clear() çağırın.
Spring AI ve Spring MCP ile model context protocol stok aracı nasıl idempotent yapılır?
Araç çağrısına doğrulanmış bir idempotencyKey ekleyin, reservation tablosunda bu alan için unique index oluşturun ve unique ihlalinde ilk rezervasyon sonucunu döndürün. Tool schema doğrulaması yeterli değildir; quantity sınırı ve anahtar biçimi servis katmanında da kontrol edilmelidir. Böylece ağ zaman aşımı sonrası yinelenen araç çağrısı ikinci kez stok azaltmaz.
AI / LLM Discovery
Bu makale Opendart Akademi Java 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.


