بناء محرك مزادات فوري باستخدام Supabase Realtime
محرك المزادات الأول لديك قام باستقصاء قاعدة البيانات كل ثانية. وصلت العروض بترتيب غير صحيح. اختفى البعض منها تماماً. أضفت خادم WebSocket منفصل للنسخة الثانية — أفضل، لكن الآن تدير بنيتين تحتيتين. المحاولة الثالثة استخدمت Supabase Realtime، وتوقفت حالات السباق. لا استقصاء. لا خادم مقابس منفصل. تذاعع محفزات PostgreSQL تحديثات العروض في اللحظة التي تلتزم فيها، وكل عميل متصل يرى نفس الحالة خلال 50 ميلي ثانية. لقد أطلقت ثلاث أنظمة مزادات في سنتين. نسخة Supabase هي الوحيدة التي لم تضطر إلى إعادة بناؤها بعد الإطلاق. إليك العمارة التي جعلتها تعمل — والدالة الواحدة التي كادت تكسرها.
يجلس Supabase Realtime فوق Write-Ahead Log (WAL) في PostgreSQL ويستخدم خادماً يعتمد على Elixir لدفع تغييرات قاعدة البيانات عبر WebSockets إلى العملاء المتصلين. بالنسبة لنظام المزادات، هذا يعني أن كل عرض يصل إلى قاعدة البيانات ينتشر فوراً لكل من يراقب المزاد. لا استقصاء. لا بنية تحتية pub/sub منفصلة. قاعدة البيانات الخاصة بك هي نظام الأحداث الخاص بك.
دعنا نبني واحدة من الصفر.
جدول المحتويات
- نظرة عامة على العمارة
- مخطط قاعدة البيانات والإعداد
- محفزات PostgreSQL لمنطق العروض
- الاشتراك من جانب العميل باستخدام JavaScript
- التعامل مع حالات السباق والتحقق من صحة العروض
- تتبع الوجود للمزايدين النشطين
- ضبط الأداء والاعتبارات الإنتاجية
- Supabase Realtime مقابل البدائل
- نشر وتوسيع نظام المزادات الخاص بك
- الأسئلة الشائعة
نظرة عامة على العمارة
قبل كتابة أي كود، دعنا نفهم ما نبنيه وكيف تتناسب القطع معاً.
يعطيك Supabase Realtime ثلاثة بدائل تنطبق بشكل مثالي على متطلبات المزاد:
- Postgres Changes: اشترك في أحداث INSERT و UPDATE و DELETE على جداول العروض والمزادات الخاصة بك. عندما يضع شخص ما عرضاً، يحصل كل مشترك على بيانات الصف الجديدة خلال ميلي ثانية.
- Broadcast: أرسل رسائل عابرة لمشاركي القناة. مثالي لإخطارات "لقد تم تجاوز عرضك" التي لا تحتاج إلى أن تبقى محفوظة.
- Presence: تتبع من يراقب المزاد حالياً. يتيح لك هذا عرض "14 مزايد يراقبون" في واجهة المستخدم الخاصة بك والكشف عن الجلسات الشبحية.
تدفق البيانات يبدو هكذا:
- المزايد يرسل عرضاً من خلال الواجهة الأمامية الخاصة بك
- استدعاء RPC أو إدراج مباشر يصل إلى جدول
bidsالخاص بك - يقوم محفز PostgreSQL بالتحقق من مبلغ العرض وتحديث
auctions.current_high_bid - يالتقط Supabase Realtime تغيير WAL ويدفعه إلى جميع المشتركين على قناة المزاد
- يطلق محفز ثانٍ حدث Broadcast لإخطار أعلى عرض سابق بأنه تم تجاوزه
- يحدّث كل عميل متصل واجهة المستخدم الخاصة به في الوقت الفعلي
زمن التأخير من وضع العرض إلى تحديث الواجهة الأمامية عبر جميع العملاء عادة ما يكون أقل من 100ms. لقد قياست p99 بحوالي 80-90ms في الإنتاج على مستوى Pro من Supabase.
لماذا عدم استخدام الاستقصاء فقط؟
أنا أعلم أن البعض منكم يفكر "ألا يمكنني فقط الاستقصاء كل 500ms؟" يمكنك. لكن مع 200 مزايد متزامن على مزاد واحد، هذا 400 طلب في الثانية يضرب قاعدة البيانات الخاصة بك لمزاد واحد. اضرب ذلك في 50 مزاد نشط وأنت في 20,000 استعلام في الثانية — معظمها لا يعيد شيئاً جديداً. تقلب WebSockets هذا النموذج: صفر استعلامات عندما لا يتغير شيء، تحديثات فورية عندما يتغير شيء.
مخطط قاعدة البيانات والإعداد
إليك المخطط الذي أستخدمه. إنه بسيط متعمداً — يمكنك توسيعه، لكن البنية الأساسية تتعامل مع معظم أنواع المزادات.
-- جدول المزادات
CREATE TABLE auctions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
item_name TEXT NOT NULL,
description TEXT,
starting_price DECIMAL(12,2) NOT NULL DEFAULT 0,
current_high_bid DECIMAL(12,2) DEFAULT 0,
highest_bidder_id UUID REFERENCES auth.users(id),
min_increment DECIMAL(12,2) DEFAULT 1.00,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('scheduled', 'active', 'ended', 'sold', 'cancelled')),
starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ends_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 minutes',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- جدول العروض
CREATE TABLE bids (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
auction_id UUID NOT NULL REFERENCES auctions(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id),
amount DECIMAL(12,2) NOT NULL,
placed_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT positive_amount CHECK (amount > 0)
);
-- فهرس للبحث السريع عن العروض لكل مزاد
CREATE INDEX idx_bids_auction_amount ON bids(auction_id, amount DESC);
CREATE INDEX idx_bids_auction_time ON bids(auction_id, placed_at DESC);
-- حرج: تفعيل هوية النسخة المتماثلة ل Realtime
ALTER TABLE auctions REPLICA IDENTITY FULL;
ALTER TABLE bids REPLICA IDENTITY FULL;
إعداد REPLICA IDENTITY FULL ضروري. بدونه، Supabase Realtime يحصل فقط على المفتاح الأساسي في أحداث UPDATE و DELETE — وليس البيانات الكاملة للصف. بالنسبة لنظام المزادات، تحتاج الحمولة الكاملة حتى يتمكن العملاء من تحديث مبالغ العروض دون إجراء استعلام منفصل.
تفعيل نسخ Realtime
في لوحة معلومات Supabase، انتقل إلى Database → Replication وشغّل النسخ لكلا جدول auctions و bids. بدلاً من ذلك، يمكنك القيام بذلك مع SQL:
BEGIN;
-- إزالة المنشور الموجود إن وجد
DROP PUBLICATION IF EXISTS supabase_realtime;
-- إنشاء منشور مع كلا الجدولين
CREATE PUBLICATION supabase_realtime FOR TABLE auctions, bids;
COMMIT;
أمان على مستوى الصف
لا تتخطَّ هذا. RLS هي طبقة التحقق من الصحة من جانب الخادم الخاصة بك.
ALTER TABLE auctions ENABLE ROW LEVEL SECURITY;
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;
-- يمكن لأي شخص عرض المزادات النشطة
CREATE POLICY "Public auction viewing" ON auctions
FOR SELECT USING (status IN ('active', 'ended', 'sold'));
-- يمكن للمستخدمين المصرح لهم عرض جميع العروض على المزادات النشطة
CREATE POLICY "View bids on active auctions" ON bids
FOR SELECT USING (
EXISTS (
SELECT 1 FROM auctions
WHERE auctions.id = bids.auction_id
AND auctions.status = 'active'
)
);
-- فقط المستخدمون المصرح لهم يمكنهم وضع العروض
CREATE POLICY "Place bids" ON bids
FOR INSERT WITH CHECK (
auth.uid() = user_id
AND EXISTS (
SELECT 1 FROM auctions
WHERE auctions.id = auction_id
AND auctions.status = 'active'
AND auctions.ends_at > NOW()
)
);
محفزات PostgreSQL لمنطق العروض
هنا حيث يحدث السحر الحقيقي. قاعدة البيانات تفرض جميع منطق العروض من جانب الخادم — العميل لا يمكنه الغش.
محفز التحقق من صحة العرض وتحديث المزاد
CREATE OR REPLACE FUNCTION process_new_bid()
RETURNS TRIGGER AS $$
DECLARE
v_auction auctions%ROWTYPE;
BEGIN
-- قفل صف المزاد لمنع حالات السباق
SELECT * INTO v_auction
FROM auctions
WHERE id = NEW.auction_id
FOR UPDATE;
-- التحقق من أن المزاد نشط
IF v_auction.status != 'active' THEN
RAISE EXCEPTION 'Auction is not active';
END IF;
-- التحقق من أن المزاد لم ينتهِ
IF v_auction.ends_at < NOW() THEN
RAISE EXCEPTION 'Auction has ended';
END IF;
-- التحقق من أن مبلغ العرض يتجاوز الأعلى الحالي + الحد الأدنى للزيادة
IF NEW.amount < v_auction.current_high_bid + v_auction.min_increment THEN
RAISE EXCEPTION 'Bid must be at least % higher than current high bid of %',
v_auction.min_increment, v_auction.current_high_bid;
END IF;
-- منع المستخدم من تجاوز عرضه الخاص
IF v_auction.highest_bidder_id = NEW.user_id THEN
RAISE EXCEPTION 'You are already the highest bidder';
END IF;
-- تحديث المزاد مع العرض الأعلى الجديد
UPDATE auctions
SET
current_high_bid = NEW.amount,
highest_bidder_id = NEW.user_id,
updated_at = NOW()
WHERE id = NEW.auction_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER validate_and_process_bid
BEFORE INSERT ON bids
FOR EACH ROW
EXECUTE FUNCTION process_new_bid();
قفل FOR UPDATE على صف المزاد حاسم. بدونه، يمكن لعرضين يصلان في نفس الوقت أن يقرآ نفس current_high_bid، وكلاهما يمرر التحقق من الصحة، وكلاهما يتم إدراجه. القفل يسلسل الوصول.
بث إخطارات تجاوز العرض
ينطلق هذا المحفز بعد عرض ناجح ويرسل إخطار عابر لقناة المزاد:
CREATE OR REPLACE FUNCTION notify_outbid()
RETURNS TRIGGER AS $$
DECLARE
v_previous_bidder UUID;
BEGIN
-- ابحث عن من تم تجاوز عرضه للتو
SELECT user_id INTO v_previous_bidder
FROM bids
WHERE auction_id = NEW.auction_id
AND id != NEW.id
ORDER BY amount DESC
LIMIT 1;
-- بث إخطار تجاوز إذا كان هناك مزايد سابق
IF v_previous_bidder IS NOT NULL THEN
PERFORM realtime.send(
jsonb_build_object(
'auction_id', NEW.auction_id,
'new_high', NEW.amount,
'outbid_user', v_previous_bidder,
'new_leader', NEW.user_id
),
'outbid',
'auction:' || NEW.auction_id::text,
true
);
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER after_bid_notify
AFTER INSERT ON bids
FOR EACH ROW
EXECUTE FUNCTION notify_outbid();
الاشتراك من جانب العميل باستخدام JavaScript
الآن دعنا نربط الواجهة الأمامية. سأعرض هذا مع أنماط vanilla JavaScript/React — نفس النهج يعمل إذا كنت تبني مع Next.js أو أي إطار عمل آخر.
تهيئة العميل
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
realtime: {
params: {
eventsPerSecond: 20 // خنق حركة المزاد
}
}
}
);
معامل eventsPerSecond مهم. على مزاد ساخن مع عشرات العروض في الثانية، لا تريد إعادة تصيير 50 مرة في الثانية. عشرون تحديث في الثانية أكثر من كافٍ لواجهة مستخدم سلسة.
الاشتراك في قناة مزاد
function subscribeToAuction(auctionId, callbacks) {
const channel = supabase.channel(`auction:${auctionId}`);
channel
// استمع للعروض الجديدة عبر Postgres Changes
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'bids',
filter: `auction_id=eq.${auctionId}`
}, (payload) => {
callbacks.onNewBid(payload.new);
})
// استمع لتغييرات حالة المزاد
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'auctions',
filter: `id=eq.${auctionId}`
}, (payload) => {
callbacks.onAuctionUpdate(payload.new);
})
// استمع لإخطارات بث تجاوز العرض
.on('broadcast', { event: 'outbid' }, ({ payload }) => {
callbacks.onOutbid(payload);
})
// تتبع المزايدين النشطين عبر Presence
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
const bidderCount = Object.keys(state).length;
callbacks.onPresenceUpdate(bidderCount, state);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// تتبع وجود هذا المستخدم
await channel.track({
user_id: supabase.auth.getUser()?.data?.user?.id,
status: 'watching',
joined_at: new Date().toISOString()
});
}
});
return channel;
}
React Hook للاشتراكات في المزادات
import { useState, useEffect, useCallback } from 'react';
function useAuction(auctionId) {
const [auction, setAuction] = useState(null);
const [bids, setBids] = useState([]);
const [bidderCount, setBidderCount] = useState(0);
const [isOutbid, setIsOutbid] = useState(false);
useEffect(() => {
// جلب الحالة الأولية
async function loadAuction() {
const { data: auctionData } = await supabase
.from('auctions')
.select('*')
.eq('id', auctionId)
.single();
setAuction(auctionData);
const { data: bidData } = await supabase
.from('bids')
.select('*')
.eq('auction_id', auctionId)
.order('amount', { ascending: false })
.limit(20);
setBids(bidData || []);
}
loadAuction();
// الاشتراك في التحديثات الفعلية
const channel = subscribeToAuction(auctionId, {
onNewBid: (bid) => {
setBids(prev => [bid, ...prev].slice(0, 20));
setIsOutbid(false);
},
onAuctionUpdate: (updated) => setAuction(updated),
onOutbid: (payload) => {
const currentUser = supabase.auth.getUser()?.data?.user;
if (payload.outbid_user === currentUser?.id) {
setIsOutbid(true);
}
},
onPresenceUpdate: (count) => setBidderCount(count)
});
return () => {
supabase.removeChannel(channel);
};
}, [auctionId]);
const placeBid = useCallback(async (amount) => {
const user = (await supabase.auth.getUser()).data.user;
const { data, error } = await supabase
.from('bids')
.insert({
auction_id: auctionId,
amount: parseFloat(amount),
user_id: user.id
})
.select()
.single();
if (error) throw new Error(error.message);
return data;
}, [auctionId]);
return { auction, bids, bidderCount, isOutbid, placeBid };
}
التعامل مع حالات السباق والتحقق من صحة العروض
حالات السباق هي أكبر مصدر واحد للأخطاء في أنظمة المزادات. إليك كيفية التعامل معها.
من جانب الخادم: PostgreSQL يقوم بالعمل الثقيل
SELECT ... FOR UPDATE في دالة المحفز الخاصة بنا هي الخط الأول للدفاع. لكن هناك نمط آخر بدأت استخدامه — أقفال استشارية للمزادات عالية المنافسة:
CREATE OR REPLACE FUNCTION place_bid_safe(
p_auction_id UUID,
p_user_id UUID,
p_amount DECIMAL
)
RETURNS TABLE(bid_id UUID, new_high DECIMAL) AS $$
DECLARE
v_lock_key BIGINT;
v_bid_id UUID;
BEGIN
-- توليد مفتاح قفل حتمي من UUID المزاد
v_lock_key := ('x' || left(p_auction_id::text, 15))::bit(64)::bigint;
-- الحصول على قفل استشاري (يحجب العروض المتزامنة على نفس المزاد)
PERFORM pg_advisory_xact_lock(v_lock_key);
-- الآن آمن للإدراج (المحفز يتعامل مع التحقق)
INSERT INTO bids (auction_id, user_id, amount)
VALUES (p_auction_id, p_user_id, p_amount)
RETURNING id INTO v_bid_id;
RETURN QUERY
SELECT v_bid_id, p_amount;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
استدعِ هذا من العميل باستخدام RPC من Supabase:
const { data, error } = await supabase.rpc('place_bid_safe', {
p_auction_id: auctionId,
p_user_id: user.id,
p_amount: bidAmount
});
من جانب العميل: واجهة مستخدم متفائلة مع التراجع
اعرض العرض فوراً في الواجهة الأمامية، لكن كن مستعداً للتراجع عنه إذا رفضه الخادم:
async function handleBidSubmit(amount) {
const optimisticBid = {
id: crypto.randomUUID(),
amount,
user_id: user.id,
placed_at: new Date().toISOString(),
_optimistic: true
};
// اعرض فوراً
setBids(prev => [optimisticBid, ...prev]);
try {
await placeBid(amount);
// سيصل العرض الحقيقي عبر Realtime ويحل محل العرض المتفائل
} catch (err) {
// أزل العرض المتفائل عند الفشل
setBids(prev => prev.filter(b => b.id !== optimisticBid.id));
showError(err.message);
}
}
تتبع الوجود للمزايدين النشطين
عرض عدد الأشخاص الذين يراقبون المزاد ينشئ إلحاحاً. تتبع الوجود بسيط جداً مع Supabase:
// تحديث حالة المستخدم عندما يبدأ بالمزايدة
async function updatePresenceStatus(channel, status) {
await channel.track({
user_id: user.id,
status, // 'watching', 'bidding', 'won'
last_active: new Date().toISOString()
});
}
على جانب العرض، يمكنك تحليل حالة الوجود لعرض عدد من يزايدون بنشاط مقابل من يراقب فقط:
function parseBidderStats(presenceState) {
const users = Object.values(presenceState).flat();
return {
total: users.length,
bidding: users.filter(u => u.status === 'bidding').length,
watching: users.filter(u => u.status === 'watching').length
};
}
ضبط الأداء والاعتبارات الإنتاجية
الخنق والتأجيل
حرب المزايدة يمكن أن تولد عشرات الأحداث في الثانية. إليك ما أقوم بتكوينه:
- من جانب الخادم:
eventsPerSecond: 20على تكوين عميل Supabase - من جانب العميل: أجّل زر العرض عند 300ms لمنع النقرات المزدوجة
- تحديثات الواجهة الأمامية: استخدم
requestAnimationFrameلرسوم قائمة العروض
توقيت انتهاء المزاد
لا تثق بساعة العميل. استخدم وظيفة PostgreSQL cron عبر pg_cron:
-- تشغيل كل 10 ثوانٍ لإغلاق المزادات المنتهية
SELECT cron.schedule(
'close-expired-auctions',
'*/10 * * * * *',
$$
UPDATE auctions
SET status = CASE
WHEN highest_bidder_id IS NOT NULL THEN 'sold'
ELSE 'ended'
END
WHERE status = 'active'
AND ends_at <= NOW();
$$
);
تمديد مكافحة الكمين
تمدد معظم منصات المزادات الموعد النهائي إذا جاء عرض في الثوانى الأخيرة:
-- أضف إلى محفز process_new_bid
IF v_auction.ends_at - NOW() < INTERVAL '30 seconds' THEN
UPDATE auctions
SET ends_at = ends_at + INTERVAL '30 seconds'
WHERE id = NEW.auction_id;
END IF;
Supabase Realtime مقابل البدائل
استخدمت معظم هذه في الإنتاج. هنا مقارنة صادقة:
| الميزة | Supabase Realtime | Pusher | Ably | Firebase RTDB | Socket.io (مستضاف ذاتياً) |
|---|---|---|---|---|---|
| مزامنة DB أصلية | ✅ PostgreSQL WAL | ❌ خدمة منفصلة | ❌ خدمة منفصلة | ✅ شجرة JSON | ❌ يدوي |
| زمن الاستجابة (p99) | ~80-100ms | ~60ms | ~50ms | ~100ms | ~40ms (يعتمد على البنية التحتية) |
| أقصى أحداث/ثانية | 200k+ | 10k (Pro) | 50k | 100k | غير محدود (تقوم بتوسيعه) |
| تكامل المصادقة | مدمج (RLS + JWT) | مخصص | قائم على الرمز | Firebase Auth | مخصص |
| الوجود | ✅ مدمج | ✅ مدمج | ✅ مدمج | ✅ مدمج | ✅ مدمج |
| المستوى المجاني | 500K MAU, 200 متزامن | 100 اتصال | 6M رسالة/شهر | 1GB مخزن | $0 (تكاليف الاستضافة) |
| تسعير Pro | $25/شهر | $49/شهر | $29/شهر | الدفع حسب الاستخدام | ~$100-500/شهر (AWS) |
| الأفضل لـ | تطبيقات وقت فعلي موجهة للـ DB | pub/sub بسيط | موثوقية عالية | تطبيقات الجوال | التحكم الكامل |
بالنسبة لنظام المزادات بالتحديد، يفوز Supabase لأن العروض الخاصة بك موجودة بالفعل في PostgreSQL. لا تحتاج إلى مزامنة بين قاعدة بيانات ونظام pub/sub منفصل. يصل العرض إلى قاعدة البيانات، وقاعدة البيانات تطلق دفع WebSocket. مصدر حقيقة واحد.
إذا كنت تبني على معمارية Headless CMS، يتناسب Supabase بشكل طبيعي إلى جانب توصيل المحتوى دون إضافة خدمة أخرى لإدارتها.
نشر وتوسيع نظام المزادات الخاص بك
بالنسبة لمعظم المشاريع، مستوى Supabase المدار Pro بسعر $25/شهر يتعامل مع حتى 10,000 مزاد يومي بسهولة. إليك ما يجب مراقبته:
- حدود الاتصال: مستوى Pro يعطيك 500 اتصال Realtime متزامن. إذا احتجت إلى أكثر، ستحتاج إلى الترقية أو تنفيذ تجميع الاتصالات على العميل.
- حجم WAL: حجم العروض العالي يولد حركة WAL كبيرة. راقب فتحة النسخ المتماثلة الخاصة بك لتجنب النمو المفرط للقرص.
- عدد القنوات: كل مزاد يحصل على قناته الخاصة. مع آلاف المزادات النشطة، اختبر أن العميل بشكل صحيح يلغي الاشتراك في المزادات المنتهية.
بالنسبة لواجهة أمامية مبنية بـ Astro أو Next.js، عميل Supabase JS يعمل بشكل متطابق — فقط تأكد من أنك تهيئه من جانب العميل للاشتراك في Realtime.
إذا كنت تبني شيئاً يحتاج إلى التعامل مع مقياس جاد — مئات الآلاف من المزايدين المتزامنين — تواصل معنا. لقد عمارنا أنظمة كهذه على نطاق واسع ويمكننا مساعدتك تجنب الفخاخ. يمكنك أيضاً التحقق من صفحة التسعير الخاصة بنا للعروض القائمة على المشروع.
الأسئلة الشائعة
كم عدد المزايدين المتزامنين الذين يمكن لـ Supabase Realtime التعامل معهم؟ يمكن لـ Supabase Realtime التعامل مع أكثر من 200,000 حدث في الثانية عبر خوادم موزعة على منصتهم المدارة. يدعم مستوى Pro بسعر $25/شهر حتى 500 اتصال متزامن لكل مشروع. بالنسبة للمزادات الأكبر، يوفر مستوى Enterprise حدود مخصصة، أو يمكنك استضافة خادم Realtime ذاتياً (إنه مفتوح المصدر) على البنية التحتية الخاصة بك.
هل Supabase Realtime سريع بما يكفي للمزاد المباشر؟ نعم. في اختباري، يبلغ متوسط زمن التأخير من الطرف إلى الطرف من إدراج العرض إلى إخطار العميل حوالي 50-80ms، مع p99 أقل من 100ms. للمرجع، وقت رد فعل الإنسان حوالي 200-300ms، لذا تبدو العروض فورية بشكل فعلي. عنق الزجاجة نادراً ما يكون Supabase — عادة ما يكون اتصال العميل بالشبكة.
كيف أمنع حالات السباق عندما يزايد شخصان في نفس الوقت؟
استخدم قفل صف PostgreSQL SELECT ... FOR UPDATE داخل دالة محفز، أو استخدم أقفال استشارية عبر pg_advisory_xact_lock(). هذا يسلسل معالجة العروض لكل مزاد بحيث يتم التحقق من عرض واحد فقط في المرة. "العرض الخاسر" لا يزال يتم التحقق منه — فقط يرى الأعلى الأول من الفائز ويما إذا كان لا يزال أعلى (يكون ناجحاً) أو يفشل برسالة خطأ مناسبة.
هل يمكنني استخدام Supabase Realtime مع Next.js أو Astro؟
بالتأكيد. عميل @supabase/supabase-js يعمل في أي بيئة JavaScript. بالنسبة لـ Next.js، هيّئ عميل Supabase في مكون عميل (لأن Realtime يحتاج إلى WebSockets متصفح) واستخدمه داخل خطافات useEffect. بالنسبة لـ Astro، استخدمه في جزر تفاعلية من جانب العميل. كود الاشتراك متطابق بغض النظر عن اختيار إطار العمل الخاص بك.
ماذا يحدث إذا سقط اتصال المستخدم أثناء المزاد؟ يحاول Supabase Realtime إعادة الاتصال تلقائياً. عندما يتصل العميل ويعيد الاشتراك، يحصل على الحالة الحالية. بالنسبة للمزادات الحرجة، أوصي أيضاً بجلب أحدث حالة مزاد عبر استعلام معياري عند إعادة الاتصال للتأكد من عدم فقدان أي شيء أثناء نافذة قطع الاتصال. سيقوم نظام Presence تلقائياً بإزالة المستخدم المقطوع بعد انتهاء المهلة.
كيف أتعامل مع أوقات انتهاء المزاد بدقة؟
لا تثق بـمؤقتات جانب العميل — يمكن التلاعب بها. استخدم امتداد PostgreSQL pg_cron للتحقق من وإغلاق المزادات المنتهية كل 10 ثوانٍ من جانب الخادم. أرسل طابع زمن الخادم للعملاء حتى يتمكنوا من عرض العد التنازلي، لكن تحديد الانتهاء الفعلي يحدث دائماً في قاعدة البيانات.
هل Supabase Realtime مجاني للمشاريع الصغيرة؟ يتضمن المستوى المجاني من Supabase Realtime مع 200 اتصال متزامن و 500,000 مستخدم نشط شهري. هذا كافٍ لموقع مزاد هواية أو MVP. إذا كنت تشغل منصة مزاد إنتاجية ذات حركة ذات معنى، مستوى Pro بسعر $25/شهر مع $0.09/GB egress هو حيث تريد البدء. إنه أرخص بكثير من تشغيل البنية التحتية الخاصة بك للـ WebSocket.
كيف أختبر نظام مزايدات فوري محلياً؟
استخدم Supabase CLI (supabase start) لتشغيل مثيل Supabase محلي مع تفعيل Realtime. افتح علامات تبويب متعددة في المتصفح لمحاكاة عدة مزايدين. لاختبار الحمل، أستخدم سكريبت Node.js بسيط ينشئ 100+ عملاء Supabase ويجعلهم يزايدون بعضهم البعض على مؤقت. يكتشف هذا حالات السباق ويساعدك على ضبط معامل eventsPerSecond قبل الذهاب إلى الإنتاج.