MongoDB 的日常使用

一、简介 

1、 常见的数据库分类

RDBMS(关系型数据库):常见的关系型数据库有 Oracle、DB2、Microsoft SQL Server、Microsoft Access、MySQL;
NoSQL(非关系型数据库):常见的非关系型数据库有 MongoDB、Redis、Voldemort、Cassandra、Riak、Couchbase、CouchDB 等。

1.NoSQL 优点

易扩展:NoSQL 数据库种类繁多,但它们都有一个共同的特点,那就是都去掉了关系型数据库的关系型特性,数据与数据之间没有关系,这样就非常容易扩展,无形之间也在架构的层面上带来了可扩展的能力;
大数据量,高性能:NoSQL 数据库都具有非常高的读写性能,尤其是在处理庞大数据时表现优秀;
灵活:NoSQL 随时都可以存储任意类型的数据,无须提前为要存储的数据建立字段;
高可用:NoSQL 在不太影响性能的情况下,就可以方便地实现高可用的架构,比如 Cassandra、HBase 模型,通过复制模型也能实现高可用。

2.NoSQL 适用场景

数据模型比较简单;
对灵活性要求很强的系统;
对数据库性能要求较高;
不需要高度的数据一致性;
对于给定 key,比较容易映射复杂值的环境。

2、 MongoDB

概述:MongoDB是由C++语言编写的,是一个基于分布式文件存储的非关系型数据库。MongoDB又被称为最像关系型数据库的非关系性数据库。
优势:MongoDB的优势在于可以存放海量数据,具备强大的查询功能,是一个独立的、面向集合文档形式的数据库。

1.MongoDB特点

存储性
面向集合:数据被分组存储在数据集中也被称为集合。每个集合在数据库中都有一个唯一的标识名,并且可以包含多个的文档。
面向文档:存储在集合中的文档,被存储为key-values键对值的形式。键用于唯一标识一个文档,为字符串类型,而值可以是各种的文件类型。
高效二进制数据存储:包括大型对象,例如视频,MongoDB使用二进制格式存储数据,可以保存任何类型的数据对象
操作性
完全索引: 可在任意属性上建立索引,包含内部对象。
强大的聚合工具:如count、group等,支持使用MapReduce完成复杂的聚合任务。
支持Perl、PHP、Java、Python等现在主流开发语言的驱动程序
可用性
支持复制和数据恢复:支持主从复制机制,可以实现数据备份、故障恢复、读扩展等功能,MongoDB数据库的主从复制主挂了从会代替主。
自动处理分片:支持集群自动切分数据,对数据进行分片可以使集群存储更多的数据,实现更大的负载,也可以保证存储的负载均衡。

二、MongoDB的属性和常用语法 

1、属性

1、 数据类型

String  字符串类型,是最常用的数据类型,不过在 MongoDB 中,只有 UTF-8 编码的字符串才是合法的
Integer    整型,用于存储数值。根据您使用服务器的不同,整型可以分为 32 位或 64 位两种
Boolean    布尔型,用于存储布尔类型的值(true/false)
Double 双精度浮点型,用于存储浮点型(小数)数据
Min/Max keys   将一个值与 BSON 元素的最低值和最高值相对比
Array  数组类型,用于将数组、列表或多个值存储在一个键中
Timestamp  时间戳,记录文档修改或添加的具体时间
Object 用于内嵌文档
Null   用于创建空值
Symbol 符号,该数据类型于字符串类型类似,不同的是,它一般用于采用特殊符号类型的语言
Date   日期时间,用 UNIX 时间格式来存储当前日期或时间,您可以创建 Date 对象并将 date、month、year 的值传递给 Date 对象来指定自己的日期时间
Object ID  对象 ID,用于创建文档的 ID
Binary Data    二进制数据,用于存储二进制数据
Code   代码类型,用于在文档中存储 JavaScript 代码
Regular expression 正则表达式类型,用于存储正则表达式

