SpringBoot MongoDB 索引冲突分析及解决方法

一、背景

spring-data-mongo 实现了基于 MongoDB 的 ORM-Mapping 能力,

通过一些简单的注解、Query封装以及工具类,就可以通过对象操作来实现集合、文档的增删改查;

在 SpringBoot 体系中,spring-data-mongo 是 MongoDB Java 工具库的不二之选。

二、问题产生

在一次项目问题的追踪中,发现SpringBoot 应用启动失败,报错信息如下:

Error creating bean with name 'mongoTemplate' defined in class path resource [org/bootfoo/BootConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.mongodb.core.MongoTemplate]: Factory method 'mongoTemplate' threw exception; nested exception is org.springframework.dao.DataIntegrityViolationException: Cannot create index for 'deviceId' in collection 'T_MDevice' with keys '{ "deviceId" : 1}' and options '{ "name" : "deviceId"}'. Index already defined as '{ "v" : 1 , "unique" : true , "key" : { "deviceId" : 1} , "name" : "deviceId" , "ns" : "appdb.T_MDevice"}'.; nested exception is com.mongodb.MongoCommandException: Command failed with error 85: 'exception: Index with name: deviceId already exists with different options' on server 127.0.0.1:27017. The full response is { "createdCollectionAutomatically" : false, "numIndexesBefore" : 6, "errmsg" : "exception: Index with name: deviceId already exists with different options", "code" : 85, "ok" : 0.0 }
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)

...

Caused by: org.springframework.dao.DataIntegrityViolationException: Cannot create index for 'deviceId' in collection 'T_MDevice' with keys '{ "deviceId" : 1}' and options '{ "name" : "deviceId"}'. Index already defined as '{ "v" : 1 , "unique" : true , "key" : { "deviceId" : 1} , "name" : "deviceId" , "ns" : "appdb.T_MDevice"}'.; nested exception is com.mongodb.MongoCommandException: Command failed with error 85: 'exception: Index with name: deviceId already exists with different options' on server 127.0.0.1:27017. The full response is { "createdCollectionAutomatically" : false, "numIndexesBefore" : 6, "errmsg" : "exception: Index with name: deviceId already exists with different options", "code" : 85, "ok" : 0.0 }
at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.createIndex(MongoPersistentEntityIndexCreator.java:157)
at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.checkForAndCreateIndexes(MongoPersistentEntityIndexCreator.java:133)
at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.checkForIndexes(MongoPersistentEntityIndexCreator.java:125)
at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.<init>(MongoPersistentEntityIndexCreator.java:91)
at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.<init>(MongoPersistentEntityIndexCreator.java:68)
at org.springframework.data.mongodb.core.MongoTemplate.<init>(MongoTemplate.java:229)
at org.bootfoo.BootConfiguration.mongoTemplate(BootConfiguration.java:121)
at org.bootfoo.BootConfiguration$$EnhancerBySpringCGLIB$$1963a75.CGLIB$mongoTemplate$2(<generated>)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:162)
... 58 more

Caused by: com.mongodb.MongoCommandException: Command failed with error 85: 'exception: Index with name: deviceId already exists with different options' on server 127.0.0.1:27017. The full response is { "createdCollectionAutomatically" : false, "numIndexesBefore" : 6, "errmsg" : "exception: Index with name: deviceId already exists with different options", "code" : 85, "ok" : 0.0 }
at com.mongodb.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:115)
at com.mongodb.connection.CommandProtocol.execute(CommandProtocol.java:114)
at com.mongodb.connection.DefaultServer$DefaultServerProtocolExecutor.execute(DefaultServer.java:168)

关键信息: org.springframework.dao.DataIntegrityViolationException: Cannot create index

从异常信息上看,出现的是索引冲突( Command failed with error 85 ),spring-data-mongo 组件在程序启动时会实现根据注解创建索引的功能。

查看业务实体定义:

@Document(collection = "T_MDevice")
public class MDevice {

  @Id
  private String id;

