На прошлой неделе к нам пришёл клиент: его аукцион «зависал», когда два пользователя одновременно ставили на один лот — один бид пропадал, другой дублировался. Потери составляли до $5000 за лот. Такая ситуация знакома многим владельцам аукционов. Мы разрабатываем аукционные системы, которые исключают подобные ошибки «из коробки». За 7 лет мы реализовали более 30 проектов для e-commerce и торговых площадок.
Проблемы, которые решаем
Race condition при параллельных ставках — самая частая беда. Два запроса обрабатываются одновременно, каждый видит текущую цену $100, оба записывают $110 — побеждает первый, второй должен быть отклонён. Без блокировки клиент получает ложное подтверждение, а аукцион — спорную ситуацию. Мы устраняем это через пессимистичную блокировку Redis, исключая двойные записи.
Ложноопределение победителя — проблема из-за несинхронизированных таймеров. Сервер показывает 5 секунд до конца, но из-за задержки сети клиент считает, что осталось 10. Ставка уходит вовремя, но сервер уже закрыл лот. Потери для организатора — тысячи долларов. Наш подход — синхронизация времени через WebSocket с серверным таймером.
Антиснайпинг — когда участник ждёт последней секунды и «крадёт» лот. Без механизма продления аукцион теряет честность. Мы добавляем автоматическое продление на 2 минуты после каждой ставки в финальной фазе.
Как мы это делаем: стек и ключевые решения
Выбираем Node.js (NestJS для строгой архитектуры) и PostgreSQL — проверенная связка для транзакционных нагрузок. Пессимистичная блокировка через Redis SETNX — чтобы ни один запрос не проскочил без блокировки. WebSocket (Socket.IO) — для мгновенных update всех клиентов; fallback на long-polling, если сеть блокирует WebSocket. Фронтенд — React 18 с Next.js (SSR для SEO) и TypeScript, компоненты на Tailwind.
Сравним два подхода к блокировке:
| Метод | Производительность | Надёжность | Сложность |
|---|---|---|---|
| Оптимистичная (retry) | Высокая, но повторные запросы | Низкая при высокой конкуренции | Низкая |
| Пессимистичная (Redis lock) | Средняя, блокировка на 5 с | Высокая, исключает race condition | Средняя |
Мы используем пессимистичную блокировку как более надёжную для аукционов с высокой конкуренцией.
Как предотвратить конфликт ставок?
Пессимистичная блокировка через Redis — надёжнее всего. Алгоритм:
class AuctionService {
async placeBid(auctionId: string, userId: string, amount: number): Promise<BidResult> {
const lockKey = `lock:auction:${auctionId}`;
const lockAcquired = await redis.set(lockKey, userId, 'NX', 'PX', 5000);
if (!lockAcquired) {
throw new Error('Auction is processing another bid, try again');
}
try {
const auction = await auctionRepo.findById(auctionId);
if (auction.status !== 'active') throw new BidError('Auction is not active');
if (new Date() > auction.endsAt) throw new BidError('Auction has ended');
if (amount <= auction.currentPrice) {
throw new BidError(`Bid must be higher than ${auction.currentPrice}`);
}
if (amount < auction.currentPrice + auction.minIncrement) {
throw new BidError(`Minimum increment is ${auction.minIncrement}`);
}
if (auction.currentLeaderId === userId) {
throw new BidError('You are already the highest bidder');
}
const bid = await bidRepo.create({ auctionId, userId, amount });
await auctionRepo.updateCurrentPrice(auctionId, amount, userId);
// Anti-sniping
const timeLeft = auction.endsAt.getTime() - Date.now();
if (timeLeft < 2 * 60 * 1000) {
const newEndTime = new Date(Date.now() + 2 * 60 * 1000);
await auctionRepo.extendTime(auctionId, newEndTime);
}
await this.broadcastBid(auctionId, bid, auction);
return { success: true, bid };
} finally {
await redis.del(lockKey);
}
}
async broadcastBid(auctionId: string, bid: Bid, auction: Auction) {
io.to(`auction:${auctionId}`).emit('bid:new', {
bidId: bid.id,
amount: bid.amount,
bidderId: bid.userId,
bidderName: anonymizeBidder(bid.userId),
timestamp: bid.createdAt,
totalBids: auction.bidCount + 1,
newEndTime: auction.endsAt
});
}
}
Почему антиснайпинг критичен для аукциона?
Без него любой участник может дождаться последних миллисекунд и выиграть без конкуренции. Наш таймер автоматически продлевается на 2 минуты, давая шанс ответить. Сервер транслирует обновлённое время всем клиентам через WebSocket.
class AuctionTimer {
async startTimer(auctionId: string, endTime: Date): Promise<void> {
const msUntilEnd = endTime.getTime() - Date.now();
setTimeout(async () => {
await this.finalizeAuction(auctionId);
}, msUntilEnd);
const broadcastInterval = setInterval(async () => {
const remaining = endTime.getTime() - Date.now();
if (remaining <= 0) {
clearInterval(broadcastInterval);
return;
}
if (remaining <= 60000) {
io.to(`auction:${auctionId}`).emit('timer:tick', {
remaining: Math.ceil(remaining / 1000)
});
}
}, 1000);
}
async finalizeAuction(auctionId: string): Promise<void> {
const auction = await auctionRepo.findById(auctionId);
if (auction.status !== 'active') return;
await auctionRepo.finalize(auctionId);
io.to(`auction:${auctionId}`).emit('auction:ended', {
winnerId: auction.currentLeaderId,
winnerName: await getUserName(auction.currentLeaderId),
finalPrice: auction.currentPrice
});
await this.notifyParticipants(auction);
}
}
Клиентский компонент на React
Все данные приходят по WebSocket — компонент реагирует мгновенно. Никаких лишних re-render, только точечные обновления.
function AuctionRoom({ auctionId }) {
const [auction, setAuction] = useState<AuctionState>();
const [bids, setBids] = useState<Bid[]>([]);
const [timeLeft, setTimeLeft] = useState<number>(0);
const socket = useSocket();
useEffect(() => {
if (!socket) return;
socket.emit('auction:join', { auctionId });
socket.on('auction:state', (state) => setAuction(state));
socket.on('bid:new', (bid) => {
setBids(prev => [bid, ...prev].slice(0, 50));
setAuction(prev => prev ? { ...prev, currentPrice: bid.amount } : prev);
});
socket.on('timer:tick', ({ remaining }) => setTimeLeft(remaining));
socket.on('auction:ended', ({ winnerId, finalPrice }) => {
setAuction(prev => prev ? { ...prev, status: 'ended' } : prev);
if (winnerId === currentUserId) {
showCongratulations(finalPrice);
}
});
return () => socket.emit('auction:leave', { auctionId });
}, [socket, auctionId]);
const placeBid = async (amount: number) => {
try {
await fetch(`/api/auctions/${auctionId}/bids`, {
method: 'POST',
body: JSON.stringify({ amount })
});
} catch (e) {
showError(e.message);
}
};
return (
<div className="auction-room">
<CurrentPrice price={auction?.currentPrice} />
<AuctionTimer seconds={timeLeft} critical={timeLeft < 30} />
<BidForm
minBid={(auction?.currentPrice ?? 0) + (auction?.minIncrement ?? 100)}
onBid={placeBid}
disabled={auction?.status !== 'active'}
/>
<BidHistory bids={bids} currentUserId={currentUserId} />
</div>
);
}
Процесс работы
- Аналитика — смотрим на бизнес-логику, определяем типы аукционов (английский, голландский, закрытый), нагрузку, интеграции с платёжными системами.
- Проектирование — рисуем архитектуру: схему БД, протокол WebSocket, стратегию блокировок, anti-sniping, таймеры.
- Реализация — пишем сервер и клиент параллельно. На каждый спринт — демо.
- Тестирование — unit-тесты, интеграционные тесты, нагрузочное тестирование (до 2000 RPS). Проверяем race condition и тайминги.
- Деплой — настраиваем CI/CD, контейнеризацию, мониторинг (Grafana, Prometheus). Передаём документацию и доступы.
Что входит в работу
- Полная кодовая база сервера (Node.js + TypeScript) и клиента (React/Next.js).
- Документация API (Swagger) и архитектурная схема.
- Нагрузочное тестирование с отчётом.
- Инструкция по развёртыванию и настройке мониторинга.
- Один месяц поддержки после запуска (исправление багов, консультации).
Сроки ориентировочно
| Комплектация | Срок |
|---|---|
| Базовый аукцион: ставки, таймер, история | 2–3 недели |
| + Антиснайпинг, уведомления, email-рассылки | +1 неделя |
| + Мультилот, интеграция с платёжными системами, личный кабинет | 4–6 недель |
Точные сроки оцениваем после анализа вашего ТЗ. Свяжитесь с нами — пришлём оценку за 1 день.
Типичные ошибки при разработке аукционов
| Ошибка | Последствие | Наше решение |
|---|---|---|
| Отсутствие блокировки ставок | Race condition, двойные ставки | Пексимистичная блокировка Redis |
| Реализация таймера только на клиенте | Рассинхронизация времени | Серверный таймер с broadcast |
| Нет anti-sniping | Лоты уходят по минимальной цене | Автоматическое продление на 2 мин |
| Игнорирование задержек сети | Запоздалые ставки | WebSocket с подтверждением |
«После внедрения системы количество спорных ставок снизилось до нуля» — клиент из e-commerce.
Пример конфликта ставок и его решения
Однажды на аукционе два участника одновременно подали ставки по $1000. Без блокировки оба запроса прошли, и система зафиксировала две ставки. После внедрения нашей блокировки второй запрос получает ошибку и клиент повторяет ставку с большей суммой. В результате — честная победа первого.
Избежать этих граблей помогают наш опыт — 7 лет в разработке real-time систем, более 30 завершённых проектов для e-commerce и аукционов. Гарантируем стабильность и масштабируемость.
Если вы хотите такую же систему, закажите консультацию — обсудим детали и приступим. Получите консультацию — расскажите о вашем аукционе, и мы предложим оптимальное решение.