Object ID 类似于关系型数据库中的主键 ID,在 MongoDB 中 Object ID 由 12 字节的字符组成,
601e2b6b aa203c c89f 2d31aa
  ↑      ↑     ↑     ↑
时间戳  机器码 进程id 计数器

Timestamps(时间戳)
与 Date 类型不同,Timestamps 由一个 64 位的值构成,其中:
前 32 位是一个 Unix 时间戳(由 Unix 纪元(1970.1.1)开始到现在经过的秒数);
后 32 位是一秒内的操作序数。

Date
是一个 64 位的对象,其中存放了从 Unix 纪元(1970.1.1)开始到现在经历的毫秒数,Date 类型是有符号的,负值则表示 1970.1.1 之前的时间。

2、 内置角色

1.数据库用户角色:read、readWrite;
2.数据库管理角色:dbAdmin、dbOwner、userAdmin;
3.集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager;
4.备份恢复角色:backup、restore;
5.所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase
6.超级用户角色:root
提供了系统超级用户的访问(dbOwner 、userAdmin、userAdminAnyDatabase)

3、MongoDB默认四个数据库

config: 集群中使用的,分片数据库
admin: 保存用户信息、权限的数据库
local: local的数据只存在于本地数据,不会被同步到其他数据库
test: test为MongoDB默认登录的数据库,只要不知道数据库登录,那么登录的就是test

2、常用语法

1.DDL

创建数据库
use DATABASE_NAME //如果数据库不存在,则创建数据库,否则切换到指定数据库。创建数据库,必须要有数据,否则显示数据库的时候,不会显示。
显示数据库
show dbs //查看所有数据库 ,注意 数据库一定要有数据 不然 无法显示,默认使用test
db //显示当前数据库。
删除数据库
db.dropDatabase() //删除当前数据库,默认为 test,你可以使用 db 命令查看当前数据库名。

查看当前数据所有表(集合)
show tables //只显示有数据的表
show collections //两种方法都可以
创建表(集合)
db.createCollection(name, options)
案例db.createCollection("table2"),注意db是当前数据库,不能写数据库名字。
//options: 可选参数, 指定有关内存大小及索引的选项
删除当前数据库的表(集合)
db.manong.drop()

