MongoDB 操作可能抛出哪些异常? 如何优雅的处理?

发布于:2025-05-13 ⋅ 阅读:(13) ⋅ 点赞:(0)

下面是一些在使用 MongoDB (通过 Spring Data) 时可能遇到的常见异常类型,以及如何优雅地处理它们:

常见的 Spring DataAccessException 子类 (映射自 MongoDB 异常):

  1. org.springframework.dao.DuplicateKeyException

    • MongoDB 原生原因: 尝试插入或更新一个文档时,违反了集合上的唯一索引约束(包括默认的 _id 字段唯一性约束)。MongoDB Driver 通常会抛出 MongoWriteExceptionMongoBulkWriteException 带有错误码 11000 或 11001。
    • 场景:
      • 使用 insertsave 插入一个已经存在 _id 的文档。
      • 插入一个文档,其中某个字段的值在具有唯一索引的字段上已经存在。
      • 使用 update 更新文档,导致具有唯一索引的字段值与其他现有文档冲突。
    • 如何处理: 这是最常见的需要特定处理的业务相关异常之一。
      • try-catch 块中明确捕获 DuplicateKeyException
      • 通常,这意味着业务逻辑需要调整,例如:
        • 如果是插入,可能需要告知用户该项已存在。
        • 如果是更新,可能需要检查冲突字段或采取其他策略。
        • 转换为一个业务异常抛出给上层。
      • 记录详细错误信息,包括尝试操作的数据和冲突的键。
  2. org.springframework.dao.DataAccessResourceFailureException

    • MongoDB 原生原因: 与 MongoDB 服务器的网络或连接问题。例如,服务器不可达、连接被拒绝、连接中断、读取或写入超时 (MongoSocketException, MongoTimeoutException)。
    • 场景:
      • 应用程序启动时无法连接到 MongoDB。
      • 在执行操作时,与服务器的网络连接中断。
      • 服务器过载导致响应延迟超过配置的超时时间。
    • 如何处理: 这通常表示底层基础设施有问题。
      • try-catch 块中捕获 DataAccessResourceFailureException
      • 这种异常通常是瞬时或配置问题,可以考虑:
        • 记录严重错误日志,触发监控告警。
        • 对于读操作,可以考虑有限次的重试。写操作重试需要考虑幂等性。
        • 如果是系统启动时的连接问题,应用程序可能无法正常运行。
        • 转换为一个表示服务不可用的业务异常。
  3. org.springframework.dao.PermissionDeniedDataAccessException

    • MongoDB 原生原因: 认证或授权失败。当前连接的用户没有执行请求操作所需的权限。MongoDB Driver 可能会抛出 MongoCommandException 带有错误码 13 (AuthenticationFailed) 或其他表示授权失败的错误码。
    • 场景:
      • 连接数据库时认证失败(用户名/密码错误)。
      • 尝试读写某个数据库或集合,但当前用户没有相应的角色权限。
      • 尝试执行某个管理命令,但用户权限不足。
    • 如何处理: 这通常是配置或安全问题。
      • 捕获 PermissionDeniedDataAccessException
      • 记录错误日志,表明认证或授权失败,但不要记录敏感信息如密码。
      • 检查数据库连接配置(用户名、密码、认证库)。
      • 检查数据库用户的权限设置。
      • 向上层抛出表示权限不足的业务异常。
  4. org.springframework.dao.InvalidDataAccessApiUsageException

    • MongoDB 原生原因: 应用程序使用了无效的数据访问 API,或者查询、更新等操作本身存在逻辑或语法错误,不符合 MongoDB 的要求。例如,使用无效的字段名、操作符,或者尝试执行一个在当前状态下不允许的操作。MongoDB Driver 可能抛出各种 MongoCommandException 或其他类型的异常。
    • 场景:
      • 构建了一个语法错误的查询表达式。
      • 尝试对一个不存在的集合执行操作(虽然有些操作会自动创建集合,但某些上下文可能不允许)。
      • 使用了不兼容的数据类型或操作符。
      • 违反了数据库层面的 schema validation 规则 (如果配置了)。
    • 如何处理: 这是应用程序层面的错误。
      • 捕获 InvalidDataAccessApiUsageException
      • 记录详细的错误日志,包括导致错误的查询或操作。
      • 这通常需要在开发阶段调试解决代码问题。
      • 向上层抛出表示请求参数或操作无效的业务异常。
  5. org.springframework.dao.QueryTimeoutException

    • MongoDB 原生原因: 查询执行时间超过了客户端或服务器端配置的超时时间。MongoDB Driver 可能抛出 MongoExecutionTimeoutException 或其他与超时相关的异常。
    • 场景:
      • 执行了一个非常慢的查询,没有合适的索引。
      • 数据库服务器负载过高。
      • 客户端配置的查询超时时间过短。
    • 如何处理: 可能表示性能问题或配置问题。
      • 捕获 QueryTimeoutException
      • 记录日志,包括超时发生的查询,以便后续进行性能分析(索引、查询优化)。
      • 考虑调整客户端或服务器端的超时配置(如果合理)。
      • 向上层抛出表示操作超时的业务异常。
  6. org.springframework.dao.UncategorizedDataAccessException

    • MongoDB 原生原因: 发生了 MongoDB Driver 抛出的异常,但 MongoExceptionTranslator 无法将其映射到上述任何一个更具体的 DataAccessException 子类。这可能是因为错误比较罕见,或者 Spring Data MongoDB 还没有为这种特定的 MongoDB 错误码建立映射。
    • 场景:
      • 一些不太常见的 MongoDB 内部错误。
      • 驱动程序或 Spring Data 版本之间的兼容性问题。
      • 非常底层的、未预料到的错误。
    • 如何处理: 这是泛型错误处理。
      • 捕获 UncategorizedDataAccessException 作为所有未特定处理的 DataAccessException 的回退。
      • 务必记录详细的日志,包括原始的 MongoDB 异常 (getRootCause()),以便进行问题诊断。
      • 向上层抛出通用的数据库错误业务异常。

