SpringBoot-MongoDB 索引冲突分析及解决

2020 年 2 月 17 日

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);        }    }
复制代码


  1. 在接收到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);        }    }
复制代码


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 的管理功能!


2020 年 2 月 17 日 11:31412

评论

发布
暂无评论
发现更多内容

3面抖音犹如开挂,一周直接拿下offer,全靠这份啃了两个月「Java进阶手册」+[Java面试宝典]

云流

编程 程序员 计算机 java面试

jenkins实现接口自动化持续集成(python+pytest+ Allure+git)

行者AI

接口自动化测试的实现

行者AI

浅谈 WebRTC 的 Audio 在进入 Encoder 之前的处理流程

阿里云视频云

WebRTC 音频技术 音视频算法 音频

软件测试中需要使用的工具

测试人生路

软件测试

得物App亮相QCon全球软件开发大会,分享百倍增长背后的技术力量

得物技术

效率 技术 得物 得物技术 Qcon

小程序市场的「App Store」来了!你准备好吃“螃蟹”了吗?

蚂蚁集团移动开发平台 mPaaS

小程序生态 mPaaS appstore

半个多月时间4面阿里,已经成功拿下offer,分享一下个人面经

Java成神之路

Java 程序员 架构 面试 编程语言

华为全栈AI技术干货深度解析,解锁企业AI开发“秘籍”

华为云开发者社区

AI 全栈 开发

双循环背景下的全球供应链机遇与挑战

CECBC区块链专委会

供应链物流

XDAG技术详解1

老五

盘点 2020 |协作,是另外一种常态

Winfield

领域驱动设计 DDD 协作 远程协作 盘点2020

AOFEX交易所APP系统开发|AOFEX交易所软件开发

开發I852946OIIO

系统开发

高光时刻!美团推出Spring源码进阶宝典:脑图+视频+文档

996小迁

spring 源码 架构 笔记

拼多多五面面经(Java岗),全面涵盖Java基础到高并发级别

Java成神之路

Java 程序员 架构 面试 编程语言

【得物技术】如何测试概率性事件-二项分布置信区间

得物技术

测试 开发 概率 得物 得物技术

5年Java高工经验,我是如何成功拿下滴滴D7Offer的?

Java架构追梦

Java 学习 架构 面试 滴滴

Rust太难?那是你没看到这套Rust语言学习万字指南!

华为云开发者社区

rust 语言 开发语言

腾讯五面、快手三面已拿offer(Java岗位),分享个人面经

Java成神之路

Java 程序员 架构 面试 编程语言

15天成功拿到阿里offer 我是如何逆袭成功?全靠“Java程序员面试笔试通关宝典”真够可以!

比伯

Java 编程 架构 面试 程序人生

《迅雷链精品课》第十三课:PBFT算法

迅雷链

区块链

资深码农:拿下软件测试,只需掌握好这两种方法!

华为云开发者社区

软件 工具 测试

LeetCode题解:42. 接雨水,动态规划,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

为什么要在以太坊上构建去中心化缓存层?到底要怎样做呢?

CECBC区块链专委会

以太坊

Locust快速上手指南

行者AI

接口自动化传值处理

行者AI

阿里三面,复盘总结55题:java基础+分布式+网络+架构设计

Java成神之路

Java 程序员 架构 面试 编程语言

自定义TBE算子入门,不妨从单算子开发开始

华为云开发者社区

算法 算子 自定义

普本开发三年,每天两小时面试备战,2个月后五面阿里定级P7

Java架构之路

Java 程序员 架构 面试 编程语言

美团五面+滴滴四面,复盘总结117道面试题,大厂套路展露无遗

Java架构之路

Java 程序员 架构 面试 编程语言

如何从危机中提炼总结,做好2020年的复盘?

CECBC区块链专委会

复盘 经济

SpringBoot-MongoDB 索引冲突分析及解决-InfoQ