ASP.NET Core'da SignalR ile Canlı Yayın Satış Altyapısı Nasıl Kurulur
Çok satıcılı bir pazaryerine canlı yayın üzerinden satış özelliği eklemem gerektiğinde ilk sorduğum soru şuydu: bir yayın izleyen 500 kişiye aynı anda "3 adet kaldı" bilgisini nasıl, gecikmesiz ve güvenilir şekilde ulaştırırım? Klasik polling (belirli aralıklarla API'ye sorma) hem sunucuyu gereksiz yere yorar hem de "gecikmesiz" hissini vermez. Cevap SignalR oldu.
Neden tek bir hub yetmedi
İlk denemem tek bir LiveHub ile her şeyi (yayın durumu, açık artırma teklifleri, sohbet mesajları) aynı kanaldan geçirmekti. Kısa sürede iki sorunla karşılaştım:
- Trafik karışıklığı: Bir açık artırmada saniyede onlarca teklif event'i gelirken, aynı kanaldan geçen sohbet mesajları gecikmeye başlıyordu.
- Ölçekleme zorluğu: Sunucuyu yatay ölçeklemek istediğimde (birden fazla instance), tek hub'ın backplane konfigürasyonu her tür trafiği aynı Redis kanalından geçirmek zorunda kalıyordu.
Çözüm, işlevsel olarak birbirinden bağımsız üç hub'a bölmekti: BroadcastHub (yayın durumu, izleyici sayısı), AuctionHub (teklif/artırma event'leri) ve ChatHub (mesajlaşma). Her biri kendi Group yapısını (yayın ID'sine göre) yönetiyor, kendi backplane trafiğini taşıyor.
public class AuctionHub : Hub
{
public async Task JoinAuction(string auctionId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"auction-{auctionId}");
}
public async Task PlaceBid(string auctionId, decimal amount)
{
// Doğrulama: teklif güncel en yüksek tekliften büyük mü?
var result = await _auctionService.TryPlaceBidAsync(auctionId, amount, Context.UserIdentifier);
if (!result.Success)
{
await Clients.Caller.SendAsync("BidRejected", result.Reason);
return;
}
await Clients.Group($"auction-{auctionId}")
.SendAsync("NewBid", result.Bid);
}
}Yarış durumu (race condition) tuzağı
Açık artırmada en kritik hata sınıfı, iki kullanıcının aynı anda teklif vermesiydi. Hub metodları varsayılan olarak concurrent çalışabildiği için, "güncel en yüksek teklif" kontrolü ile teklifi kaydetme arasında bir yarış durumu oluşuyordu — iki düşük teklif birbirini "geçerli" sanıp ikisi de kabul edilebiliyordu.
Çözüm, açık artırma başına bir SemaphoreSlim kilidi kullanmaktı (dağıtık ortamda bu veritabanı seviyesinde bir UPDATE ... WHERE current_bid < @newBid optimistic lock ile birleştirildi, tek instance'ta bellek içi semafor yeterliydi ama yatay ölçeklemede veritabanı seviyesi asıl garantiyi veriyor):
var rowsAffected = await _db.Auctions
.Where(a => a.Id == auctionId && a.CurrentBid < amount)
.ExecuteUpdateAsync(s => s.SetProperty(a => a.CurrentBid, amount));
if (rowsAffected == 0)
{
// Başka bir teklif araya girdi, reddet.
}Bu satır, SignalR'ın kendisinden değil, altındaki veri katmanının doğru kurgulanmasından geliyor — gerçek zamanlı sistemlerde en sinsi hatalar genelde iletim katmanında değil, durum tutarlılığında çıkıyor.
Bağlantı kopması ve yeniden bağlanma
Mobil kullanıcılarda WiFi'den mobil veriye geçiş gibi anlarda bağlantı kopuyor. SignalR'ın otomatik reconnect mekanizması bunu kısmen çözüyor, ama reconnect sırasında kullanıcı hangi group'lara üyeydi bilgisini kaybediyor. Bunu OnReconnectedAsync override'ında, bağlantı ID'sinden değil, kullanıcının o an izlediği yayın ID'sini client tarafında saklayıp yeniden JoinAuction çağırarak çözdüm — yani state'in kaynağı sunucu değil, client'ın kendisi (idempotent bir "yeniden katıl" akışı).
Sonuç
Üç ayrı hub, veritabanı seviyesinde optimistic locking ve client-taraflı group-rejoin akışı — bu üçü birlikte, tek bir kod tabanında hem canlı yayın hem açık artırma hem sohbeti aynı anda, birbirini yavaşlatmadan taşıyan bir altyapı oluşturdu. Bu projenin diğer teknik detaylarını (OpenAI embedding tabanlı öneri motoru, ONNX ile otomatik arka plan kaldırma) aşağıdaki proje sayfasında bulabilirsin.