MongoDB学习

351 阅读11分钟

MongoDB

什么是MongoDB

MongoDB是一个开源的文档型数据库管理系统,它使用类似于JSON的BSON格式存储数据。MongoDB的设计目标是提供高性能、高可用性和易于扩展的数据库。它可以处理大量的数据和高并发的请求。

MongoDB是面向文档的数据库,文档是一个由键值对组成的数据结构,其中键是字符串,值可以是各种数据类型,包括其他文档、数组和原始数据类型。MongoDB使用集合来组织文档,集合类似于关系数据库中的表。

MongoDB支持动态模式,这意味着可以在不事先定义模式的情况下存储数据。这种灵活性使得MongoDB在快速应用开发和大量变化的环境中非常有用。

MongoDB提供了丰富的查询功能,可以使用类似于SQL的查询语言进行查询,或者使用JavaScript编写查询。还提供了MapReduce功能,可以对大量的数据进行计算和分析。

MongoDB可以在多台服务器上运行,并提供了高可用性和自动分片功能,使得它非常适合在大规模分布式环境中使用。

MySQL、MongoDB读写对比

相对于 MySQL,MongoDB在读写性能上的表现优秀,主要有以下几个方面:

  1. 写入性能:MongoDB相对于MySQL,使用了另一种存储方式,MMap和WiredTiger,在写入时使用了内存映射文件和高级数据结构和算法,因此,在多线程写入和高并发写入时表现优秀。

  2. 索引性能:MongoDB支持多种类型索引,包括单键索引、复合索引、地理空间索引,支持区间查询和正则表达式查询,查询性能相对于MySQL表现更优。

  3. 缓存性能:MongoDB支持数据缓存,可以将经常访问的数据缓存到内存中,而MySQL使用的是查询缓存,可以缓存整个查询,因此,MongoDB在查询性能上更容易达到内存级别的性能。

    MongoDB支持在内存中缓存数据,称为WiredTiger引擎的内存映射文件缓存。当MongoDB启动时,会预留一个固定大小的内存缓存池,用于存储索引和数据文件的页。在应用程序操作数据时,MongoDB会根据需要将数据从磁盘读入该内存缓存池中。同时,当数据被修改时,MongoDB会将这些修改写入日志文件和在内存中对应的页中。这种方式能以较少的磁盘I/O访问更多的数据,改善系统读写性能,缩短操作响应时间。

  4. 分片和负载均衡:MongoDB支持水平扩展,可以对数据进行分片,实现负载均衡和高可用性。

总体而言,MongoDB在写入性能、查询性能和扩展性上表现优异,而且它的灵活性和可配置性也很高。相对于MySQL,它更适合大规模数据、高并发读写的场景。但是,在数据一致性和对SQL语言的支持方面,MySQL仍然则表现更优。因此,针对具体场景和业务需求做出合适的选择。

什么是WiredTiger

WiredTiger是一款高性能、高度可扩展的开源数据库存储引擎,它主要运行在大规模、高并发的生产环境中。WiredTiger由Grammarscope公司开发,现已被MongoDB收购并成为了MongoDB的默认存储引擎(从MongoDB 3.2版本开始)。

作为一款多线程、事务性存储引擎,WiredTiger可以支持多种持久化和压缩技术。它包含了许多优秀的功能,如易于配置、高效的索引、更好地管理内存使用和多版本并发控制(MVCC),可以帮助开发人员更好地处理大型、高性能的数据访问。下面是WiredTiger的一些主要特点:

  1. ACID事务支持:WiredTiger可以保证事务的原子性、一致性、隔离性和持久性,能够处理高并发请求,并减少竞争条件的发生。
  2. 内存映射文件缓存:WiredTiger可以将经常访问的数据缓存到内存中,使用内存映射文件缓存技术,从而大大减少磁盘I/O的次数,提升读写速度。
  3. B树索引优化:WiredTiger使用B树索引来优化索引读取性能,提升磁盘页面的数据访问。
  4. 可压缩性:WiredTiger可以存储和查询压缩的数据,从而节省磁盘空间和提高读写性能。
  5. 支持多版本并发控制(MVCC):WiredTiger实现了多版本并发控制来处理读写锁等并发问题,避免数据竞争和锁等问题影响性能。

