← Назад к вопросам

Что будешь делать если при передаче данных в NoSQL произошла ошибка?

2.0 Middle🔥 181 комментариев
#Архитектура и паттерны#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Обработка ошибок при записи в NoSQL базы данных

Это вопрос о надежности и обработке edge cases, очень важный для production-систем. Расскажу мой подход к обработке ошибок в NoSQL (MongoDB, DynamoDB, Firestore).

Типы ошибок, которые могут возникнуть

1. Ошибки валидации:

// Документ не соответствует схеме
{
  name: 123,  // Должна быть строка
  email: "invalid",  // Невалидный email
}

2. Ошибки уникальности:

// Дублирование индекса
db.users.insert({ email: "test@example.com" })
db.users.insert({ email: "test@example.com" })  // Ошибка: duplicate key

3. Ошибки размера:

// Документ слишком большой (>16MB в MongoDB)
const hugeDoc = { data: Buffer.alloc(20 * 1024 * 1024) };

4. Ошибки соединения:

// База данных недоступна
// Network timeout
// Connection pool exhausted

5. Ошибки памяти:

// Out of memory на сервере БД
// Disk full

Мой подход: Многоуровневая обработка ошибок

Уровень 1: Валидация ДО записи

import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';

class UserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(3)
  name: string;
}

async function createUser(userData: any) {
  // Валидируем ДО отправки в БД
  const userDto = plainToInstance(UserDto, userData);
  const errors = await validate(userDto);

  if (errors.length > 0) {
    throw new BadRequestException({
      message: 'Validation failed',
      errors: errors.map(e => ({
        field: e.property,
        constraints: e.constraints,
      })),
    });
  }

  // Валидация прошла — записываем в БД
  return db.users.insertOne(userDto);
}

Уровень 2: Try-Catch с специфической обработкой

import { MongoError } from 'mongodb';

async function insertDocument(collection: string, document: any) {
  try {
    const result = await db.collection(collection).insertOne(document);
    return { success: true, insertedId: result.insertedId };
  } catch (error) {
    // Дублирование ключа (E11000 в MongoDB)
    if (error.code === 11000) {
      const field = Object.keys(error.keyPattern)[0];
      throw new ConflictException(
        `${field} must be unique. Value "${document[field]}" already exists.`
      );
    }

    // Документ слишком большой
    if (error.code === 13) {
      throw new BadRequestException(
        'Document size exceeds maximum allowed (16MB)'
      );
    }

    // Ошибка соединения
    if (error.name === 'MongoNetworkError') {
      throw new ServiceUnavailableException(
        'Database temporarily unavailable. Please try again.'
      );
    }

    // Неизвестная ошибка
    logger.error('Database insert error:', error);
    throw new InternalServerErrorException(
      'Failed to save data. Please try again later.'
    );
  }
}

Retry механизм с exponential backoff

import pRetry from 'p-retry';

async function insertWithRetry(
  collection: string,
  document: any,
  maxRetries = 3
) {
  return pRetry(
    async () => {
      return db.collection(collection).insertOne(document);
    },
    {
      retries: maxRetries,
      onFailedAttempt: (error) => {
        logger.warn(
          `Insert attempt ${error.attemptNumber} failed. ` +
          `${error.retriesLeft} retries left. ` +
          `Error: ${error.message}`
        );
      },
      // Exponential backoff: 100ms, 200ms, 400ms
      minTimeout: 100,
      maxTimeout: 1000,
      factor: 2,
    }
  );
}

Когда НЕ нужно переить:

  • Ошибки валидации (422)
  • Дублирование уникального ключа (409)
  • Недостаточно памяти/прав (413, 403)

Когда НУЖНО переити:

  • Network timeout
  • Временная недоступность БД
  • Race conditions в distributed системе

Асинхронная очередь для надежности

Для критичных данных, которые нельзя потерять, используем очередь (RabbitMQ, Redis, Kafka):

import Bull from 'bull';

const writeQueue = new Bull('database-writes');

// Пользователь отправляет данные
async function handleUserSubmission(data: any) {
  // Сразу добавляем в очередь (быстро)
  await writeQueue.add({
    action: 'insertUser',
    data,
  });

  // Сразу отвечаем клиенту (не ждем БД)
  return { status: 'accepted', id: uuid() };
}