  @Indexed(unique=true)
  private String deviceId;

deviceId 这个字段上定义了一个索引, unique=true 表示这是一个唯一索引。

我们继续 查看 MongoDB中表的定义:

db.getCollection('T_MDevice').getIndexes()

>>
[
  {
    "v" : 1,
    "key" : {
      "_id" : 1
    },
    "name" : "_id_",
    "ns" : "appdb.T_MDevice"
  },
  {
    "v" : 1,
    "key" : {
      "deviceId" : 1
    },
    "name" : "deviceId",
    "ns" : "appdb.T_MDevice"
  }
]

发现数据库表中同样存在一个名为 deviceId的索引,但是并非唯一索引!

三、详细分析

为了核实错误产生的原因,我们尝试通过 Mongo Shell去执行索引的创建,发现返回了同样的错误。

通过将数据库中的索引删除,或更正为 unique=true 之后可以解决当前的问题。

从严谨度上看,一个索引冲突导致 SpringBoot 服务启动不了,是可以接受的。

但从灵活性来看,是否有某些方式能 禁用索引的自动创建 ,或者仅仅是打印日志呢?

尝试 google spring data mongodb disable index creation

发现 JIRA-DATAMONGO-1201 在2015年就已经提出,至今未解决。

stackoverflow 找到许多 同样问题

但大多数的解答是不采用索引注解,选择其他方式对索引进行管理。

这些结果并不能令人满意。

尝试查看 spring-data-mongo 的机制,定位到 MongoPersistentEntityIndexCreator 类:

初始化方法中,会根据 MappingContext(实体映射上下文)中已有的实体去创建索引

public MongoPersistentEntityIndexCreator(MongoMappingContext mappingContext, MongoDbFactory mongoDbFactory,
      IndexResolver indexResolver) {
    ...
    //根据已有实体创建
    for (MongoPersistentEntity<?> entity : mappingContext.getPersistentEntities()) {
      checkForIndexes(entity);
    }
  }

在接收到MappingContextEvent时,创建对应实体的索引

 public void onApplicationEvent(MappingContextEvent<?, ?> event) {

    if (!event.wasEmittedBy(mappingContext)) {
      return;
    }

    PersistentEntity<?, ?> entity = event.getPersistentEntity();

    // Double check type as Spring infrastructure does not consider nested generics
    if (entity instanceof MongoPersistentEntity) {
      //创建单个实体索引
      checkForIndexes((MongoPersistentEntity<?>) entity);
    }
  }

MongoPersistentEntityIndexCreator是通过MongoTemplate引入的,如下:

  public MongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter mongoConverter) {

    Assert.notNull(mongoDbFactory);

    this.mongoDbFactory = mongoDbFactory;
    this.exceptionTranslator = mongoDbFactory.getExceptionTranslator();
    this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter(mongoDbFactory) : mongoConverter;
    ...

    // We always have a mapping context in the converter, whether it's a simple one or not
    mappingContext = this.mongoConverter.getMappingContext();
    // We create indexes based on mapping events
    if (null != mappingContext && mappingContext instanceof MongoMappingContext) {
      indexCreator = new MongoPersistentEntityIndexCreator((MongoMappingContext) mappingContext, mongoDbFactory);
      eventPublisher = new MongoMappingEventPublisher(indexCreator);
      if (mappingContext instanceof ApplicationEventPublisherAware) {
        ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
      }
    }
  }

  ...
  //MongoTemplate实现了 ApplicationContextAware,当ApplicationContext被实例化时被感知
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

    prepareIndexCreator(applicationContext);

    eventPublisher = applicationContext;
    if (mappingContext instanceof ApplicationEventPublisherAware) {
      //MappingContext作为事件来源,向ApplicationContext发布
      ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
    }
    resourceLoader = applicationContext;
  }

  ...
  //注入事件监听
  private void prepareIndexCreator(ApplicationContext context) {

    String[] indexCreators = context.getBeanNamesForType(MongoPersistentEntityIndexCreator.class);

    for (String creator : indexCreators) {
      MongoPersistentEntityIndexCreator creatorBean = context.getBean(creator, MongoPersistentEntityIndexCreator.class);
      if (creatorBean.isIndexCreatorFor(mappingContext)) {
        return;
      }
    }

    if (context instanceof ConfigurableApplicationContext) {
      //使 IndexCreator 监听 ApplicationContext的事件
      ((ConfigurableApplicationContext) context).addApplicationListener(indexCreator);
    }
  }