总之,WiredTiger是一款性能强大、高度可扩展、易于使用的存储引擎。在处理大量数据和高并发请求的需求下,WiredTiger有着非常好的表现,可以帮助开发人员更好地管理和处理数据,保持高效和可靠。

MongoDB的索引有什么优势

  • MongoDB的索引是基于B-Tree结构实现的,异步更新索引方式也是按照缓存的方式在内存中对索引维护进行操作的
  • MongoDB的索引更新机制被称为“rebuild”方式,这是指MongoDB在索引建立过程中,不是实时在磁盘上修改索引文件,而是在内存中创建一个缓存,缓存记录了所有需要修改的内容,在达到一定条件后,将缓存刷新到磁盘上。
  • 在使用“rebuild”方式更新索引时,MongoDB会在写入操作完成之后,将写入日志记录到内存中的操作服务于缓存中的索引文件,以避免索引文件损坏,并且MongoDB会定期刷新内存中缓存的索引,保障索引的可靠性和一致性。

MongoDB是如何来缓存的

  • MongoDB使用一种称为WiredTiger Storage Engine(从MongoDB 3.2版本开始成为默认存储引擎)的存储引擎来管理数据。

    在WiredTiger中,页是存储和管理数据的基本单位。每个页是4KB大小(可通过特定配置改变),MongoDB会将数据按页的形式加载到内存中。MongoDB通过维护一个页的使用率指标(Page Utilization)来判断页的使用情况。该指标是由WiredTiger引擎统计并维护的。

  • WiredTiger基于页的使用率来决定何时将页写回到磁盘上。例如,当页面使用率低于一定阈值时,WiredTiger可以选择将其从内存中移除并写回到磁盘上,以节省内存的开销。通过这种方式,MongoDB能够避免将大量未使用的数据保留在内存中,从而使系统更加高效。

  • 此外,WiredTiger还提供了一些其他的页管理技术,例如数据压缩、多版本并发控制(MVCC)、检查点等功能,帮助MongoDB更好地管理数据,并提升数据读写性能。

内存映射文件缓存

内存映射文件缓存是一种将文件内容直接映射到内存的缓存技术,常被应用在数据库等各种需要频繁读取文件的场景中。MongoDB的WiredTiger存储引擎就是使用了内存映射文件缓存来优化数据库读写性能。

下面是一些内存映射文件缓存的相关概念:

  1. 内存映射(Memory Mapping):内存映射是一种将文件在磁盘上的内容映射到内存中的技术。应用程序看到的文件内容不再是来自磁盘,而是直接从内存中读取,这样可以大大提高文件访问速度。
  2. 页(Page):当文件被映射到内存中后,操作系统会将文件物理块的大小划分为若干个固定大小的页,MongoDB中的页大小为4KB。当应用程序读取文件时,页级别的访问可以将数据读取到内存中,加速数据读取。
  3. 缓存池(Buffer Pool):缓存池是一个由操作系统分配的内存区域。MongoDB会向操作系统请求一定量的内存来作为缓存池,用于存储页的内容,从而避免了由应用程序自己管理内存的麻烦。缓存池大小是MongoDB读写速度的重要参数,越大的缓存池可以存储更多的页,应用程序会更快地访问数据。
  4. 页面换入/换出(Page Fault):数据映射到内存后,当应用程序不断地在缓存池中访问数据时,有些页可能会被替换出缓存池。当应用程序再次访问这些被换出的页时,操作系统会将它们再次读入内存中,这个过程就称为“Page Fault”。

内存映射文件缓存技术可以将文件数据缓存到内存中,减少了文件读取操作的次数,从而提高了读取效率和响应速度。通过命中缓存,MongoDB可以更快速地读写数据,并且可以更好地适应大型的数据访问量。

spring boot整合MongoDB