优雅处理的策略总结:

  1. 捕获特定异常: 始终先捕获并处理你预期可能发生的、需要特定业务逻辑响应的异常(如 DuplicateKeyException)。
  2. 按层次捕获: 在捕获特定异常之后,可以捕获更通用的 DataAccessException 来处理所有其他数据访问相关的错误。
  3. 记录详细日志:catch 块中,记录错误级别日志。日志应包含:
    • 异常类型和消息。
    • 导致错误的上下文信息(例如,尝试插入的数据、执行的操作类型)。
    • 如果是 UncategorizedDataAccessExceptionDataAccessException,尝试获取并记录其 getRootCause(),这通常是原始的 MongoDB Driver 异常,包含错误码和详细信息。
  4. 转换为业务异常: 不要让底层的 DataAccessException 异常上升到控制器层或客户端。在服务层或其他业务逻辑层,将捕获到的 DataAccessException 转换为应用程序定义的业务异常(例如 ResourceAlreadyExistsException, ServiceUnavailableException, PermissionDeniedException, InvalidInputException 等)。这样可以保持业务层的整洁,并使异常处理更贴近业务概念。
  5. 向上层抛出: 将转换后的业务异常抛出,由更上层的代码(如全局异常处理器)负责向用户返回友好的错误响应。
  6. 考虑重试: 对于 DataAccessResourceFailureExceptionQueryTimeoutException 这类瞬时错误,可以考虑实现重试逻辑(通常带指数退避)。但这需要在设计时仔细考虑操作的幂等性。
  7. 监控和告警:DataAccessResourceFailureExceptionUncategorizedDataAccessException 这类异常设置监控和告警,它们显示了更严重的基础设施或未处理的错误。

示例代码结构:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.PermissionDeniedDataAccessException;
// ... 其他可能的特定异常 ...
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 假设有自定义业务异常
class MyBusinessException extends RuntimeException {
    private String errorCode;
    public MyBusinessException(String message, Throwable cause) {
        super(message, cause);
    }
     public MyBusinessException(String message, String errorCode, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
    // Getter for errorCode if needed
}


@Service
public class ProductService {

    private static final Logger logger = LoggerFactory.getLogger(ProductService.class);

    @Autowired
    private MongoTemplate mongoTemplate; // 或 ProductRepository

    /**
     * 插入产品
     * @param product 要插入的产品
     * @throws MyBusinessException 如果插入失败
     */
    public void addProduct(Product product) {
        try {
            mongoTemplate.insert(product, "products");
            // 或者 productRepository.insert(product);

        } catch (DuplicateKeyException e) {
            logger.warn("Attempted to insert duplicate product with id: {}", product.getId(), e);
            // 转换为业务异常
            throw new MyBusinessException("Product with this identifier or unique name already exists.", "DUPLICATE_ENTRY", e);

        } catch (DataAccessResourceFailureException e) {
            logger.error("Database resource failure during product insertion.", e);
            // 转换为业务异常
            throw new MyBusinessException("Unable to connect to the database. Please try again later.", "DB_CONNECTION_ERROR", e);

        } catch (PermissionDeniedDataAccessException e) {
             logger.error("Permission denied during product insertion.", e);
             // 转换为业务异常
             throw new MyBusinessException("You do not have sufficient permissions to perform this operation.", "PERMISSION_DENIED", e);

        } catch (DataAccessException e) {
            // 捕获所有其他 Spring Data Access 异常
            logger.error("An unexpected database access error occurred during product insertion.", e);
            // 转换为通用的业务异常
            throw new MyBusinessException("An unexpected database error occurred.", "DB_UNEXPECTED_ERROR", e);

        } catch (Exception e) {
            // 捕获所有其他非数据访问异常 (例如 NullPointerException, IllegalArgumentException 等)
            logger.error("An unexpected error occurred during product insertion.", e);
             throw new RuntimeException("An unexpected application error occurred.", e);
        }
    }

    // ... 其他方法 ...
}

通过这种分层捕获和处理的方式,可以使应用程序更健壮,提供更有意义的错误信息,并将底层数据库的实现细节封装在数据访问层之下。


网站公告

今日签到

点亮在社区的每一天
去签到