对某一行数据添加字段
db.manong.update({"age":18},{$set:{'title':'哈哈哈'}})
如果是所有的数据都增加这个字段值为'title':'哈哈哈'
db.manong.update({},{$set:{'title':哈哈哈'}})
对所有数据删除字段
db.manong.update({},{$unset:{'content':''}},false, true)
对指定数据删除字段
db.manong.update({"name": "shell"}, {$unset:{"script_type":"kshell"}})
对某一行数据字段修改字段值
db.manong.update_many({'address': '北京'}, {'$set': {'salary': 9999}})
修改字段名
db.getCollection('集合名').updateMany( {}, { $rename: { "老字段名": "新字段名" } } )
多字段修改
db.manong.updateMany({},{$rename:{"name":"studentName", "age":"studentAge"}});
查看字段
db.manong.find({"age":18}).pretty();

2.DML

db.users.find(): select * from users
db.users.find({ "age" : 27}): select * from users where age = 27
db.users.find({ "username" : "joe", "age" : 27}): select * from users where "username" = "joe" and age = 27
db.users.find({}, { "username" : 1, "email" : 1}): select username, email from users
db.users.find({}, { "username" : 1, "_id" : 0}): //no case // 即时加上了列筛选,_id 也会返回;必须显式的阻止_id 返回
db.users.find({ "age" : {"$gte" : 18, "$lte" : 30}}): select * from users where age >=18 and age <= 30 // $lt(<) $lte(<=) $gt(>) $gte(>=)
db.users.find({ "username" : {"$ne" : "joe"}}): select * from users where username <> "joe"
db.users.find({ "ticket_no" : {"$in" : [725, 542, 390]}}): select * from users where ticket_no in (725, 542, 390)
db.users.find({"ticket_no" : {"$nin" : [725, 542, 390]}}): select * from users where ticket_no not in (725, 542, 390)
db.users.find({"$or" : [{"ticket_no" : 725}, {"winner" : true}]}): select * form users where ticket_no = 725 or winner = true
db.users.find({ "id_num" : {"$mod" : [5, 1]}}): select * from users where (id_num mod 5) = 1
db.users.find({ "$not": {"age" : 27}}): select * from users where not (age = 27)
db.users.find({"username" : {"$in" : [null], "$exists" : true}}): select * from users where username is null // 如果直接通过 find ({"username" : null}) 进行查询,那么连带 "没有 username" 的纪录一并筛选出来
db.users.find({"name" : /joey?/i}):// 正则查询,value 是符合 PCRE 的表达式
db.food.find({fruit : {$all : ["apple", "banana"]}}): // 对数组的查询,字段 fruit 中,既包含 "apple", 又包含 "banana" 的纪录
db.food.find({"fruit.2" : "peach"}): // 对数组的查询,字段 fruit 中,第 3 个 (从 0 开始) 元素是 peach 的纪录
db.food.find({"fruit" : {"$size" : 3}}): // 对数组的查询,查询数组元素个数是 3 的记录,$size 前面无法和其他的操作符复合使用
db.users.findOne(criteria, {"comments" : {"$slice" : 10}}):  // 对数组的查询,只返回数组 comments 中的前十条,还可以 {"$slice" : -10}, {"$slice" : [23, 10]}; 分别返回最后 10 条,和中间 10 条
db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"}): // 嵌套查询
db.blog.find({"comments" : {"$elemMatch" : {"author" : "joe", "score" : {"$gte" : 5}}}}): // 嵌套查询,仅当嵌套的元素是数组时使用,
db.foo.find({"$where" : "this.x + this.y == 10"}): // 复杂的查询,$where 当然是非常方便的,但效率低下。对于复杂查询,考虑的顺序应当是 正则 -> MapReduce -> $where
db.foo.find({"$where" : "function() { return this.x + this.y == 10; }"}): // $where 可以支持 javascript 函数作为查询条件
db.foo.find().sort({"x" : 1}).limit(1).skip(10): // 返回第 (10, 11] 条,按 "x" 进行排序;三个 limit 的顺序是任意的,应该尽量避免 skip 中使用 large-number

 

三、Spring整合MongoDB 

1、 配置


    org.springframework.boot
    spring-boot-starter-data-mongodb
spring:
  data:
    mongodb:
      host: 127.0.0.1
      database: test
      port: 27017
      # 也可用uri   mongodb://127.0.0.1:27017/test

2、JPA 方式

1.创建实体类

public class MyUser {
	@Id
    private String id;

    private String name;

    private Integer age;

    public MyUser() {}

    public MyUser(String id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "MyUser{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 2.接口和测试类

/**
 * 继承 MongoRepository 即可,不要实现
 */
public interface MyUserRepository extends MongoRepository {
}


@SpringBootTest
public class MyUserRepositoryTest {

    @Autowired
    private MyUserRepository myUserRepository;

    /**
     * 插入单条数据
     */
    @Test
    public void insertOne() {
        MyUser user = new MyUser("11", "tom", 18);
        myUserRepository.insert(user);
    }

    /**
     * 插入多条数据
     */
    @Test
    public void insertMany() {
		MyUser user01 = new MyUser("11", "tom", 18);
		MyUser user02 = new MyUser("22", "jake", 22);
        List list = new ArrayList<>();
        list.add(user01);
        list.add(user02);
        myUserRepository.insert(list);
    }
	
	/**
     * 删除操作
     */
    @Test
    public void remove() {
        //根据id删除单个对象
        myUserRepository.deleteById("11");

        //根据字段删除 
        Student student = new Student();
        student.setAge(22);
        myUserRepository.delete(student);

        //根据字段删除多个
        MyUser user02 = new Student();
        user02.setName("jerry");
        List list = new ArrayList<>();
        list.add(user02);
        myUserRepository.deleteAll(list);
		
        //删除所有
        myUserRepository.deleteAll();
    }
	
    /**
     * 修改数据
     */
    @Test
    public void update() {
        Optional op = myUserRepository.findById("11");
        MyUser user = op.get();
        user.setAge(22);
        myUserRepository.save(user);
    }

    /**
     * 查询数据
     */
    @Test
    public void query() {
        //根据Id查询单个对象
        Optional stuOp = myUserRepository.findById("01");
        System.out.println(stuOp.get());

        //根据字段查询单个对象
        MyUser user = new MyUser();
        user.setAge(19);
        Optional stuOp1 = myUserRepository.findOne(Example.of(user));
        System.out.println(stuOp1.get());

        //根据id列表查询多个对象
        Iterable itStu = myUserRepository.findAllById(Arrays.asList(new String[]{"001", "002"}));
        Iterator itor = itStu.iterator();
        while (itor.hasNext())
            System.out.println(itor.next());

        //查询所有
        List all = myUserRepository.findAll();

        //查询所有并排序
        List all1 = myUserRepository.findAll(Sort.by(Sort.Order.desc("studentId"), Sort.Order.asc("studentName")));
        for (MyUser use : all1)
            System.out.println(use);

        //根据条件查询所有并排序
        MyUser user01 = new MyUser();
        user01.setName("tom");
        List all2 = myUserRepository.findAll(Example.of(user01), Sort.by(Sort.Order.desc("studentId"), Sort.Order.asc("studentName")));
        for (MyUser use : all2)
            System.out.println(use);

        //根据条件查询所有并排序,且分页
        Pageable pageable = PageRequest.of(0, 2);
        Page all3 = myUserRepository.findAll(pageable);
        List content = all3.getContent();
        for (MyUser use : content)
            System.out.println(use);
    }

    /**
     * 其他操作
     */
    @Test
    public void other() {
        //count
        long count = myUserRepository.count();
        System.out.println(count);
        MyUser user = new MyUser();
        user.setAge(18);
        long count1 = myUserRepository.count(Example.of(user));
        System.out.println(count1);

        //exists
        boolean exists = myUserRepository.exists(Example.of(user));
        System.out.println(exists);
        boolean b = myUserRepository.existsById("001");
        System.out.println(b);
    }

}

3、MongoTemplate 方式

1.接口

public interface UserDao {

    //插入单个对象
    void addOne(MyUser user);

    //根据id删除单个对象
    void deleteOneById(String  studentId);

    //修改单个对象
    void updateOne(MyUser user);

    //根据id获取单个对象
    MyUser findOneById(String studentId);
    
    //获取全部学生
    List findAll();
}

2.实现类

public class UserDaoImpl implements UserDao {

    @Autowired
    private MongoTemplate mongoTemplate;


    @Override
    public void addOne(MyUser user) {
        mongoTemplate.save(user);
    }

    @Override
    public void deleteOneById(String studentId) {
        MyUser stu = mongoTemplate.findById(studentId, MyUser.class);
        if (stu != null)
            mongoTemplate.remove(stu);

    }

    @Override
    public void updateOne(MyUser user) {
        mongoTemplate.save(user);

    }

    @Override
    public MyUser findOneById(String id) {
        return mongoTemplate.findById(id, MyUser.class);
    }

    @Override
    public List findAll() {
        return mongoTemplate.findAll(MyUser.class);
    }
}

3.MongoTemplate的其它用法

Criteria
eq:等于,第一个参数是对象属性,第二个参数是值
allEq:参数为一个Map对象,相当于多个eq的叠加
gt:大于
ge:大于等于
lt:小于
le:小于等于
between:在两个值之间Expression.between('age',new Integer(10),new Integer(20));
like:like查询
in:in查询

is查询:
Query query = new Query();
// where...is... 相当于 where ? = ?
query.addCriteria(Criteria.where("数据库字段名").is("你的参数"));
// findOne 返回的是一个对象 Class代表你的表对应的映射类
mongoTemplate.findOne(query, Class.class);
// find 返回的是数组
mongoTemplate.find(query, Class.class);

in查询:
ArrayList list = new ArrayList<>();
// list代表你的数据
Query query = Query.query(Criteria.where("数据库字段").in(list));
mongoTemplate.find(query, Class.class);
// 如果想要查询指定的数据是否在数据库的数组中,可以用以下方法。
Query query = Query.query(Criteria.where("数据库字段(数组)").is("你的数组"));

字符模糊查询:
Query query = Query.query(Criteria.where("name").regex("小"));

指定字段不返回:
query.fields().exclude("field");

数组中添加或删除一条数据:
Query query = Query.query(Criteria.where("_id").is("id"));
Update update = new Update(); 
// push方法可以在数组中添加一条数据。
// pull方法可以在数组中删除一条数据。
// update.pull()。
update.push("字段名称", "data");
mongoTemplate.updateFirst(query, update, Class.class);

// 分页
query.limit(10);
// 排序
Sort sort=Sort.by(Sort.Direction.DESC,"timestamp");
query.with(sort);

四、MongoDB的内部

1、索引

Cassandra、Elasticsearch (Lucene)、Google Bigtable、Apache HBase、LevelDB 和 RocksDB 这些当前比较流行的 NoSQL 数据库存储引擎是基于 LSM 开发的。
MongoDB 虽然是 NoSQL 的,但是其存储引擎 Wired Tiger 却是用的 B+Tree。
MongoDB 的索引和 MySql 的索引有点不一样,它的索引在创建时必须指定顺序(1:升序,-1:降序),同时所有的集合都有一个默认索引 _id,这是一个唯一索引,类似 MySql 的主键。

1.索引类型有

单字段索引:建立在单个字段上的索引,索引创建的排序顺序无所谓,MongoDB 可以头/尾开始遍历。
复合索引:建立在多个字段上的索引。
   多 key 索引:我们知道 MongoDB 的一个字段可能是数组,在对这种字段创建索引时,就是多 key 索引。MongoDB 会为数组的每个值创建索引。就是说你可以按照数组里面的值做条件来查询,这个时候依然会走索引。
Hash 索引:按数据的哈希值索引,用在 hash 分片集群上。
地理位置索引:基于经纬度的索引,适合 2D 和 3D 的位置查询。
文本索引: MongoDB 虽然支持全文索引,但是性能低下,暂时不建议使用。

2、集群

1.MongDB 复制集群

MongoDB 高可用的基础是复制集群,复制集群本质来说就是一份数据存多份,保证一台机器挂掉了数据不会丢失。一个副本集至少有 3 个节点组成:
一个主节点(Primary):负责整个集群的写操作入口,主节点挂掉之后会自动选出新的主节点。
一或多个从节点(Secondary):一般是 2 个或以上,从主节点同步数据,在主节点挂掉之后选举新节点。
零个或 1 个仲裁节点(Arbiter):这个是为了节约资源或者多机房容灾用,只负责主节点选举时投票不存数据,保证能有节点获得多数赞成票。
    从上面的节点类型可以看出,一个三节点的复制集群可能是 PSS 或者 PSA 结构。PSA 结构优点是节约成本,但是缺点是 Primary 挂掉之后,一些依赖 majority(多数)特性的写功能出问题,因此一般不建议使用。

2.如何保证数据一致

Journal:Journal日志是 MongoDB 的预写日志 WAL,类似 MySQL 的 redo log,然后100ms一次将Journal 日子刷盘。
Oplog:Oplog 用来做主从复制,类似 MySql 里的 binlog。MongoDB 的写操作都由 Primary 节点负责,Primary 节点会在写数据时会将操作记录在 Oplog 中,Secondary 节点通过拉取 oplog 信息,回放操作实现数据同步的。
Checkpoint:将内存变更刷新到磁盘持久化的过程。MongoDB 会每60s一次将内存中的变更刷盘,并记录当前持久化点(checkpoint),以便数据库在重启后能快速恢复数据。

3.节点选举

MongoDB 的节点选举规则能够保证在Primary挂掉之后选取的新节点一定是集群中数据最全的一个。

4.结论

1.MongoDB 宕机重启之后可以通过 checkpoint 快速恢复上一个 60s 之前的数据。
2.MongoDB 最后一个 checkpoint 到宕机期间的数据可以通过 Journal日志回放恢复。
3.Journal日志因为是 100ms 刷盘一次,因此至多会丢失 100ms 的数据(这个可以通过 WriteConcern 的参数控制不丢失,只是性能会受影响,适合可靠性要求非常严格的场景)
4.如果在写数据开启了多数写,那么就算 Primary 宕机了也是至多丢失 100ms 数据(可避免,同上)

3、分片架构

Config:配置,本质上是一个 MongoDB 的副本集,负责存储集群的各种元数据和配置,如分片地址、chunks 等
Mongos:路由服务,不存具体数据,从 Config 获取集群配置讲请求转发到特定的分片,并且整合分片结果返回给客户端。
Mongod:一般将具体的单个分片叫 mongod,实质上每个分片都是一个单独的复制集群,具备负责集群的高可用特性。

1.数据均衡的实现方式

1.分片集群上数据管理单元叫 chunk,一个 chunk 默认 64M,可选范围 1 ~ 1024M。
2.集群有多少个 chunk,每个 chunk 的范围,每个 chunk 是存在哪个分片上的,这些数据都是存储在 Config 的。
3.chunk 会在其内部包含的数据超过阈值时分裂成两个。
4.MongoDB 在运行时会自定检测不同分片上的 chunk 数,当发现最多和最少的差异超过阈值就会启动 chunk 迁移,使得每个分片上的 chunk 数差不多。
5.chunk 迁移过程叫 rebalance,会比较耗资源,因此一般要把它的执行时间设置到业务低峰期。

2.分片算法

区间分片:可以按 shardkey 做区间查询的分片算法,直接按照 shardkey 的值来分片。

MongoDB 的日常使用_第1张图片

hash分片:用的最多的分片算法,按 shardkey 的 hash 值来分片。hash 分片可以看作一种特殊的区间分片。

MongoDB 的日常使用_第2张图片

分片的本质是将 shardkey 按一定的函数变换 f(x) 之后的空间划分为一个个连续的段,每一段就是一个 chunk。
区间分片 f(x) = x;hash 分片 f(x) = hash(x)
每个 chunk 在空间中起始值是存在 Config 里面的。
当请求到 Mongos 的时候,根据 shardkey 的值算出 f(x) 的具体值为 f(shardkey),找到包含该值的 chunk,然后就能定位到数据的实际位置了。

4、其它特性

1.数据压缩

MongoDB 会自动把客户数据压缩之后再落盘。
Snappy:默认的压缩算法,压缩比 3 ~ 5 倍
Zlib:高度压缩算法,压缩比 5 ~ 7 倍
前缀压缩:索引用的压缩算法,简单理解就是丢掉重复的前缀
zstd:MongoDB 4.2 之后新增的压缩算法,拥有更好的压缩率

2.执行计划 explain 

和 MySql 的 explain 功能相同
queryPlanner:MongoDB 运行查询优化器对当前的查询进行评估并选择一个最佳的查询计划。
exectionStats:mongoDB 运行查询优化器对当前的查询进行评估并选择一个最佳的查询计划进行执行。在执行完毕后返回这个最佳执行计划执行完成时的相关统计信息。
allPlansExecution:即按照最佳的执行计划执行以及列出统计信息,如果有多个查询计划,还会列出这些非最佳执行计划部分的统计信息。。

你可能感兴趣的:(mongodb,数据库)