SQL术语/概念MongoDB术语/概念解释/说明
databasedatabase数据库
tablecollection数据库表/集合
rowdocument数据记录行/文档
columnfield数据字段/域
indexindex索引
table joins表连接,MongoDB不支持
primary keyprimary key主键,MongoDB自动将_id字段设置为主键

使⽤ Spring Boot 进⾏ MongoDB 连接,需要使⽤ spring-boot-starter-data-mongodb 包。spring-bootstarter-data-mongodb 继承 Spring Data 的通⽤功能外,针对 MongoDB 的特性开发了很多定制的功能,让使⽤ Spring Boot 操作 MongoDB 更加简便。

Spring Boot 操作 MongoDB 有两种⽐较流⾏的使⽤⽅法:

  • 将 MongoTemplate 直接注⼊到 Dao 中使⽤
  • ⼀继承 MongoRepository,MongoRepository 内置了很多⽅法可直接使⽤。

MongoTemplate

添加 spring-boot-starter-data-mongodb 包引⽤:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

application.properties配置

spring.data.mongodb.uri=mongodb://test:test@localhost:27017/test

如果是集群:

spring.data.mongodb.uri=mongodb://user:pwd@ip1:port1,ip2:port2/database

密码和⽤户名如果没有设置可以不⽤添加:

spring.data.mongodb.uri=mongodb://localhost:27017/test

创建数据实体类:

public class User implements Serializable {
 private static final long serialVersionUID = -3258839839160856613L;
 private Long id;
 private String userName;
 private String passWord;
 //getter、setter 省略
}

Repository 层实现了 User 对象的增、删、改、查功能。

public interface UserRepository  {
    public void saveUser(User user);
​
    public User findUserByUserName(String userName);
​
    public long updateUser(User user);
​
    public void deleteUserById(Long id);
}

在实现类中使用MongoTemplate对MongoDB进行增删改查的操作。

@Component
public class UserRepositoryImpl implements UserRepository {
​
​
   //注入MongoTemplate
    @Autowired
    private MongoTemplate mongoTemplate;
​
    /**
     * 添加数据
     * save ⽅法中会进⾏判断,如果对象包含了 ID 信息就会进⾏更新,如果没有包含 ID 信息就⾃动保存。
     * @param user
     */
    @Override
    public void saveUser(User user) {
        mongoTemplate.save(user);
    }
​
    /**
     * 根据⽤户名查询对象
     * @param userName
     * @return
     */
    @Override
    public User findUserByUserName(String userName) {
        Query query=new Query(Criteria.where("userName").is(userName));
        User user = mongoTemplate.findOne(query , User.class);
        return user;
    }
​
​
    /**
     * 更新对象
     * 可以选择更新⼀条数据,或者更新多条数据
     * @param user
     * @return
     */
    @Override
    public long updateUser(User user) {
        Query query=new Query(Criteria.where("id").is(user.getId()));
        Update update= new Update().set("userName", user.getUserName()).set("passWord"
                , user.getPassWord());
        //更新查询返回结果集的第⼀条
        UpdateResult result =mongoTemplate.updateFirst(query,update,User.class);
        //更新查询返回结果集的所有
        // mongoTemplate.updateMulti(query,update,UserEntity.class);
        if(result!=null)
            return result.getMatchedCount();
        else
            return 0;
    }
​
    /**
     * 删除对象
     * @param id
     */
    @Override
    public void deleteUserById(Long id) {
        Query query=new Query(Criteria.where("id").is(id));
        mongoTemplate.remove(query,User.class);
    }
}

MongoRepository

MongoRepository 继承于 PagingAndSortingRepository,再往上就是 CrudRepository, MongoRepository 和JPA、Elasticsearch 的使⽤⽐较类似,都是 Spring Data 家族的产品,最终使 ⽤⽅法也就和 JPA、ElasticSearch 的使⽤⽅式类似,可以根据⽅法名⾃动⽣成 SQL 来查询。

新建 UserRepository 需要继承 MongoRepository,这样就可直接使⽤ MongoRepository 的内置⽅法

public interface UserRepository extends MongoRepository<User,Long> {
    User findByUserName(String userName);
    //分页查询
    Page<User> findAll(Pageable var1);
}