// Worker обрабатывает очередь
writeQueue.process(async (job) => {
  const { action, data } = job.data;

  if (action === 'insertUser') {
    try {
      const result = await db.users.insertOne(data);
      return { success: true, insertedId: result.insertedId };
    } catch (error) {
      // Если ошибка — job переит автоматически
      if (isRetryable(error)) {
        throw error;  // Bull переит
      } else {
        // Если ошибка не переитается — логируем и бросаем alert
        logger.error('Non-retryable error in queue:', error);
        await sendAlert({
          type: 'database-error',
          jobId: job.id,
          error: error.message,
        });
        throw error;
      }
    }
  }
});

// Обработчик неудачных jobs
writeQueue.on('failed', (job, err) => {
  logger.error(`Job ${job.id} failed after all retries:`, err);
  // Могли отправить в Dead Letter Queue
  deadLetterQueue.add(job.data);
});

Транзакции для операций с несколькими документами

// MongoDB транзакция
async function transferBalance(
  fromUserId: string,
  toUserId: string,
  amount: number
) {
  const session = db.startSession();

  try {
    await session.withTransaction(async () => {
      // Вычитаем у отправителя
      const fromResult = await db.users.findOneAndUpdate(
        { _id: fromUserId, balance: { $gte: amount } },
        { $inc: { balance: -amount } },
        { session }
      );

      if (!fromResult.value) {
        throw new BadRequestException('Insufficient balance');
      }

      // Добавляем получателю
      await db.users.findOneAndUpdate(
        { _id: toUserId },
        { $inc: { balance: amount } },
        { session }
      );

      // Логируем транзакцию
      await db.transactions.insertOne(
        {
          from: fromUserId,
          to: toUserId,
          amount,
          timestamp: new Date(),
          status: 'completed',
        },
        { session }
      );
    });
  } catch (error) {
    logger.error('Transaction failed:', error);
    throw error;
  } finally {
    await session.endSession();
  }
}

Мониторинг и алертинг

// Отслеживаем ошибки БД
const mongooseConnection = mongoose.connection;

mongooseConnection.on('error', (error) => {
  logger.error('MongoDB connection error:', error);
  sendAlert({
    type: 'database-error',
    severity: 'critical',
    message: error.message,
  });
});

// Метрики для мониторинга
const dbErrorCounter = new Counter({
  name: 'db_errors_total',
  help: 'Total database errors',
  labelNames: ['error_code', 'collection'],
});

async function insertWithMonitoring(collection, document) {
  try {
    return await db.collection(collection).insertOne(document);
  } catch (error) {
    dbErrorCounter.labels(error.code, collection).inc();
    throw error;
  }
}

Чеклист для обработки ошибок NoSQL

  1. Валидация входных данных — перед отправкой в БД
  2. Специфическая обработка ошибок — разные коды ошибок обрабатываются по-разному
  3. Retry логика — с exponential backoff
  4. Очереди — для критичных данных
  5. Транзакции — для multi-document операций
  6. Мониторинг — логирование и алертинг
  7. Graceful degradation — система продолжает работать при ошибке
  8. User feedback — понятные сообщения об ошибках
  9. Logging — достаточно информации для debug
  10. Rate limiting — предотвращение DDoS на БД

Пример: Полный production-ready контроллер

@Post('users')
async createUser(@Body() createUserDto: CreateUserDto) {
  // 1. Валидация
  const errors = await validate(createUserDto);
  if (errors.length > 0) {
    throw new BadRequestException(errors);
  }

  // 2. Попытка с retry
  try {
    const user = await this.userService.createUserWithRetry(createUserDto);
    return { success: true, user };
  } catch (error) {
    // 3. Специфическая обработка
    if (error.code === 11000) {
      throw new ConflictException('Email already exists');
    }

    // 4. Логирование и alert
    this.logger.error('Failed to create user:', error);
    await this.alertService.notifyAdmins({ error });

    // 5. Graceful response
    throw new InternalServerErrorException(
      'Failed to create user. Please try again later.'
    );
  }
}

Итог: Обработка ошибок NoSQL — это не просто try-catch, а многоуровневая система с валидацией, retry логикой, очередями, мониторингом и правильными HTTP ответами. Это разница между "работающей" системой и production-ready системой.

Что будешь делать если при передаче данных в NoSQL произошла ошибка? | PrepBro