由此可见, MongoTemplate 在初始化时,先通过 MongoConverter 带入 MongoMappingContext,

随后完成一系列初始化,整个过程如下:

  • 实例化 MongoTemplate;
  • 实例化 MongoConverter;
  • 实例化 MongoPersistentEntityIndexCreator;
  • 初始化索引(通过MappingContext已有实体);
  • Repository初始化 -> MappingContext 发布映射事件;
  • ApplicationContext 将事件通知到 IndexCreator;
  • IndexCreator 创建索引

在实例化过程中,没有任何配置可以阻止索引的创建。

四、解决问题

从前面的分析中,可以发现问题关键在 IndexCreator,能否提供一个自定义的实现呢,答案是可以的!

实现的要点如下

  • 实现一个IndexCreator,可继承MongoPersistentEntityIndexCreator,去掉索引的创建功能;
  • 实例化 MongoConverter和 MongoTemplate时,使用一个空的 MongoMappingContext对象避免初始化索引;
  • 将自定义的IndexCreator作为Bean进行注册,这样在prepareIndexCreator方法执行时,原来的 MongoPersistentEntityIndexCreator不会监听ApplicationContext的事件
  • IndexCreator 实现了ApplicationContext监听,接管 MappingEvent事件处理。

实例化Bean

 @Bean
  public MongoMappingContext mappingContext() {
    return new MongoMappingContext();
  }

  // 使用 MappingContext 实例化 MongoTemplate
  @Bean
  public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoMappingContext mappingContext) {
    MappingMongoConverter converter = new MappingMongoConverter(new DefaultDbRefResolver(mongoDbFactory),
        mappingContext);
    converter.setTypeMapper(new DefaultMongoTypeMapper(null));

    MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);

    return mongoTemplate;
  }

自定义IndexCreator

  // 自定义IndexCreator实现
  @Component
  public static class CustomIndexCreator extends MongoPersistentEntityIndexCreator {

    // 构造器引用MappingContext
    public CustomIndexCreator(MongoMappingContext mappingContext, MongoDbFactory mongoDbFactory) {
      super(mappingContext, mongoDbFactory);
    }

    public void onApplicationEvent(MappingContextEvent<?, ?> event) {
      PersistentEntity<?, ?> entity = event.getPersistentEntity();

      // 获得Mongo实体类
      if (entity instanceof MongoPersistentEntity) {
        System.out.println("Detected MongoEntity " + entity.getName());

        //可实现索引处理..
      }
    }
  }

在这里 CustomIndexCreator继承了 MongoPersistentEntityIndexCreator ,将自动接管MappingContextEvent事件的监听。

在业务实现上可以根据需要完成索引的处理!

小结

spring-data-mongo 提供了非常大的便利性,但在灵活性支持上仍然不足。上述的方法实际上有些隐晦,在官方文档中并未提及这样的方式。

ORM-Mapping 框架在实现Schema映射处理时需要考虑校验级别,比如 Hibernate便提供了 none/create/update/validation 多种选择,毕竟这对开发者来说更加友好。