MongoDB多数据源

添加依赖

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

在配置⽂件中添加不同的数据源:

mongodb.primary.uri=mongodb://localhost:27017
mongodb.primary.database=primary
mongodb.secondary.uri=mongodb://localhost:27017
mongodb.secondary.database=secondary

集群可以采用以下配置:

mongodb.xxx.uri=mongodb://192.168.0.1:27017,192.168.0.2:27017,192.168.0.3:27017
mongodb.xxx.database=xxx

配置数据源

封装读取以 Mongodb 开头的两个配置文件:

@ConfigurationProperties(prefix = "mongodb")
public class MultipleMongoProperties {
 private MongoProperties primary = new MongoProperties();
 private MongoProperties secondary = new MongoProperties();
 //省略 getter、setter
}

创建 MultipleMongoConfig 类分别创建两个数据源的 MongoTemplate:

Configuration
public class MultipleMongoConfig {
​
    @Autowired
    private MultipleMongoProperties mongoProperties;
​
    /**
     * 利⽤构建好的 MongoDbFactory 创建对应的 MongoTemplate
     * @return
     * @throws Exception
     */
    @Primary
    @Bean(name = "primaryMongoTemplate")
    public MongoTemplate primaryMongoTemplate() throws Exception {
        return new MongoTemplate(primaryFactory(this.mongoProperties.getPrimary()));
    }
​
    @Bean
    @Qualifier("secondaryMongoTemplate")
    public MongoTemplate secondaryMongoTemplate() throws Exception {
        return new MongoTemplate(secondaryFactory(this.mongoProperties.getSecondary()));
    }
​
    /**
     * 根据配置⽂件信息构建第⼀个数据源的 MongoDbFactory
     * @param mongo
     * @return
     * @throws Exception
     */
    @Bean
    @Primary
    public MongoDbFactory primaryFactory(MongoProperties mongo) throws Exception {
        MongoClient client = new MongoClient(new MongoClientURI(mongoProperties.getPrimary().getUri()));
        return new SimpleMongoDbFactory(client, mongoProperties.getPrimary().getDatabase());
    }
​
​
    @Bean
    public MongoDbFactory secondaryFactory(MongoProperties mongo) throws Exception {
        MongoClient client = new MongoClient(new MongoClientURI(mongoProperties.getSecondary().getUri()));
        return new SimpleMongoDbFactory(client, mongoProperties.getSecondary().getDatabase());
    }
}

配置不同包路径下使用不同的数据源

第一个库的封装:

@Configuration
@EnableConfigurationProperties(MultipleMongoProperties.class)
@EnableMongoRepositories(basePackages = "edu.hpu.repository.primary",
        mongoTemplateRef = "primaryMongoTemplate")
public class PrimaryMongoConfig {
}

第二个库的封装:

@Configuration
@EnableMongoRepositories(basePackages = "edu.hpu.repository.secondary",
        mongoTemplateRef = SecondaryMongoConfig.MONGO_TEMPLATE)
public class SecondaryMongoConfig {
​
    protected static final String MONGO_TEMPLATE = "secondaryMongoTemplate";
}

Repository

在edu.hpu.repository.primary、edu.hpu.repository.secondary包下分别创建两个Repository。

public interface PrimaryRepository extends MongoRepository<User, String> {
}
public interface SecondaryRepository extends MongoRepository<User,String> {
}

测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class RepositoryTest {
​
    @Autowired
    private PrimaryRepository primaryRepository;
​
    @Autowired
    private SecondaryRepository secondaryRepository;
​
    @Test
    public void TestSave() {
        //分别向两个库中存入数据
        this.primaryRepository.save(new User("01","张三", "123456"));
        this.secondaryRepository.save(new User("02","李四", "654321"));
        //从第一个库中取出数据
        List<User> primaries = this.primaryRepository.findAll();
        for (User primary : primaries) {
            System.out.println(primary.toString());
        }
        //从第二个库中取出数据
        List<User> secondaries = this.secondaryRepository.findAll();
        for (User secondary : secondaries) {
            System.out.println(secondary.toString());
        }
    }
}