期待 spring-data-mongo 在后续的演进中能尽快完善 Schema的管理功能!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • springboot Mongodb的集成与使用实例详解

    说说springboot与大叔lind.ddd的渊源 Mongodb在Lind.DDD中被二次封装过(大叔的.net和.net core),将它当成是一种仓储来使用,对于开发人员来说只公开curd几个标准的接口即可,而在springboot框架里,它与大叔lind有些类似之处,同样是被二次封装了,开发人员只需要关注自己的业务即可,而标准的curd操作完成由springboot帮助我们来实现,一般地,我们会设计一个与实体对象的接口仓储,让它去继承mongo的标准接口,然后在springboot的依

  • springboot-mongodb的多数据源配置的方法步骤

    在日常工作中,我们可能需要连接多个MongoDB数据源,比如用户库user,日志库log.本章我们来记录连接多个数据源的步骤,以两个数据源为例,多个数据源类推. 1.pom.xml中引入mongodb的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </d

  • SpringBoot实现的Mongodb管理工具使用解析

    项目介绍 Mongodb网页管理工具,基于Spring Boot2.0,前端采用layerUI实现. 源于线上环境部署mongodb时屏蔽了外网访问mongodb,所以使用不了mongochef这样方便的远程连接工具,便Mongodb提供的java api实现的的网页版管理 未设置登录权限相关模块,低耦合性 方便嵌入到现有的项目 部署文档 https://a870439570.github.io/work-doc/mongdb 部分效果图如下 显示所有的数据源 显示指定数据源下的表 源码地址 h

  • springboot配置多数据源的实例(MongoDB主从)

    相信看过上一篇文章的小伙伴已经知道了, 这章要讲的就是MongoDB主从配置. 在这边文章中,你将要学到的是在项目中配置主从数据库,并且兼容其他数据库哟..这些都是博主项目中需要并且比较重要的知识哦~ 好了,废话不多说,直接进主题. 1.pom依赖 <span style="white-space:pre"> </span><dependency> <groupId>org.springframework.boot</groupId

  • Spring + Spring Boot + MyBatis + MongoDB的整合教程

    前言 我之前是学Spring MVC的,后面听同学说Spring Boot挺好用,极力推荐我学这个鬼.一开始,在网上找Spring Boot的学习资料,他们博文写得不是说不好,而是不太详细. 我就在想我要自己写一篇尽可能详细的文章出来,下面话不多说了,来一看看详细的介绍吧. 技术栈 Spring Spring Boot MyBatis MongoDB MySQL 设计模式 MVC 功能 注册(用户完成注册后是默认未激活的,程序有个定时器在检测没有激活的用户,然后发一次邮件提醒用户激活) 登录 发

  • springboot+mongodb 实现按日期分组分页查询功能

    具体代码如下所示: WalletDetailsResp walletDetailsResp = new WalletDetailsResp(); List<WalletDetailsResp.WalletDetail> list = new ArrayList<>(); WalletDetailsResp.PageInfoBean pageInfoBean = new WalletDetailsResp.PageInfoBean(); List<Integer> typ

  • Spring Boot中使用MongoDB的连接池配置的方法

    因为今天开发遇到了性能问题,可能与MongoDB的连接有关,所以稍稍深入看了一下,正好搜到原来有人写过这篇相关的内容,所以转载过来.回头有时间可以写个扩展到SpringForAll里,主体思路还是一样的.感谢这位美女程序媛的文章! 说明 Spring Boot中通过依赖 spring-boot-starter-data-mongodb ,来实现 spring-data-mongodb 的自动配置. 但是默认情况下,Spring Boot 中,并没有像使用MySQL或者Redis一样,提供了连接池

  • SpringBoot MongoDB 索引冲突分析及解决方法

    一.背景 spring-data-mongo 实现了基于 MongoDB 的 ORM-Mapping 能力, 通过一些简单的注解.Query封装以及工具类,就可以通过对象操作来实现集合.文档的增删改查: 在 SpringBoot 体系中,spring-data-mongo 是 MongoDB Java 工具库的不二之选. 二.问题产生 在一次项目问题的追踪中,发现SpringBoot 应用启动失败,报错信息如下: Error creating bean with name 'mongoTempl

  • Spring Boot 多个定时器冲突问题的解决方法

    目录 战术分析 使用场景 问题场景重现 添加注解 配置类 战术分析 上次的博客疏忽了定时器的一个大重点… 实际开发项目中一定不止一个定时器,很多场景都需要用到,而多个定时器带来的问题 : 就是如何避免多个定时器的互相冲突. 推荐一个 Spring Boot 基础教程及实战示例:https://github.com/javastacks/spring-boot-best-practice 使用场景 我们的订单服务,一般会有一个待支付订单,而这个待支付订单是有时间限制的,比如阿里巴巴的订单是五天,淘

  • Android Force Close 出现的异常原因分析及解决方法

    一.原因: forceclose,意为强行关闭,当前应用程序发生了冲突. NullPointExection(空指针),IndexOutOfBoundsException(下标越界),就连Android API使用的顺序错误也可能导致(比如setContentView()之前进行了findViewById()操作)等等一系列未捕获异常 二.如何避免 如何避免弹出Force Close窗口 ,可以实现Thread.UncaughtExceptionHandler接口的uncaughtExcepti

  • Nginx 连接tomcat时会话粘性问题分析及解决方法

    在多台后台服务器的环境下,我们为了确保一个客户只和一台服务器通信,我们势必使用长连接.使用什么方式来实现这种连接呢,常见的有使用nginx自带的ip_hash来做,我想这绝对不是一个好的办法,如果前端是CDN,或者说一个局域网的客户同时访问服务器,导致出现服务器分配不均衡,以及不能保证每次访问都粘滞在同一台服务器.如果基于cookie会是一种什么情形,想想看, 每台电脑都会有不同的cookie,在保持长连接的同时还保证了服务器的压力均衡. 问题分析: 1. 一开始请求过来,没有带session信

  • PHP Header失效的原因分析及解决方法

    在PHP中用header("location:test.php")进行跳转要注意以下几点: 1.location和":"号间不能有空格,否则会出错. 2.在用header前不能有任何的输出,包括include的页面中标签"?>"后不能有空格!! 3.header后的PHP代码还会被执行. 续: 问题:header函数前输入内容 一般来说在header函数前不能输出html内容,类似的还有setcookie() 和 session 函数,这些

  • MySQL 出现错误1418 的原因分析及解决方法

    MySQL 出现错误1418 的原因分析及解决方法 具体错误: 使用mysql创建.调用存储过程,函数以及触发器的时候会有错误符号为1418错误. ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL,or READS SQL DATA in its declaration and binary logging is enabled(you *might* want to use the less safe log

  • PHP中__autoload和Smarty冲突的简单解决方法

    本文讲述了PHP中__autoload和Smarty冲突的简单解决方法.分享给大家供大家参考,具体如下: 一.问题: 最近,在项目中发现,PHP 的 __autoload 方法失效了.调试了好久,百思不得其解,查了下资料才知道原来是 Smarty 的原因.新版的 Smarty 改变了autoload的方式. 二.解决方法: 在 Smarty 的包含类文件后加一段代码,spl_autoload_register("__autoload"); 如下: <?php define('RO

  • Ajax向后台传json格式的数据出现415错误的原因分析及解决方法

    问题描述: ajax往后台传json格式数据报415错误,如下图所示 页面代码 function saveUser(){ var uuId = document.getElementById("uuid").value; var idCard = document.getElementById("idCard").value; alert(uuId+idCard); // var result = new Object(); // result.uuId = uuI

  • 腾讯云ubuntu服务器tomcat访问慢的原因分析及解决方法

    在腾讯云上配了个一元的学生云,开始一切正常,直到配置tomcat开始出现各种莫名其妙的问题.最莫名其妙的是tomcat启动了,端口也 正常监听,安全组也放行端口了,然后问题来了. 用浏览器访问tomcat主页,会发现超级慢,浏览器一直在等待服务器的响应,从这里可以看出能够接入8080端口,但是服务器没有返回数据.(这个问题折腾几天) 后来在网上找了无数资料,终于发现了原因.tomcat8.0在腾讯云ubuntu14.04上有bug. 问题原因: 随机数引起线程阻塞. tomcat不断启动,关闭,

  • java应用cpu占用过高问题分析及解决方法

    使用jstack分析java程序cpu占用率过高的问题 1,使用jps查找出java进程的pid,如3707 2,使用top -p 14292 -H观察该进程中所有线程的CPU占用. [root@cp01-game-dudai-0100.cp01.baidu.com ~]# top -p 14292 -H top - 22:14:13 up 33 days, 7:29, 4 users, load average: 25.68, 32.11, 33.76 Tasks: 113 total, 2

随机推荐