DSLBQ'S_BLOG

分类: 数据库

  • MongoDB

    MongoDB

    简介

    说明

    • 官方MongoDB是一个文档数据库,旨在方便应用开发和扩展。
    • 百度百科

    MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展高性能数据存储解决方案

    MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型 。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引

    总结: mongoDB 是一个非关系型文档数据库

    历史

    • 2009年2月,MongoDB数据库首次在数据库领域亮相,打破了关系型数据库一统天下的局面;
    • 2010年8月, MongoDB 1.6发布。这个版本最大的一个功能就是Sharding,自动分片
    • 2014年12月, MongoDB3.0发布。由于收购了WiredTiger 存储引擎,大幅提升了MongoDB的写入性能;
    • 2015年12月,3.2版本发布,开始支持了关系型数据库的核心功能:关联。你可以一次同时查询多个MongoDB的集合。
    • 2016年, MongoDB推出Atlas,在AWS、 Azure 和GCP上的MongoDB托管服务;
    • 2017年10月,MongoDB成功在纳斯达克敲钟,成为26年来第一家以数据库产品为主要业务的上市公司。
    • 2018年6月, MongoDB4.0 发布推出ACID事务支持,成为第一个支持强事务的NoSQL数据库;
    • 2018年–至今,MongoDB已经从一个在数据库领域籍籍无名的“小透明”,变成了话题度和热度都很高的“流量”数据库。

    特点

    特点

    • 面向集合存储,易存储对象类型的数据
    • 支持查询,以及动态查询
    • 支持RUBY,PYTHON,JAVA,C++,PHP,C#等多种语言
    • 文件存储格式为BSON(一种JSON的扩展)
    • 支持复制和故障恢复和分片
    • 支持事务支持
    • 索引 聚合 关联….

    应用场景

    • 游戏应用:使用云数据库MongoDB作为游戏服务器的数据库存储用户信息。用户的游戏装备、积分等直接以内嵌文档的形式存储,方便进行查询与更新。
    • 物流应用:使用云数据库MongoDB存储订单信息,订单状态在运送过程中会不断更新,以云数据库MongoDB内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来,方便快捷且一目了然。
    • 社交应用:使用云数据库MongoDB存储用户信息以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。并且,云数据库MongoDB非常适合用来存储聊天记录,因为它提供了非常丰富的查询,并在写入和读取方面都相对较快。
    • 视频直播:使用云数据库MongoDB存储用户信息、礼物信息等。
    • 大数据应用:使用云数据库MongoDB作为大数据的云存储系统,随时进行数据提取分析,掌握行业动态。

    安装

    传统方式

    # 1.下载 MongoDB - https://www.mongodb.com/try/download/community

    # 2.将下载安装包上传到 linux 系统 - tar -zxf mongodb-linux-aarch64-ubuntu2004-5.0.5.tgz

    # 3.查看安装目录 - ls `bin`目录   用来存放启动mongoDB的服务以及客户端链接的脚本文件等

    # 4.启动 MongoDB 服务 - ./mongod --port=27017 --dbpath=../data --logpath=../logs/mongo.log `--port`   指定服务监听端口号 默认为 27017 `--dbpath` 指定 mongodb 数据存放目录 启动要求目录必须存在 `--logpath` 指定 mongodb 日志文件存放位置

    注意: 由于指定日志文件因此启动时日志输出到日志中终端不显示任何日志


    # 5.客户端连接 - ./mongo --port=27017

    Docker方式

    # 1.拉取 mongodb 镜像 - docker pull mongo:5.0.5

    # 2.运行 mongo 镜像 - docker run -d --name mongo --p 27017:27017 mongo:5.0.5

    # 3.进入 mongo 容器 - docker exec -it bc6c bash

    核心概念

    库<DataBase>

    mongodb中的库就类似于传统关系型数据库中库的概念,用来通过不同库隔离不同应用数据。mongodb中可以建立多个数据库。每一个库都有自己的集合和权限,不同的数据库也放置在不同的文件中。默认的数据库为”test”,数据库存储在启动指定的data目录中。

    集合<Collection>

    集合就是 MongoDB 文档组,类似于 RDBMS (关系数据库管理系统:Relational Database Management System)中的表的概念。

    集合存在于数据库中,一个库中可以创建多个集合。每个集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。

    文档<Document>

    文档集合中一条条记录,是一组键值(key-value)对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。

    一个简单的文档例子如下:

    {"site":"www.baizhiedu.xin", "name":"编程不良人"}

    关系总结

    RDBMSMongoDB
    数据库<database>数据库<database>
    表<table>集合<collection>
    行<row>文档<document>
    列<colume>字段<field>

    基本操作

    库<database>

    • 查看所有库
      > show databases; | show dbs;

      注意

      • admin: 从权限的角度来看,这是”root”数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
      • local: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
      • config: 当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
    • 创建数据库
      >use 库名

      注意: use 代表创建并使用,当库中没有数据时默认不显示这个库

    • 删除数据库
      • 默认删除当前选中的库
      > db.dropDatabase()
    • 查看当前所在库
      > db;

    集合<Collection>

    • 查看库中所有集合
      > show collections; | show tables;
    • 创建集合
      > db.createCollection('集合名称', [options])
    • options可以是如下参数:

    字段类型描述
    capped布尔(可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 当该值为 true 时,必须指定 size 参数。
    size数值(可选)为固定集合指定一个最大值,即字节数。 如果 capped 为 true,也需要指定该字段。
    max数值(可选)指定固定集合中包含文档的最大数量。

    注意:当集合不存在时,向集合中插入文档也会自动创建该集合。

    • 删除集合
    > db.集合名称.drop();

    文档<document>

    参考文档: https://docs.mongodb.com/manual/reference/method/

    • 插入文档
      • 单条文档
        > db.集合名称.insert({"name":"编程不良人","age":23,"bir":"2012-12-12"});
      • 多条文档
        > db.集合名称.insertMany(
           [ <document 1> , <document 2>, ... ],
           {
         			writeConcern: 1,//写入策略,默认为 1,即要求确认写操作,0 是不要求。
              ordered: true //指定是否按顺序写入,默认 true,按顺序写入。
           }
        )
        > db.集合名称.insert([
          	{"name":"不良人","age":23,"bir":"2012-12-12"},
          	{"name":"小黑","age":25,"bir":"2012-12-12"}
        ]);
        
      • 脚本方式
        for(let i=0;i<100;i++){     
            db.users.insert(
                {"_id":i,"name":"编程不良人_"+i,"age":23}
            ); 
        }
        
      注意:在 mongodb 中每个文档都会有一个_id作为唯一标识,_id默认会自动生成如果手动指定将使用手动指定的值作为_id 的值。
    • 查询所有
      > db.集合名称.find();
    • 删除文档
      db.集合名称.remove(
         <query>,
         {
           justOne: <boolean>,
           writeConcern: <document>
         }
      )
      参数说明:
      • query :可选删除的文档的条件。
      • justOne : 可选如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
      • writeConcern :可选抛出异常的级别。
    • 更新文档
      db.集合名称.update(
         <query>,
         <update>,
         {
           upsert: <boolean>,
           multi: <boolean>,
           writeConcern: <document>
         }
      );
      参数说明:
      • query : update的查询条件,类似sql update查询内where后面的。
      • update : update的对象和一些更新的操作符(如$,$inc…)等,也可以理解为sql update查询内set后面的
      • upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
      • multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
      • writeConcern :可选,抛出异常的级别。
      - db.集合名称.update({"name":"zhangsan"},{name:"11",bir:new date()}) 
      	`这个更新是将符合条件的全部更新成后面的文档,相当于先删除在更新`
      - db.集合名称.update({"name":"xiaohei"},{$set:{name:"mingming"}})
      	`保留原来数据更新,但是只更新符合条件的第一条数据`
      - db.集合名称.update({name:”小黑”},{$set:{name:”小明”}},{multi:true})		
      	`保留原来数据更新,更新符合条件的所有数据`
      - db.集合名称.update({name:”小黑”},{$set:{name:”小明”}},{multi:true,upsert:true})
      	`保留原来数据更新,更新符合条件的所有数据 没有条件符合时插入数据
      );

    文档查询

    MongoDB 查询文档使用 find() 方法。find() 方法以非结构化的方式来显示所有文档。

    语法

    > db.集合名称.find(query, projection)
    • query可选,使用查询操作符指定查询条件
    • projection可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。

    如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下:

    > db.集合名称.find().pretty()

    注意: pretty() 方法以格式化的方式来显示所有文档。

    对比语法

    如果你熟悉常规的 SQL 数据,通过下表可以更好的理解 MongoDB 的条件语句查询:

    AND

    > db.集合名称.find({key1:value1, key2:value2,...}).pretty()

    类似于 WHERE 语句:WHERE key1=value1 AND key2=value2

    OR

    MongoDB OR 条件语句使用了关键字 $or,语法格式如下:

    > db.集合名称.find(
       {
          $or: [
             {key1: value1}, {key2:value2}
          ]
       }
    ).pretty()

    类似于 WHERE 语句:WHERE key1=value1 or key2=value2

    AND 和 OR 联合

    类似SQL语句为:’where age >50 AND (name = ‘编程不良人’ OR name = ‘MongoDB’)’

    > db.集合名称.find({"age": {$gt:50}, $or: [{"name": "编程不良人"},{"name": "MongoDB"}]}).pretty();

    数组中查询

    -- 测试数据
    > db.集合名称.insert({ "_id" : 11, "age" : 29, "likes" : [ "看电视", "读书xx", "美女" ], "name" : "不良人_xx_11" })
    -- 执行数组查询
    > db.users.find({likes:"看电视"})
    -- $size 按照数组长度查询
    > db.users.find({likes:{$size:3}});

    模糊查询

    类似 SQL 中为 ‘where name like ‘%name%”

    > db.users.find({likes:/良/});

    注意:在 mongoDB 中使用正则表达式可以是实现近似模糊查询功能

    排序

    > db.集合名称.find().sort({name:1,age:1}),
    - 1 升序  -1 降序

    类似 SQL 语句为: ‘order by name,age’

    分页

    > db.集合名称.find().sort({条件}).skip(start).limit(rows);

    类似于 SQL 语句为: ‘limit start,rows’

    总条数

    > db.集合名称.count();
    > db.集合名称.find({"name":"编程不良人"}).count();

    类似于 SQL 语句为: ‘select count(id) from ….’

    去重

    > db.集合名称.distinct('字段')

    类似于 SQL 语句为: ‘select distinct name from ….’

    指定返回字段

    > db.集合名称.find({条件},{name:1,age:1}) 
    - 参数2: 1 返回  0 不返回    `注意:1和0不能同时使用`

    $type

    说明

    $type操作符是基于BSON类型来检索集合中匹配的数据类型,并返回结果。

    MongoDB 中可以使用的类型如下表所示:

    使用

    > db.col.insert({
        title: 'PHP 教程', 
        description: 'PHP 是一种创建动态交互性站点的强有力的服务器端脚本语言。',
        by: '编程不良人',
        url: 'http://www.baizhiedu.xin',
        tags: ['php'],
        likes: 200
    });
    ​
    > db.col.insert({
        title: 'Java 教程', 
        description: 'Java 是由Sun Microsystems公司于1995年5月推出的高级程序设计语言。',
        by: '编程不良人',
        url: 'http://www.baizhiedu.xin',
        tags: ['java'],
        likes: 550
    });
    ​
    > db.col.insert({
        title: 'MongoDB 教程', 
        description: 'MongoDB 是一个 Nosql 数据库',
        by: '编程不良人',
        url: 'http://www.baizhiedu.xin',
        tags: ['mongodb'],
        likes: 100
    });
    ​
    > db.col.insert({
        title: 2233, 
        description: '2233 是一个 B站的',
        by: '编程不良人',
        url: 'http://www.baizhiedu.xin',
        tags: ['2233'],
        likes: 100
    });
    • 如果想获取 “col” 集合中 title 为 String 的数据,你可以使用以下命令:
    db.col.find({"title" : {$type : 2}}).pretty();
    或
    db.col.find({"title" : {$type : 'string'}}).pretty();
    • 如果想获取 “col” 集合中 tags 为 Array 的数据,你可以使用以下命令:
    dge
    或
    db.col.find({"tags" : {$type : 'array'}}).pretty();

    索引<index>

    https://docs.mongodb.com/manual/indexes/

    说明

    索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构。

    原理

    从根本上说,MongoDB中的索引与其他数据库系统中的索引类似。MongoDB在集合层面上定义了索引,并支持对MongoDB集合中的任何字段或文档的子字段进行索引。

    操作

    0、创建索引

    > db.集合名称.createIndex(keys, options)
    > db.集合名称.createIndex({"title":1,"description":-1})

    说明: 语法中 Key 值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。

    createIndex() 接收可选参数,可选参数列表如下:

    ParameterTypeDescription
    backgroundBoolean建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 “background” 可选参数。 “background” 默认值为false
    uniqueBoolean建立的索引是否唯一。指定为true创建唯一索引。默认值为false.
    namestring索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。
    sparseBoolean对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false.
    expireAfterSecondsinteger指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。
    vindex version索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。
    weightsdocument索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。
    default_languagestring对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语
    language_overridestring对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language.

    1、查看集合索引

    > db.集合名称.getIndexes()

    2、查看集合索引大小

    > db.集合名称.totalIndexSize()

    3、删除集合所有索引

    > db.集合名称.dropIndexes()

    4、删除集合指定索引

    > db.集合名称.dropIndex("索引名称")

    复合索引

    说明: 一个索引的值是由多个 key 进行维护的索引的称之为复合索引

    > db.集合名称.createIndex({"title":1,"description":-1})

    注意: mongoDB 中复合索引和传统关系型数据库一致都是左前缀原则

    聚合<aggregate>

    说明

    MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似 SQL 语句中的 count(*)

    使用

    {
       title: 'MongoDB Overview', 
       description: 'MongoDB is no sql database',
       by_user: 'runoob.com',
       url: 'http://www.runoob.com',
       tags: ['mongodb', 'database', 'NoSQL'],
       likes: 100
    },
    {
       title: 'NoSQL Overview', 
       description: 'No sql database is very fast',
       by_user: 'runoob.com',
       url: 'http://www.runoob.com',
       tags: ['mongodb', 'database', 'NoSQL'],
       likes: 10
    },
    {
       title: 'Neo4j Overview', 
       description: 'Neo4j is no sql database',
       by_user: 'Neo4j',
       url: 'http://www.neo4j.com',
       tags: ['neo4j', 'database', 'NoSQL'],
       likes: 750
    }

    现在我们通过以上集合计算每个作者所写的文章数,使用aggregate()计算结果如下:

    > db.集合名称.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}])

    常见聚合表达式

    整合应用

    说明: 这里主要以 springboot 应用为基础应用进行整合开发。

    Spring Data : Spring 数据框架 JPA 、Redis、Elasticsearch、AMQP、MongoDBJdbcTemplateRedisTemplateElasticTempalteAmqpTemplateMongoTemplate

    环境搭建

    # 引入依赖
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    # 编写配置
    # mongodb 没有开启任何安全协议
    # mongodb(协议)://121.5.167.13(主机):27017(端口)/baizhi(库名)
    spring.data.mongodb.uri=mongodb://121.5.167.13:27017/baizhi
    ​
    ​
    # mongodb 存在密码
    #spring.data.mongodb.host=tx.chenyn.cn
    #spring.data.mongodb.port=27017
    #spring.data.mongodb.database=baizhi
    #spring.data.mongodb.username=root
    #spring.data.mongodb.password=root

    集合操作

    创建集合

    @Test
    public void testCreateCollection(){
      mongoTemplate.createCollection("users");//参数: 创建集合名称
    }
    // 注意:创建集合不能存在,存在报错

    删除集合

    @Test
    public void testDeleteCollection(){
      mongoTemplate.dropCollection("users");
    }

    相关注解

    • @Document
      • 修饰范围: 用在类上
      • 作用: 用来映射这个类的一个对象为 mongo 中一条文档数据
      • 属性:(value 、collection )用来指定操作的集合名称
    • @Id
      • 修饰范围: 用在成员变量、方法上
      • 作用: 用来将成员变量的值映射为文档的_id 的值
    • @Field
      • 修饰范围: 用在成员变量、方法上
      • 作用: 用来将成员变量以及值映射为文档中一个key、value对
      • 属性: ( name,value)用来指定在文档中 key 的名称,默认为成员变量名
    • @Transient
      • 修饰范围: 用在成员变量、方法上
      • 作用 : 用来指定改成员变量,不参与文档的序列化

    文档操作

    查询

    Criteria

    常见查询

    @Test
    public void testQuery(){
      //基于 id 查询
      template.findById("1",User.class);
    
      //查询所有
      template.findAll(User.class);
      template.find(new Query(),User.class);
    
      //等值查询
      template.find(Query.query(Criteria.where("name").is("编程不良人")), 
                   User.class);
    
      // > gt  < lt  >= gte  <= lte
      template.find(Query.query(Criteria.where("age").lt(25)),
                    User.class);
      template.find(Query.query(Criteria.where("age").gt(25)),
                    User.class);
      template.find(Query.query(Criteria.where("age").lte(25)),
                    User.class);
      template.find(Query.query(Criteria.where("age").gte(25)),
                    User.class);
    
      //and
      template.find(Query.query(Criteria.where("name").is("编程不良人")
                                .and("age").is(23)),User.class);
    
      //or
      Criteria criteria = new Criteria()
        .orOperator(Criteria.where("name").is("编程不良人_1"),
         Criteria.where("name").is("编程不良人_2"));
      template.find(Query.query(criteria), User.class);
    
      //and or
      Criteria criteria1 = new Criteria()
        .and("age").is(23)
        .orOperator(
        Criteria.where("name").is("编程不良人_1"),
        Criteria.where("name").is("编程不良人_2"));
      template.find(Query.query(criteria1), User.class);
    
      //sort 排序
      Query query = new Query();
      query.with(Sort.by(Sort.Order.desc("age")));//desc 降序  asc 升序
      template.find(query, User.class);
    
    
      //skip limit 分页
      Query queryPage = new Query();
      queryPage.with(Sort.by(Sort.Order.desc("age")))//desc 降序  asc 升序
        .skip(0) //起始条数
        .limit(4); //每页显示记录数
      template.find(queryPage, User.class);
    
    
      //count 总条数
      template.count(new Query(), User.class);
    
      //distinct 去重
      //参数 1:查询条件 参数 2: 去重字段  参数 3: 操作集合  参数 4: 返回类型
      template.findDistinct(new Query(), "name", 
                            User.class, String.class);
      
      //使用 json 字符串方式查询 
            Query query = new BasicQuery(
              "{$or:[{name:'编程不良人'},{name:'徐凤年'}]}", 
              "{name:0}");
    
      template.find(query, User.class);
    }

    添加

    @Test
    public void testSaveOrUpdate(){
      User user = new User();
      user.setId("1");
      user.setAge(23);
      user.setName("编程不良人_1");
      user.setBir(new Date());
      User userDB = mongoTemplate.insert(user);//返回保存的对象 insert or save
      System.out.println(userDB);
    }

    insert: 插入重复数据时:insert报DuplicateKeyException提示主键重复;save对已存在的数据进行更新。
    save: 批处理操作时:insert可以一次性插入整个数据,效率较高;save需遍历整个数据,一次插入或更新,效率较低。

    更新

    @Test
    public void  testUpdate(){
      //1.更新条件
      Query query = Query.query(Criteria.where("age").is(23));
      //2.更新内容
      Update update = new Update();
      update.set("name","编程小陈陈");
    
      //单条更新
      mongoTemplate.updateFirst(query, update, User.class);
      //多条更新
      mongoTemplate.updateMulti(query, update, User.class);
      //更新插入
      mongoTemplate.upsert(query,update,User.class);
    
      //返回值均为 updateResult
      //System.out.println("匹配条数:" + updateResult.getMatchedCount());
      //System.out.println("修改条数:" + updateResult.getModifiedCount());
      //System.out.println("插入id_:" + updateResult.getUpsertedId());
    }

    删除

    @Test
    public void testDelete(){
      //删除所有
      mongoTemplate.remove(new Query(),User.class);
      //条件删除
      mongoTemplate.remove(
        Query.query(Criteria.where("name").is("编程不良人")),
        User.class
      );
    }

    副本集

    <Replica Set>

    说明

    https://docs.mongodb.com/manual/replication/

    MongoDB 副本集(Replica Set)是有自动故障恢复功能的主从集群,有一个Primary节点和一个或多个Secondary节点组成。副本集没有固定的主节点,当主节点发生故障时整个集群会选举一个主节点为系统提供服务以保证系统的高可用。

    Automatic Failover

    自动故障转移机制: 当主节点未与集合的其他成员通信超过配置的选举超时时间(默认为 10 秒)时,合格的辅助节点将调用选举以将自己提名为新的主节点。集群尝试完成新主节点的选举并恢复正常操作。

    搭建副本集

    • 创建数据目录
      # 在安装目录中创建 - mkdir -p ../repl/data1 - mkdir -p ../repl/data2 - mkdir -p ../repl/data3
    • 搭建副本集 $ mongod --port 27017  --dbpath ../repl/data1 --bind_ip 0.0.0.0 --replSet myreplace/[121.5.167.13:27018,121.5.167.13:27019] $ mongod --port 27018  --dbpath ../repl/data2 --bind_ip 0.0.0.0 --replSet myreplace/[121.5.167.13:27019,121.5.167.13:27017] $ mongod --port 27019  --dbpath ../repl/data3 --bind_ip 0.0.0.0 --replSet myreplace/[121.5.167.13:27017,121.5.167.13:27018] # 注意: --replSet 副本集 myreplace 副本集名称/集群中其他节点的主机和端口
    • 配置副本集,连接任意节点
      • use admin
      • 初始化副本集
        > var config = { _id:"myreplace", members:[ {_id:0,host:"121.5.167.13:27017"}, {_id:1,host:"121.5.167.13:27018"}, {_id:2,host:"121.5.167.13:27019"} ] } > rs.initiate(config);//初始化配置
      • 设置客户端临时可以访问
        > rs.slaveOk(); > rs.secondaryOk();

    分片集群<Sharding Cluster>

    说明

    https://docs.mongodb.com/manual/sharding/

    分片(sharding)是指将数据拆分,将其分散存在不同机器的过程,有时也用分区(partitioning)来表示这个概念,将数据分散在不同的机器上,不需要功能强大的大型计算机就能存储更多的数据,处理更大的负载。

    分片目的是通过分片能够增加更多机器来应对不断的增加负载和数据,还不影响应用运行。

    MongoDB支持自动分片,可以摆脱手动分片的管理困扰,集群自动切分数据做负载均衡。MongoDB分片的基本思想就是将集合拆分成多个块,这些快分散在若干个片里,每个片只负责总数据的一部分,应用程序不必知道哪些片对应哪些数据,甚至不需要知道数据拆分了,所以在分片之前会运行一个路由进程,mongos进程,这个路由器知道所有的数据存放位置,应用只需要直接与mongos交互即可。mongos自动将请求转到相应的片上获取数据,从应用角度看分不分片没有什么区别。

    架构

    • Shard: 用于存储实际的数据块,实际生产环境中一个shard server角色可由几台机器组个一个replica set承担,防止主机单点故障
    • Config Server:mongod实例,存储了整个 ClusterMetadata。
    • Query Routers: 前端路由,客户端由此接入,且让整个集群看上去像单一数据库,前端应用可以透明使用。
    • Shard Key: 片键,设置分片时需要在集合中选一个键,用该键的值作为拆分数据的依据,这个片键称之为(shard key),片键的选取很重要,片键的选取决定了数据散列是否均匀。

    搭建

    # 1.集群规划
    - Shard Server 1:27017
    - Shard Repl   1:27018
    
    - Shard Server 2:27019
    - Shard Repl   2:27020
    
    - Shard Server 3:27021
    - Shard Repl   3:27022
    
    - Config Server :27023
    - Config Server :27024
    - Config Server :27025
    
    - Route Process :27026
    
    # 2.进入安装的 bin 目录创建数据目录
    - mkdir -p ../cluster/shard/s0
    - mkdir -p ../cluster/shard/s0-repl
    
    - mkdir -p ../cluster/shard/s1
    - mkdir -p ../cluster/shard/s1-repl
    
    - mkdir -p ../cluster/shard/s2
    - mkdir -p ../cluster/shard/s2-repl
    
    - mkdir -p ../cluster/shard/config1
    - mkdir -p ../cluster/shard/config2
    - mkdir -p ../cluster/shard/config3
    
    # 3.启动4个 shard服务
    
    # 启动 s0、r0
    > ./mongod --port 27017 --dbpath ../cluster/shard/s0 --bind_ip 0.0.0.0 --shardsvr --replSet r0/121.5.167.13:27018
    > ./mongod --port 27018 --dbpath ../cluster/shard/s0-repl --bind_ip 0.0.0.0 --shardsvr --replSet r0/121.5.167.13:27017
    -- 1.登录任意节点
    -- 2. use admin
    -- 3. 执行
    		config = { _id:"r0", members:[
          {_id:0,host:"121.5.167.13:27017"},
          {_id:1,host:"121.5.167.13:27018"},
        	]
        }
    		rs.initiate(config);//初始化
    
    # 启动 s1、r1
    > ./mongod --port 27019 --dbpath ../cluster/shard/s1 --bind_ip 0.0.0.0 --shardsvr  --replSet r1/121.5.167.13:27020
    > ./mongod --port 27020 --dbpath ../cluster/shard/s1-repl --bind_ip 0.0.0.0 --shardsvr --replSet r1/121.5.167.13:27019
    -- 1.登录任意节点
    -- 2. use admin
    -- 3. 执行
    		config = { _id:"r1", members:[
          {_id:0,host:"121.5.167.13:27019"},
          {_id:1,host:"121.5.167.13:27020"},
        	]
        }
    		rs.initiate(config);//初始化
    
    # 启动 s2、r2
    > ./mongod --port 27021 --dbpath ../cluster/shard/s2 --bind_ip 0.0.0.0 --shardsvr --replSet r2/121.5.167.13:27022
    > ./mongod --port 27022 --dbpath ../cluster/shard/s2-repl --bind_ip 0.0.0.0 --shardsvr --replSet r2/121.5.167.13:27021
    -- 1.登录任意节点
    -- 2. use admin
    -- 3. 执行
    		config = { _id:"r2", members:[
          {_id:0,host:"121.5.167.13:27021"},
          {_id:1,host:"121.5.167.13:27022"},
        	]
        }
    		rs.initiate(config);//初始化
    
    # 4.启动3个config服务
    
    > ./mongod --port 27023 --dbpath ../cluster/shard/config1 --bind_ip 0.0.0.0 --replSet  config/[121.5.167.13:27024,121.5.167.13:27025] --configsvr
    
    > ./mongod --port 27024 --dbpath ../cluster/shard/config2 --bind_ip 0.0.0.0 --replSet  config/[121.5.167.13:27023,121.5.167.13:27025] --configsvr
    
    > ./mongod --port 27025 --dbpath ../cluster/shard/config3 --bind_ip 0.0.0.0 --replSet  config/[121.5.167.13:27023,121.5.167.13:27024] --configsvr
    
    # 5.初始化 config server 副本集
    - `登录任意节点 congfig server`
    > 1.use admin 
    > 2.在admin中执行
      config = { 
          _id:"config", 
          configsvr: true,
          members:[
              {_id:0,host:"121.5.167.13:27023"},
              {_id:1,host:"121.5.167.13:27024"},
              {_id:2,host:"121.5.167.13:27025"}
            ]
      }
    > 3.rs.initiate(config); //初始化副本集配置 
    
    # 6.启动 mongos 路由服务
    
    > ./mongos --port 27026 --configdb config/121.5.167.13:27023,121.5.167.13:27024,121.5.167.13:27025 --bind_ip 0.0.0.0 
    
    # 7.登录 mongos 服务
    > 1.登录 mongo --port 27026
    > 2.use admin
    > 3.添加分片信息
    	db.runCommand({ addshard:"r0/121.5.167.13:27017,121.5.167.13:27018",
    	"allowLocal":true });
    	db.runCommand({ addshard:"r1/121.5.167.13:27019,121.5.167.13:27020",
    	"allowLocal":true });
    	db.runCommand({ addshard:"r2/121.5.167.13:27021,121.5.167.13:27022",
    	"allowLocal":true });
    > 4.指定分片的数据库
    	db.runCommand({ enablesharding:"baizhi" });
    
    > 5.设置库的片键信息
    	db.runCommand({ shardcollection: "baizhi.users", key: { _id:1}});
    	db.runCommand({ shardcollection: "baizhi.emps", key: { _id: "hashed"}})
  • Redis

    Redis

    1. NoSQL的引言

    NoSQL(Not Only SQL ),意即不仅仅是SQL, 泛指非关系型的数据库。Nosql这个技术门类,早期就有人提出,发展至2009年趋势越发高涨。

    2. 为什么是NoSQL

    随着互联网网站的兴起,传统的关系数据库在应付动态网站,特别是超大规模和高并发的纯动态网站已经显得力不从心,暴露了很多难以克服的问题。如商城网站中对商品数据频繁查询、对热搜商品的排行统计、订单超时问题、以及微信朋友圈(音频,视频)存储等相关使用传统的关系型数据库实现就显得非常复杂,虽然能实现相应功能但是在性能上却不是那么乐观。nosql这个技术门类的出现,更好的解决了这些问题,它告诉了世界不仅仅是sql。

    3. NoSQL的四大分类

    3.1 键值(Key-Value)存储数据库

    # 1.说明: 
    - 这一类数据库主要会使用到一个哈希表,这个表中有一个特定的键和一个指针指向特定的数据。
    ​
    # 2.特点
    - Key/value模型对于IT系统来说的优势在于简单、易部署。  
    - 但是如果DBA只对部分值进行查询或更新的时候,Key/value就显得效率低下了。
    ​
    # 3.相关产品
    - Tokyo Cabinet/Tyrant,
    - Redis
    - SSDB
    - Voldemort 
    - Oracle BDB

    3.2 列存储数据库

    # 1.说明
    - 这部分数据库通常是用来应对分布式存储的海量数据。
    ​
    # 2.特点
    - 键仍然存在,但是它们的特点是指向了多个列。这些列是由列家族来安排的。
    ​
    # 3.相关产品
    - Cassandra、HBase、Riak.

    3.3 文档型数据库

    # 1.说明
    - 文档型数据库的灵感是来自于Lotus Notes办公软件的,而且它同第一种键值存储相类似该类型的数据模型是版本化的文档,半结构化的文档以特定的格式存储,比如JSON。文档型数据库可 以看作是键值数据库的升级版,允许之间嵌套键值。而且文档型数据库比键值数据库的查询效率更高
    ​
    # 2.特点
    - 以文档形式存储
    ​
    # 3.相关产品
    - MongoDB、CouchDB、 MongoDb(4.x). 国内也有文档型数据库SequoiaDB,已经开源。

    3.4 图形(Graph)数据库

    # 1.说明
    - 图形结构的数据库同其他行列以及刚性结构的SQL数据库不同,它是使用灵活的图形模型,并且能够扩展到多个服务器上。
    - NoSQL数据库没有标准的查询语言(SQL),因此进行数据库查询需要制定数据模型。许多NoSQL数据库都有REST式的数据接口或者查询API。
    ​
    # 2.特点
    ​
    # 3.相关产品
    - Neo4J、InfoGrid、 Infinite Graph、

    4. NoSQL应用场景

    • 数据模型比较简单
    • 需要灵活性更强的IT系统
    • 对数据库性能要求较高
    • 不需要高度的数据一致性

    5. 什么是Redis

    Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.

    Redis 开源 遵循BSD 基于内存数据存储 被用于作为 数据库 缓存 消息中间件

    • 总结: redis是一个内存型的数据库

    6. Redis特点

    • Redis是一个高性能key/value内存型数据库
    • Redis支持丰富的数据类型
    • Redis支持持久化
    • Redis单线程,单进程

    7. Redis安装

    # 0.准备环境
    - vmware15.x+
    - centos7.x+
    ​
    # 1.下载redis源码包
    - https://redis.io/

    # 2.下载完整源码包
    - redis-4.0.10.tar.gz
    # 3.将下载redis资料包上传到Linux中

    # 4.解压缩文件
    [root@localhost ~]# tar -zxvf redis-4.0.10.tar.gz
    [root@localhost ~]# ll

    # 5.安装gcc  
    - yum install -y gcc
    ​
    # 6.进入解压缩目录执行如下命令
    - make MALLOC=libc
    ​
    # 7.编译完成后执行如下命令
    - make install PREFIX=/usr/redis
    ​
    # 8.进入/usr/redis目录启动redis服务 
    - ./redis-server
    # 9.Redis服务端口默认是 6379
    ​
    # 10.进入bin目录执行客户端连接操作
    - ./redis-cli –p 6379

    # 11.连接成功出现上面界面连接成功

    8. Redis数据库相关指令

    8.1 数据库操作指令

    # 1.Redis中库说明
    - 使用redis的默认配置器动redis服务后,默认会存在16个库,编号从0-15
    - 可以使用select 库的编号 来选择一个redis的库
    ​
    # 2.Redis中操作库的指令
    - 清空当前的库  FLUSHDB
    - 清空全部的库  FLUSHALL
    ​
    # 3.redis客户端显示中文
    - ./redis-cli  -p 7000 --raw

    8.2 操作key相关指令

    # 1.DEL指令
    - 语法 :  DEL key [key ...] 
    - 作用 :  删除给定的一个或多个key 。不存在的key 会被忽略。
    - 可用版本: >= 1.0.0
    - 返回值: 被删除key 的数量。 
    ​
    # 2.EXISTS指令
    - 语法:  EXISTS key
    - 作用:  检查给定key 是否存在。
    - 可用版本: >= 1.0.0
    - 返回值: 若key 存在,返回1 ,否则返回0。
    ​
    # 3.EXPIRE
    - 语法:  EXPIRE key seconds
    - 作用:  为给定key 设置生存时间,当key 过期时(生存时间为0 ),它会被自动删除。
    - 可用版本: >= 1.0.0
    - 时间复杂度: O(1)
    - 返回值:设置成功返回1 。
    ​
    # 4.KEYS
    - 语法 :  KEYS pattern
    - 作用 :  查找所有符合给定模式pattern 的key 。
    - 语法:
      KEYS * 匹配数据库中所有key 。
      KEYS h?llo 匹配hello ,hallo 和hxllo 等。
      KEYS h*llo 匹配hllo 和heeeeello 等。
      KEYS h[ae]llo 匹配hello 和hallo ,但不匹配hillo 。特殊符号用 "\" 隔开
    - 可用版本: >= 1.0.0
    - 返回值: 符合给定模式的key 列表。
    ​
    # 5.MOVE
    - 语法 :  MOVE key db
    - 作用 :  将当前数据库的key 移动到给定的数据库db 当中。
    - 可用版本: >= 1.0.0
    - 返回值: 移动成功返回1 ,失败则返回0 。
    ​
    # 6.PEXPIRE
    - 语法 :  PEXPIRE key milliseconds
    - 作用 :  这个命令和EXPIRE 命令的作用类似,但是它以毫秒为单位设置key 的生存时间,而不像EXPIRE 命令那样,以秒为单位。
    - 可用版本: >= 2.6.0
    - 时间复杂度: O(1)
    - 返回值:设置成功,返回1  key 不存在或设置失败,返回0
    ​
    # 7.PEXPIREAT
    - 语法 :  PEXPIREAT key milliseconds-timestamp
    - 作用 :  这个命令和EXPIREAT 命令类似,但它以毫秒为单位设置key 的过期unix 时间戳,而不是像EXPIREAT那样,以秒为单位。
    - 可用版本: >= 2.6.0
    - 返回值:如果生存时间设置成功,返回1 。当key 不存在或没办法设置生存时间时,返回0 。(查看EXPIRE 命令获取更多信息)
    ​
    # 8.TTL
    - 语法 :   TTL key
    - 作用 :   以秒为单位,返回给定key 的剩余生存时间(TTL, time to live)。
    - 可用版本: >= 1.0.0
    - 返回值:
      当key 不存在时,返回-2 。
      当key 存在但没有设置剩余生存时间时,返回-1 。
      否则,以秒为单位,返回key 的剩余生存时间。
    - Note : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。
    ​
    # 9.PTTL
    - 语法 :  PTTL key
    - 作用 :  这个命令类似于TTL 命令,但它以毫秒为单位返回key 的剩余生存时间,而不是像TTL 命令那样,以秒为单位。
    - 可用版本: >= 2.6.0
    - 返回值: 当key 不存在时,返回-2 。当key 存在但没有设置剩余生存时间时,返回-1 。
    - 否则,以毫秒为单位,返回key 的剩余生存时间。
    - 注意 : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。
    ​
    # 10.RANDOMKEY
    - 语法 :  RANDOMKEY
    - 作用 :  从当前数据库中随机返回(不删除) 一个key 。
    - 可用版本: >= 1.0.0
    - 返回值:当数据库不为空时,返回一个key 。当数据库为空时,返回nil 。
    ​
    # 11.RENAME
    - 语法 :  RENAME key newkey
    - 作用 :  将key 改名为newkey 。当key 和newkey 相同,或者key 不存在时,返回一个错误。当newkey 已经存在时,RENAME 命令将覆盖旧值。
    - 可用版本: >= 1.0.0
    - 返回值: 改名成功时提示OK ,失败时候返回一个错误。
    ​
    # 12.TYPE
    - 语法 :  TYPE key
    - 作用 :  返回key 所储存的值的类型。
    - 可用版本: >= 1.0.0
    - 返回值:
      none (key 不存在)
      string (字符串)
      list (列表)
      set (集合)
      zset (有序集)
      hash (哈希表)

    8.3 String类型

    1. 内存存储模型

    2. 常用操作命令

    命令说明
    set设置一个key/value
    get根据key获得对应的value
    mset一次设置多个key value
    mget一次获得多个key的value
    getset获得原始key的值,同时设置新值
    strlen获得对应key存储value的长度
    append为对应key的value追加内容
    getrange 索引0开始截取value的内容
    setex设置一个key存活的有效期(秒)
    psetex设置一个key存活的有效期(毫秒)
    setnx存在不做任何操作,不存在添加
    msetnx原子操作(只要有一个存在不做任何操作)可以同时设置多个key,只有有一个存在都不保存
    decr进行数值类型的-1操作
    decrby根据提供的数据进行减法操作
    Incr进行数值类型的+1操作
    incrby根据提供的数据进行加法操作
    Incrbyfloat根据提供的数据加入浮点数

    8.4 List类型

    list 列表 相当于java中list 集合 特点 元素有序 且 可以重复

    1.内存存储模型

    2.常用操作指令

    命令说明
    lpush将某个值加入到一个key列表头部
    lpushx同lpush,但是必须要保证这个key存在
    rpush将某个值加入到一个key列表末尾
    rpushx同rpush,但是必须要保证这个key存在
    lpop返回和移除列表左边的第一个元素
    rpop返回和移除列表右边的第一个元素
    lrange获取某一个下标区间内的元素
    llen获取列表元素个数
    lset设置某一个指定索引的值(索引必须存在)
    lindex获取某一个指定索引位置的元素
    lrem删除重复元素
    ltrim保留列表中特定区间内的元素
    linsert在某一个元素之前,之后插入新元素

    8.5 Set类型

    特点: Set类型 Set集合 元素无序 不可以重复

    1.内存存储模型

    2.常用命令

    命令说明
    sadd为集合添加元素
    smembers显示集合中所有元素 无序
    scard返回集合中元素的个数
    spop随机返回一个元素 并将元素在集合中删除
    smove从一个集合中向另一个集合移动元素 必须是同一种类型
    srem从集合中删除一个元素
    sismember判断一个集合中是否含有这个元素
    srandmember随机返回元素
    sdiff去掉第一个集合中其它集合含有的相同元素
    sinter求交集
    sunion求和集

    8.6 ZSet类型

    特点: 可排序的set集合 排序 不可重复

    ZSET 官方 可排序SET sortSet

    1.内存模型

    2.常用命令

    命令说明
    zadd添加一个有序集合元素
    zcard返回集合的元素个数
    zrange 升序 zrevrange 降序返回一个范围内的元素
    zrangebyscore按照分数查找一个范围内的元素
    zrank返回排名
    zrevrank倒序排名
    zscore显示某一个元素的分数
    zrem移除某一个元素
    zincrby给某个特定元素加分

    8.7 hash类型

    特点: value 是一个map结构 存在key value key 无序的

    1.内存模型

    2.常用命令

    命令说明
    hset设置一个key/value对
    hget获得一个key对应的value
    hgetall获得所有的key/value对
    hdel删除某一个key/value对
    hexists判断一个key是否存在
    hkeys获得所有的key
    hvals获得所有的value
    hmset设置多个key/value
    hmget获得多个key的value
    hsetnx设置一个不存在的key的值
    hincrby为value进行加法运算
    hincrbyfloat为value加入浮点值

    9. 持久化机制

    client redis[内存] —–> 内存数据- 数据持久化–>磁盘

    Redis官方提供了两种不同的持久化方法来将数据存储到硬盘里面分别是:

    • 快照(Snapshot)
    • AOF (Append Only File) 只追加日志文件

    9.1 快照(Snapshot)

    1. 特点

    这种方式可以将某一时刻的所有数据都写入硬盘中,当然这也是redis的默认开启持久化方式,保存的文件是以.rdb形式结尾的文件因此这种方式也称之为RDB方式。

    2.快照生成方式

    • 客户端方式: BGSAVE 和 SAVE指令
    • 服务器配置自动触发
    # 1.客户端方式之BGSAVE
    - a.客户端可以使用BGSAVE命令来创建一个快照,当接收到客户端的BGSAVE命令时,redis会调用fork¹来创建一个子进程,然后子进程负责将快照写入磁盘中,而父进程则继续处理命令请求。
      
      `名词解释: fork当一个进程创建子进程的时候,底层的操作系统会创建该进程的一个副本,在类unix系统中创建子进程的操作会进行优化:在刚开始的时候,父子进程共享相同内存,直到父进程或子进程对内存进行了写之后,对被写入的内存的共享才会结束服务`

    # 2.客户端方式之SAVE
    - b.客户端还可以使用SAVE命令来创建一个快照,接收到SAVE命令的redis服务器在快照创建完毕之前将不再响应任何其他的命令
    • 注意: SAVE命令并不常用,使用SAVE命令在快照创建完毕之前,redis处于阻塞状态,无法对外服务
    # 3.服务器配置方式之满足配置自动触发
    - 如果用户在redis.conf中设置了save配置选项,redis会在save选项条件满足之后自动触发一次BGSAVE命令,如果设置多个save配置选项,当任意一个save配置选项条件满足,redis也会触发一次BGSAVE命令
    # 4.服务器接收客户端shutdown指令
    - 当redis通过shutdown指令接收到关闭服务器的请求时,会执行一个save命令,阻塞所有的客户端,不再执行客户端执行发送的任何命令,并且在save命令执行完毕之后关闭服务器

    3.配置生成快照名称和位置

    #1.修改生成快照名称
    - dbfilename dump.rdb
    ​
    # 2.修改生成位置
    - dir ./

    9.2 AOF 只追加日志文件

    1.特点

    这种方式可以将所有客户端执行的写命令记录到日志文件中,AOF持久化会将被执行的写命令写到AOF的文件末尾,以此来记录数据发生的变化,因此只要redis从头到尾执行一次AOF文件所包含的所有写命令,就可以恢复AOF文件的记录的数据集.

    2.开启AOF持久化

    在redis的默认配置中AOF持久化机制是没有开启的,需要在配置中开启

    # 1.开启AOF持久化
    - a.修改 appendonly yes 开启持久化
    - b.修改 appendfilename "appendonly.aof" 指定生成文件名称

    3.日志追加频率

    # 1.always 【谨慎使用】
    - 说明: 每个redis写命令都要同步写入硬盘,严重降低redis速度
    - 解释: 如果用户使用了always选项,那么每个redis写命令都会被写入硬盘,从而将发生系统崩溃时出现的数据丢失减到最少;遗憾的是,因为这种同步策略需要对硬盘进行大量的写入操作,所以redis处理命令的速度会受到硬盘性能的限制;
    - 注意: 转盘式硬盘在这种频率下200左右个命令/s ; 固态硬盘(SSD) 几百万个命令/s;
    - 警告: 使用SSD用户请谨慎使用always选项,这种模式不断写入少量数据的做法有可能会引发严重的写入放大问题,导致将固态硬盘的寿命从原来的几年降低为几个月。
    
    # 2.everysec 【推荐】
    - 说明: 每秒执行一次同步显式的将多个写命令同步到磁盘
    - 解释: 为了兼顾数据安全和写入性能,用户可以考虑使用everysec选项,让redis每秒一次的频率对AOF文件进行同步;redis每秒同步一次AOF文件时性能和不使用任何持久化特性时的性能相差无几,而通过每秒同步一次AOF文件,redis可以保证,即使系统崩溃,用户最多丢失一秒之内产生的数据。
    
    # 3.no	【不推荐】
    - 说明: 由操作系统决定何时同步 
    - 解释:最后使用no选项,将完全有操作系统决定什么时候同步AOF日志文件,这个选项不会对redis性能带来影响但是系统崩溃时,会丢失不定数量的数据,另外如果用户硬盘处理写入操作不够快的话,当缓冲区被等待写入硬盘数据填满时,redis会处于阻塞状态,并导致redis的处理命令请求的速度变慢。

    4.修改同步频率

    # 1.修改日志同步频率
    - 修改appendfsync everysec|always|no 指定

    9.3 AOF文件的重写

    1. AOF带来的问题

    AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件Redis提供了AOF重写(ReWriter)机制。

    2. AOF重写

    用来在一定程度上减小AOF文件的体积

    3. 触发重写方式

    # 1.客户端方式触发重写
    - 执行BGREWRITEAOF命令  不会阻塞redis的服务
    
    # 2.服务器配置方式自动触发
    - 配置redis.conf中的auto-aof-rewrite-percentage选项 参加下图↓↓↓
    - 如果设置auto-aof-rewrite-percentage值为100和auto-aof-rewrite-min-size 64mb,并且启用的AOF持久化时,那么当AOF文件体积大于64M,并且AOF文件的体积比上一次重写之后体积大了至少一倍(100%)时,会自动触发,如果重写过于频繁,用户可以考虑将auto-aof-rewrite-percentage设置为更大

    4. 重写原理

    注意:重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,替换原有的文件这点和快照有点类似。

    # 重写流程
    - 1. redis调用fork ,现在有父子两个进程 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
    - 2. 父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
    - 3. 当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
    - 4. 现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

    9.4 持久化总结

    两种持久化方案既可以同时使用(aof),又可以单独使用,在某种情况下也可以都不使用,具体使用那种持久化方案取决于用户的数据和应用决定。

    无论使用AOF还是快照机制持久化,将数据持久化到硬盘都是有必要的,除了持久化外,用户还应该对持久化的文件进行备份(最好备份在多个不同地方)。

    10. java操作Redis

    10.1 环境准备

    1. 引入依赖

    <!--引入jedis连接依赖-->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
    </dependency>

    2.创建jedis对象

     public static void main(String[] args) {
       //1.创建jedis对象
       Jedis jedis = new Jedis("192.168.40.4", 6379);//1.redis服务必须关闭防火墙  2.redis服务必须开启远程连接
       jedis.select(0);//选择操作的库默认0号库
       //2.执行相关操作
       //....
       //3.释放资源
       jedis.close();
     }

    10.2 操作key相关API

    private Jedis jedis;
        @Before
        public void before(){
            this.jedis = new Jedis("192.168.202.205", 7000);
        }
        @After
        public void after(){
            jedis.close();
        }
    
        //测试key相关
        @Test
        public void testKeys(){
            //删除一个key
            jedis.del("name");
            //删除多个key
            jedis.del("name","age");
    
            //判断一个key是否存在exits
            Boolean name = jedis.exists("name");
            System.out.println(name);
    
            //设置一个key超时时间 expire pexpire
            Long age = jedis.expire("age", 100);
            System.out.println(age);
    
            //获取一个key超时时间 ttl
            Long age1 = jedis.ttl("newage");
            System.out.println(age1);
    
            //随机获取一个key
            String s = jedis.randomKey();
    
            //修改key名称
            jedis.rename("age","newage");
    
            //查看可以对应值的类型
            String name1 = jedis.type("name");
            System.out.println(name1);
            String maps = jedis.type("maps");
            System.out.println(maps);
        }

    10.3操作String相关API

    //测试String相关
        @Test
        public void testString(){
            //set
            jedis.set("name","小陈");
            //get
            String s = jedis.get("name");
            System.out.println(s);
            //mset
            jedis.mset("content","好人","address","海淀区");
            //mget
            List<String> mget = jedis.mget("name", "content", "address");
            mget.forEach(v-> System.out.println("v = " + v));
            //getset
            String set = jedis.getSet("name", "小明");
            System.out.println(set);
    
            //............
        }

    10.4操作List相关API

    //测试List相关
        @Test
        public void testList(){
    
            //lpush
            jedis.lpush("names1","张三","王五","赵柳","win7");
    
            //rpush
            jedis.rpush("names1","xiaomingming");
    
            //lrange
    
            List<String> names1 = jedis.lrange("names1", 0, -1);
            names1.forEach(name-> System.out.println("name = " + name));
    
            //lpop rpop
            String names11 = jedis.lpop("names1");
            System.out.println(names11);
    
            //llen
            jedis.linsert("lists", BinaryClient.LIST_POSITION.BEFORE,"xiaohei","xiaobai");
    
          	//........
    
        }

    10.5操作Set的相关API

    //测试SET相关
    @Test
    public void testSet(){
    
      //sadd
      jedis.sadd("names","zhangsan","lisi");
    
      //smembers
      jedis.smembers("names");
    
      //sismember
      jedis.sismember("names","xiaochen");
    
      //...
    }

    10.6 操作ZSet相关API

    //测试ZSET相关
    @Test
    public void testZset(){
    
      //zadd
      jedis.zadd("names",10,"张三");
    
      //zrange
      jedis.zrange("names",0,-1);
    
      //zcard
      jedis.zcard("names");
    
      //zrangeByScore
      jedis.zrangeByScore("names","0","100",0,5);
    
      //..
    
    }

    10.7 操作Hash相关API

    //测试HASH相关
    @Test
    public void testHash(){
      //hset
      jedis.hset("maps","name","zhangsan");
      //hget
      jedis.hget("maps","name");
      //hgetall
      jedis.hgetAll("mps");
      //hkeys
      jedis.hkeys("maps");
      //hvals
      jedis.hvals("maps");
      //....
    }

    11.SpringBoot整合Redis

    Spring Boot Data(数据) Redis 中提供了RedisTemplate和StringRedisTemplate,其中StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处主要体现在操作的数据类型不同,RedisTemplate中的两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着StringRedisTemplate的key和value都只能是字符串。

    注意: 使用RedisTemplate默认是将对象序列化到Redis中,所以放入的对象必须实现对象序列化接口

    11.1 环境准备

    1.引入依赖

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

    2.配置application.propertie

    spring.redis.host=localhost
    spring.redis.port=6379
    spring.redis.database=0

    11.2 使用StringRedisTemplate和RedisTemplate

    @Autowired
        private StringRedisTemplate stringRedisTemplate;  //对字符串支持比较友好,不能存储对象
        @Autowired
        private RedisTemplate redisTemplate;  //存储对象
    
        @Test
        public void testRedisTemplate(){
            System.out.println(redisTemplate);
            //设置redistemplate值使用对象序列化策略
            redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());//指定值使用对象序列化
            //redisTemplate.opsForValue().set("user",new User("21","小黑",23,new Date()));
            User user = (User) redisTemplate.opsForValue().get("user");
            System.out.println(user);
    //      Set keys = redisTemplate.keys("*");
    //      keys.forEach(key -> System.out.println(key));
            /*Object name = redisTemplate.opsForValue().get("name");
            System.out.println(name);*/
    
            //Object xiaohei = redisTemplate.opsForValue().get("xiaohei");
            //System.out.println(xiaohei);
            /*redisTemplate.opsForValue().set("name","xxxx");
            Object name = redisTemplate.opsForValue().get("name");
            System.out.println(name);*/
            /*redisTemplate.opsForList().leftPushAll("lists","xxxx","1111");
            List lists = redisTemplate.opsForList().range("lists", 0, -1);
            lists.forEach(list-> System.out.println(list));*/
        }
    
    
        //key的绑定操作 如果日后对某一个key的操作及其频繁,可以将这个key绑定到对应redistemplate中,日后基于绑定操作都是操作这个key
        //boundValueOps 用来对String值绑定key
        //boundListOps 用来对List值绑定key
        //boundSetOps 用来对Set值绑定key
        //boundZsetOps 用来对Zset值绑定key
        //boundHashOps 用来对Hash值绑定key
    
        @Test
        public void testBoundKey(){
            BoundValueOperations<String, String> nameValueOperations = stringRedisTemplate.boundValueOps("name");
            nameValueOperations.set("1");
            //yuew
            nameValueOperations.set("2");
            String s = nameValueOperations.get();
            System.out.println(s);
    
        }
    
    
        //hash相关操作 opsForHash
        @Test
        public void testHash(){
            stringRedisTemplate.opsForHash().put("maps","name","小黑");
            Object o = stringRedisTemplate.opsForHash().get("maps", "name");
            System.out.println(o);
        }
    
        //zset相关操作 opsForZSet
        @Test
        public void testZSet(){
            stringRedisTemplate.opsForZSet().add("zsets","小黑",10);
            Set<String> zsets = stringRedisTemplate.opsForZSet().range("zsets", 0, -1);
            zsets.forEach(value-> System.out.println(value));
        }
    
        //set相关操作 opsForSet
        @Test
        public void testSet(){
            stringRedisTemplate.opsForSet().add("sets","xiaosan","xiaosi","xiaowu");
            Set<String> sets = stringRedisTemplate.opsForSet().members("sets");
            sets.forEach(value-> System.out.println(value));
        }
    
        //list相关的操作opsForList
        @Test
        public void testList(){
            // stringRedisTemplate.opsForList().leftPushAll("lists","张三","李四","王五");
            List<String> lists = stringRedisTemplate.opsForList().range("lists", 0, -1);
            lists.forEach(key -> System.out.println(key));
        }
    
    
        //String相关的操作 opsForValue
        @Test
        public void testString(){
            //stringRedisTemplate.opsForValue().set("166","好同学");
            String s = stringRedisTemplate.opsForValue().get("166");
            System.out.println(s);
            Long size = stringRedisTemplate.opsForValue().size("166");
            System.out.println(size);
        }
    
    
        //key相关的操作
        @Test
        public void test(){
            Set<String> keys = stringRedisTemplate.keys("*");//查看所有key
            Boolean name = stringRedisTemplate.hasKey("name");//判断某个key是否存在
            stringRedisTemplate.delete("age");//根据指定key删除
            stringRedisTemplate.rename("","");//修改key的名称
            stringRedisTemplate.expire("key",10, TimeUnit.HOURS);
          	//设置key超时时间 参数1:设置key名 参数2:时间 参数3:时间的单位
            stringRedisTemplate.move("",1);//移动key
        }

    12. Redis 主从复制

    12.1 主从复制

    主从复制架构仅仅用来解决数据的冗余备份,从节点仅仅用来同步数据

    无法解决: 1.master节点出现故障的自动故障转移

    12.2 主从复制架构图

    12.3 搭建主从复制

    # 1.准备3台机器并修改配置
    - master
    	port 6379
    	bind 0.0.0.0
    	
    - slave1
    	port 6380
    	bind 0.0.0.0
    	slaveof masterip masterport
    
    - slave2
    	port 6381
    	bind 0.0.0.0
    	slaveof masterip masterport
    # 2.启动3台机器进行测试
    - cd /usr/redis/bin
    - ./redis-server /root/master/redis.conf
    - ./redis-server /root/slave1/redis.conf
    - ./redis-server /root/slave2/redis.conf

    13. Redis哨兵机制

    13.1 哨兵Sentinel机制

    Sentinel(哨兵)是Redis 的高可用性解决方案:由一个或多个Sentinel 实例 组成的Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。简单的说哨兵就是带有自动故障转移功能的主从架构

    无法解决: 1.单节点并发压力问题 2.单节点内存和磁盘物理上限

    13.2 哨兵架构原理

    13.3 搭建哨兵架构

    # 1.在主节点上创建哨兵配置
    - 在Master对应redis.conf同目录下新建sentinel.conf文件,名字绝对不能错;
    
    # 2.配置哨兵,在sentinel.conf文件中填入内容:
    - sentinel monitor 被监控数据库名字(自己起名字) ip port 1
    
    # 3.启动哨兵模式进行测试
    - redis-sentinel  /root/sentinel/sentinel.conf
    	说明:这个后面的数字2,是指当有两个及以上的sentinel服务检测到master宕机,才会去执行主从切换的功能。

    13.4 通过springboot操作哨兵

    # redis sentinel 配置
    # master书写是使用哨兵监听的那个名称
    spring.redis.sentinel.master=mymaster
    # 连接的不再是一个具体redis主机,书写的是多个哨兵节点
    spring.redis.sentinel.nodes=192.168.202.206:26379
    • 注意:如果连接过程中出现如下错误:RedisConnectionException: DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command ‘CONFIG SET protected-mode no’ from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2)
    • 解决方案:在哨兵的配置文件中加入bind 0.0.0.0 开启远程连接权限

    14. Redis集群

    14.1 集群

    Redis在3.0后开始支持Cluster(模式)模式,目前redis的集群支持节点的自动发现,支持slave-master选举和容错,支持在线分片(sharding shard )等特性。reshard

    14.2 集群架构图

    14.3 集群细节

    - 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
    - 节点的fail是通过集群中超过半数的节点检测失效时才生效. 
    - 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
    - redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value

    14.4 集群搭建

    判断一个是集群中的节点是否可用,是集群中的所用主节点选举过程,如果半数以上的节点认为当前节点挂掉,那么当前节点就是挂掉了,所以搭建redis集群时建议节点数最好为奇数,搭建集群至少需要三个主节点,三个从节点,至少需要6个节点

    # 1.准备环境安装ruby以及redis集群依赖
    - yum install -y ruby rubygems
    - gem install redis-xxx.gem
    # 2.在一台机器创建7个目录
    # 3.每个目录复制一份配置文件
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7000/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7001/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7002/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7003/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7004/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7005/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7006/
    # 4.修改不同目录配置文件
    - port 	6379 .....                		 //修改端口
    - bind  0.0.0.0                   		 //开启远程连接
    - cluster-enabled  yes 	        			 //开启集群模式
    - cluster-config-file  nodes-port.conf //集群节点配置文件
    - cluster-node-timeout  5000      	   //集群节点超时时间
    - appendonly  yes   		               //开启AOF持久化
    
    # 5.指定不同目录配置文件启动七个节点
    - [root@localhost bin]# ./redis-server  /root/7000/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7001/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7002/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7003/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7004/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7005/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7006/redis.conf
    # 6.查看进程
    - [root@localhost bin]# ps aux|grep redis

    1.创建集群

    # 1.复制集群操作脚本到bin目录中
    - [root@localhost bin]# cp /root/redis-4.0.10/src/redis-trib.rb .
    
    # 2.创建集群
    - ./redis-trib.rb create --replicas 1 192.168.202.205:7000 192.168.202.205:7001 192.168.202.205:7002 192.168.202.205:7003 192.168.202.205:7004 192.168.202.205:7005
    # 3.集群创建成功出现如下提示

    2.查看集群状态

    # 1.查看集群状态 check [原始集群中任意节点] [无]
    - ./redis-trib.rb check 192.168.202.205:7000
    
    # 2.集群节点状态说明
    - 主节点 
    	主节点存在hash slots,且主节点的hash slots 没有交叉
    	主节点不能删除
    	一个主节点可以有多个从节点
    	主节点宕机时多个副本之间自动选举主节点
    
    - 从节点
    	从节点没有hash slots
    	从节点可以删除
    	从节点不负责数据的写,只负责数据的同步

    3.添加主节点

    # 1.添加主节点 add-node [新加入节点] [原始集群中任意节点]
    - ./redis-trib.rb  add-node 192.168.1.158:7006  192.168.1.158:7005
    - 注意:
      1.该节点必须以集群模式启动
      2.默认情况下该节点就是以master节点形式添加

    4.添加从节点

    # 1.添加从节点 add-node --slave [新加入节点] [集群中任意节点]
    - ./redis-trib.rb  add-node --slave 192.168.1.158:7006 192.168.1.158:7000
    - 注意:
    	当添加副本节点时没有指定主节点,redis会随机给副本节点较少的主节点添加当前副本节点
    	
    # 2.为确定的master节点添加主节点 add-node --slave --master-id master节点id [新加入节点] [集群任意节点]
    - ./redis-trib.rb  add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006  127.0.0.1:7000

    5.删除副本节点

    # 1.删除节点 del-node [集群中任意节点] [删除节点id]
    - ./redis-trib.rb  del-node 127.0.0.1:7002 0ca3f102ecf0c888fc7a7ce43a13e9be9f6d3dd1
    - 注意:
     1.被删除的节点必须是从节点或没有被分配hash slots的节点

    6.集群在线分片

    # 1.在线分片 reshard [集群中任意节点] [无]
    - ./redis-trib.rb  reshard  192.168.1.158:7000

    15.Redis实现分布式Session管理

    15.1 管理机制

    redis的session管理是利用spring提供的session管理解决方案,将一个应用session交给Redis存储,整个应用中所有session的请求都会去redis中获取对应的session数据。

    15.2 开发Session管理

    1. 引入依赖

    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
    </dependency>

    2. 开发Session管理配置类

    @Configuration
    @EnableRedisHttpSession
    public class RedisSessionManager {
       
    }

    3.打包测试即可

    16.分布式缓存

    1. 什么是缓存(Cache)

    定义:就是计算机中内存的一段数据

    2. 内存中数据特点

    • 读写快
    • 断电立即消失

    3. 缓存解决了什么问题?

    • 提高网站吞吐量,提高网站运行效率
    • 核心解决问题:缓存的存在是用来减轻数据库访问压力

    4.既然缓存能提高效率,那项目中所有数据加入缓存岂不是更好?

    注意:使用缓存时一定是数据库中极少发生修改,更多用于查询这种情况,如省市县

    5. 本地缓存和分布式缓存区别?

    • 本地缓存:存储在应用服务器内存中数据称之为本地缓存(local cache)
    • 分布式缓存:存储在当前应用服务器内存之外数据称之为分布式缓存(Distribute cache)
    • 集群:将同一种服务的多个节点放在一起共同对系统提供服务过程称之为集群
    • 分布式:有多个不同服务集群共同对系统提供服务,这个系统称之为分布式系统(Distribute System)

    6. 利用mybatis自身本地缓存结合redis实现分布式缓存

    1. mybatis中应用级缓存(二级缓存)SqlSessionFactory级别缓存,所有会话共享
    2. 如何开启二级缓存
      <!-- mapper.xml--><cache />
    3. 查看cache标签缓存实现:mybatis底层默认使用的是org.apache.ibatis.cache.impl.PerpetualCache实现
    4. 自定义RedisCache实现
      1. 通过mybatis默认cache源码得知,可以使用自定义cache类 implements Cache接口,并对里面的方法进行实现
        public class RedisCache implements Cache {…}
      2. mybatis配置使用redisCache实现
        <cache type="com.xxx.xxx.RedisCache" />

    7. 缓存在项目中的应用

    • 如果项目中表查询之间没有任何关联,查询使用现在的这种缓存方式没有任何问题
    • 现在缓存方式在表连接查询过程中一定存在问题:
      当A表与B表同时开启了二级缓存并且做了关联查询并缓存到redis对应的数据中,另一请求删除A表数据并且删除redis中对应的A表缓存,此时B表由于使用关联查询,在redis中还缓存着A表的数据。解决方案:在xml使用<cache-ref /> 将多个具有关联关系的查谒缓存放在一起处理

    17.经典场景

    缓存穿透

    什么叫缓存穿透:去缓存层中没有命中数据,进而去数据库查询数据。不能避免低频缓存穿透,可以避免高频的缓存穿透

    场景1:黑客可以通过一个固定的请求去攻击数据库

    解决方案:可以查询数据库,然后缓存NULL值到redis中,这样下一次查询就直接中缓存层返回

    场景2:黑客用一个随机条件的请求去攻击数据库

    解决方案:黑客如果用随机条件请求去攻击数据库,场景1的解决方案不能解决问题,反而会事得其反,在redis中占用更多的无效的数据,此时应该使用布隆过滤器来解决此问题

    • 布隆算法:通过一定的错误率来换取空间,假设传值10,经过hash函数计算(hash值范围为[0,lenth-1])为1,那么在bitset的1下标位标记为true,客户端再传998,经过hash函数计算为5,那么在bitset的5下标位标记为true
    • 布隆算法特性:标记为true时,可能不存在,标记为false时,绝对不存在
    • 布隆算法由于存在hash碰撞所以导致会有错误率的产生,那么如何降低错误率?
      • 加大数组的长度:范围越大,错误率产生的机率就越低
      • 增加hash函数的个数:假如使用3个hash函数,值10经过计算后分别为2,6,8,存储到bitset,值78经过计算2,9,11存储到bitset,如果只使用一个hash函数,那么这两个值可能出现重复,使用3个hash函数后,就降低了值重复的错误率
      • hash函数并不是越多越好,需要参数数组的长度,假设数据长度为10,hash函数为9个,那么经过多次标记后,所有数组都被置为true,此时错误率就增加
    • 布隆算法弊端:因为错误率导致下标位可能不止标记一条数据的存在,所以,删除数据的同时不能直接把下标位标为false,因为会影响到其他数据的使用,此时应该使用记数器记录此下标的出现的个数,再根据个数进行操作
    • guava有提供对布隆算法的使用封装

    缓存雪崩

    什么叫缓存雪崩:缓存层中的数据,在某一时刻突然失效(无法访问)导致大量的请求打向数据库

    导致雪崩的原因:

    1. redis中缓存的数据有效期是一致的:给每一条数据加上一个随机有效期,不要同时失效
    2. redis数据库挂掉了:使用分布式部署,hotdata使用切片集群部署,当一台redis挂掉,只是丢失一部份数据,如果hotdata数据量较少,则使用副本集群部署

    一致性hash算法

    1. 为什么需要一致性Hash环?

    假设我们有N个Cache服务器的Node,我们的要求就是将key均分的存储到Cache节点上,经典的做饭就是如下:

    position = hash(key) mod N

    那么这样在如下两种情况下会出现问题:

    • 有一台机器挂了
    • 新增一台机器

    这时候公式变成

    position = hash(key) mod (N+1) position = hash(key) mod (N-1)

    之前所有的求得结果都不对了,导致全部cache miss,只能回源到mysql,最终可想而知整个系统崩溃,如果这样设计分布式缓存系统,那这个分布式缓存系统是失败的。

    2. 一致性hash算法的特性

    • 平衡性:尽可能让数据尽可能分散到所有节点上,避免造成极其不均匀
    • 单调性:要求在新增或者减少节点的时候,原有的结果绝大部分不受影响,而新增的数据尽可能分配到新加的节点
    • 分散性:好的算法在不同终端,针对相同的数据的计算,得到的结果应该是一样的,一致性要很强
    • 负载:针对相同的节点,避免被不同终端映射不同的内容
    • 平滑性:对于增加节点或者减少节点,应该能够平滑过渡

    3. 一致性Hash算法原理介绍

    • 背景:一致性哈希算法在1997年由MIT的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题。
    • 算法原理
      1. 虚拟一个环的概念,在环上构造一个0~232
      1. 将N台服务器节点计算Hash值,映射到这个环上(可以用节点的IP或者主机名来计算hash值)
    1. 将数据用相同的Hash算法计算的值,映射到这个环上
    1. 然后顺时针寻找,找到的第一个服务器节点就是目标要保存的节点,如果超过232,就放到第一个节点
    2. d1,d2就存储到Node B
    3. d3, d4, d5存储到Node C
    4. d6, d7, d8存储到Node A
    1. 假如Node B宕机了,看看是什么情形:Node B宕机后,其他都不受印象,d1,d2会发生一次cache miss,然后再会存储Node C,影响比较小
    1. 如果新增加一台机器Node D,又会发生什么呢?d6,d7就会存储到了Node D,不再存储到Node A

    4. 一致性Hash节点太少改良版本

    如果出现极限情况下,我只有两台机器,然后大量的数据都集中某一台机器,例如下面情况

    这个根本原因就是节点太少,hash算法不均衡造成的,那么怎么解决

    • 将一个物理节点虚拟出N个虚拟节点来用,在环上体现虚拟节点。 Node A可以虚拟出Node A1,Node A2,Node A3 Node B可以虚拟出Node B1,Node B2,Node B3

    这样环上其实就有6个节点了。

    5. 一致性hash算法的应用场景

    • 场景1:经典CDN
      当你请求一个静态资源的时候,你需要请求到最近的节点
    • 场景2:session的问题

    缓存击穿

    什么是缓存击穿:对一个设置过期时间的hotdata,可能在被超高并发访问时过期,导致对这个key的请求大量落在了数据库层,瞬间可能把后端的DB压垮。

    一般公司不需要解决,因为很少这么大的请求数出现

    缓存击穿和缓存雪崩本质都是缓存穿透,缓存击穿与缓存雪崩是缓存穿透的特殊表现

    解决方案:采用分布式锁

    分布式锁

    Redis分布式锁会遇到的问题:

    1. 程序运行时可能抛异常
    2. 程序运行时可能宕机
    3. 删除锁时可能会误删其他线程创建的锁

    解决方案:使用redisson的分布式锁创建,采用锁续命的机制判断当前线程是否还持有锁,如果还持有锁,则会延长锁持有时间

    分布式锁主从架构锁失效:

    1. 使用Redlock对所有redis中间件进行加锁,只有过半redis加锁成功后才会返回客户端加锁成功(有争议,性能问题)
    2. 换成使用zookeeper进行加锁,原理跟redlock类似

    分布式锁的性能优化:

    1. 使用分段锁,如库存为200个,分为10笔20个,再对这10笔数据进行加锁,性能则能提升

    redis缓存与数据库双写不一致的问题:

    有两个线程1,2,当线程1更新数据库后,由于未知原因出现卡顿还未更新到redis,此时线程2更新数据库,也同时更新redis,线程1恢复执行,再更新redis,这就导致redis缓存与数据库双写不一致

    解决方案:

    1. 延时双删:不一定能解决此问题,而且容易造成写入吞吐量降低(不推荐)
    2. 内存队列:能解决此问题,但由于串行执行,所以性能不高,而且实现不方便(不推荐)
    3. 读写锁:redisson.getReadWriteLock(),读锁可以共享,读写锁互斥,在redisson底层会判断mode是read模式,则直接继续加read锁,如果是write锁,read锁则会等待(读多写少的场景)
    4. 设置超时时间:能够容忍时间差的场景

    内存淘汰策略

    Redis作为当前最常用的开源内存数据库,性能十分高,据官方数据表示Redis读的速度是110000次/s,写的速度是81000次/s 。而且Redis支持数据持久化,众多数据结构存储,master-slave模式数据备份等多种功能。

    但是长期将Redis作为缓存使用,难免会遇到内存空间存储瓶颈,当Redis内存超出物理内存限制时,内存数据就会与磁盘产生频繁交换,使Redis性能急剧下降。此时如何淘汰无用数据释放空间,存储新数据就变得尤为重要了。

    淘汰原理

    Redis在生产环境中,采用配置参数maxmemory 的方式来限制内存大小。当实际存储内存超出maxmemory 参数值时,可以通过这几种方法——Redis内存淘汰策略,来决定如何腾出新空间继续支持读写工作。

    那么Redis内存淘汰策略是如何工作的呢? 首先,客户端会发起需要更多内存的申请;其次,Redis检查内存使用情况,如果实际使用内存已经超出maxmemory,Redis就会根据用户配置的淘汰策略选出无用的key;

    确认选中数据没有问题,成功执行淘汰任务。

    淘汰策略

    1. volatile-lru:从设置过期时间的数据集(server.db[i].expires)中挑选出最近最少使用的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。
    2. volatile-ttl:除了淘汰机制采用LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。
    3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。
    4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。
    5. allkeys-random:从数据集(server.db[i].dict)中选择任意数据淘汰。
    6. no-enviction:禁止驱逐数据,也就是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失,这也是系统默认的一种淘汰策略。

    区分不同的淘汰策略选择不同的key,Redis淘汰策略主要分为LRU淘汰TTL淘汰随机淘汰三种机制。

    LRU淘汰

    LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新,server.lrulock 的值是根据 server.unixtime 计算出来进行排序的,然后选择最近使用时间最久的数据进行删除。另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。每一次访问数据,会更新对应redisObject.lru。在Redis中,LRU算法是一个近似算法,默认情况下,Redis会随机挑选5个键,并从中选择一个最久未使用的key进行淘汰。在配置文件中,按maxmemory-samples选项进行配置,选项配置越大,消耗时间就越长,但结构也就越精准。

    TTL淘汰

    Redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires。与 LRU 数据淘汰机制类似,TTL 数据淘汰机制中会先从过期时间的表中随机挑选几个键值对,取出其中 ttl ***的键值对淘汰。同样,TTL淘汰策略并不是面向所有过期时间的表中最快过期的键值对,而只是随机挑选的几个键值对。

    随机淘汰

    在随机淘汰的场景下获取待删除的键值对,随机找hash桶再次hash指定位置的dictEntry即可。

    Redis中的淘汰机制都是几近于算法实现的,主要从性能和可靠性上做平衡,所以并不是完全可靠,所以开发者们在充分了解Redis淘汰策略之后还应在平时多主动设置或更新key的expire时间,主动删除没有价值的数据,提升Redis整体性能和空间。

    Redis缓存功能,是由edis.c文件中的freeMemoryIfNeeded函数实现的。如果maxmemory被设置,那么每次在执行命令钱,该函数都会被调用来判断内存是否够用、释放内存、返回错误。如果没有足够的内存程序主逻辑将会阻止设置了REDIS_COM_DENYOOM flag的命令执行,对其返回command not allowed when used memory > ‘maxmemory’的错误消息。

    使用建议

    关于使用这6种策略,还需要根据自身系统特征,正确选择或修改驱逐:

    • 在Redis中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的。
    • 如果所有数据访问概率大致相等时,可以选择allkeys-random。
    • 如果需要通过设置不同的ttl来判断数据过期的先后顺序,此时可以选择volatile-ttl策略。
    • 如果希望一些数据能长期被保存,而一些数据可以被淘汰掉时,选择volatile-lru或volatile-random都是比较不错的。
    • 由于设置expire会消耗额外的内存,如果计划避免Redis内存在此项上的浪费,可以选用allkeys-lru 策略,这样就可以不再设置过期时间,高效利用内存了。
  • MySql优化

    MySql优化

    索引是什么

    • 索引是帮助 MySql高效获取数据的数据结构
    • 索引存储在文件系统中
    • 索引的文件存储形式与存储引擎有关

    为什么要设计索引

    为了加快数据的访问,提高我们程序的效率,例如查字典,根据字典的编码就能定位到文字的位置,系统的数据可能成百万上千万,如何快速的定位到数据,此时就需要用到索引。

    索引文件结构

    Hash

    • 需要设置一个较好的哈希算法来保证数据的均匀存储(扰动函数)
    • 利用Hash存储的话需要将所有的数据文件添加到内存,比较耗费内存空间
    • 如果等值查询是比较快,但如果范围查询,需要一个个比较,效率就比较低

    二叉树

    • 无论是二叉树还是红黑树,都会因为树的深度过深而造成IO次数变多,影响数据读取效率

    B树

    • B树特点
      • 所有键值分布在整颗树中
      • 搜索有可能在非叶子节点结束,在关键字全集内做一次查找,性能逼近二分查找
      • 每个节点最多拥有m个子树
      • 根节点至少有2个子树
      • 分支节点至少拥有m/2颗子树(除根节点和叶子节点外,其余都是分支节点)
      • 所有叶子节点都在同一层,每个节点最多可以有m-1个key,并且以升序排列
    • 实例图说明:每个节点占用一个磁盘块(页),一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址,两个关键字划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为16和34,P1指针指向的子树的数据范围为小于16,P2指针指向的子树的数据范围为16到34,P3指针指向的子树的数据范围为大于34
    • 查找关键字【28】过程
      • 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
      • 比较关键字28在区间(16,34)找到磁盘块1的指针P2
      • 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
      • 比较关键字28在区间(25,31)找到磁盘块8的指针P2
      • 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
    • 主键和数据一起存储,假设页大小为16K,一行数据为1K,3层深度只能存储4096条数据

    B+树

    • B+树是在B树的基础之上做的一种优化,变化如下:
      • B+树每个节点可以包含更多的节点,这样做的原因有两个:第一,为了降低树的高度,第二,将数据范围变为多个区间,区间越多,数据检索越快
      • 非叶子节点存储主键,叶子节点存储主键+数据,假设页大小为16K,主键为10字节,一行数据为1K,3层深度能存储的数据为4096万笔
      • 叶子节点两两指针相互连接(符合磁盘的预计特性),顺序查询性能更高

    索引分类

    • 主键索引:当给一张表创建主键索引的时候,在组织数据时一定会按主键来创建,关系到数据的组织形式
    • 唯一索引:如果给一个列创建了唯一索引,意味着这个列不允许有重复值出现
    • 普通索引:二级索引或辅助索引,给除了主键和唯一列之外创建的索引,就是普通索引
    • 全文索引:较少使用,见相关文章,一般使用ES
    • 联合索引:多个非主键和唯一列之外创建的索引,就是联合索引

    存储引擎

    不同的数据文件在磁盘里不同的组织形式

    InnoDB,MyISAM使用的是B+树,MEMORY使用Hash,

    InnoDB支持自适应Hash(用户没办法进行干预)

    局部性原理

    程序和数据访问都有聚集成群的倾向,在一个时间段内,仅使用其中一小部分(称空间局部性),或者最近访问过的程序代码和数据,很快又被访问的可能性很大(称时间局部性)

    磁盘预读

    预读的长度一般为页(page)的整倍数,页是存储器的逻辑块,操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块称为一页(在许多操作系统中,页的大小通常为4K),主存和磁盘以页为单位交换数据。

    名词解释

    • 聚簇索引:如果表设置了主键,则主键就是聚簇索引,如果表没有主键,则会默认第一个NOT NULL,且唯一(UNIQUE)的列作为聚簇索引,如果都没有,则会默认创建一个隐藏的row_id作为聚簇索引
    • 普通索引:也叫二级索引,非聚簇索引,InnoDB的普通索引叶子节点存储的是主键(聚簇索引)值,而MyISAM的普通索引存储的是记录指针
    • 回表查询:先通过普通索引的值定位到聚簇索引值,再通过聚簇索引的值定位到记录数据,需要扫描两次索引B+树,它的性能较扫一遍索引树更低
    • 索引覆盖:只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快,通常使用联合索引的方式包含所需信息来实现索引覆盖
    • 最左匹配:在MySql中建立联合索引会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配,例如创建一个(name,age)的联合索引,实际上MYSQL会建立一个nameage的普通索引,此时如果查询age的数据,并不会走联合索引,因为此索引的最左前缀为name
    • 索引下推:索引条件下推优化(Index Condition Pushdown)ICP是MySql5.6添加的,用于优化数据查询:
      • 不使用索引条件下推优化时,存储引擎通过索引检索到数据时,然后返回给MySql服务,服务再判断筛选符合条件的数据
      • 当使用索引条件下推优化时,如果存在某些被索引的列的判断条件时,MySql服务会将这一部判断条件传递给存储引擎,然后由存储引擎判断索引是否符合MySql服务传递的条件,当索引符合条件时,直接将筛选完成的数据返回给MySql服务,MySql服务不需要再进行筛选
      • 由于筛选下放到磁盘,所以需要磁盘有更高的读取效率,索引下推的优点为减少IO量,减少内存占用
    • MRR:Multi Range Reader:主要用于在使用二级索引访问数据时减少随机读,启用了MRR优化后,MySQL首先会基于索引进行数据定位并收集满足条件的主键,然后对这些主键进行排序,这样可以以主键的顺序进行表行的读取,能够减少随机读的数量,MRR优化的目的就是通过对主键进行排序后的一定程度的顺序读,减少随机读的数量(如果可以基于索引得出查询结果,则不会进行MRR优化)。见相关文章
    • FIC: 快速索引创建(Fast Index Creation) 在添加或删除二级索引时,可以不用复制原表,而是在创建或删除二级索引的时候对原表加上一个S锁,允许其他会话操作,然后根据当前表数据创建索引,新索引创建完成后,解除S锁,允许写操作。见相关文章
    • 优化器:CBO:基于成本的优化,会优化SQL语句,使执行成本降至最低。RBO:基于规则的优化

    索引长度计算

    • 所有的索引字段,如果没有设置NOT NULL, 则需要加一个字节
    • 定长字段,int占4个字节,date占3个字节,char(n)占n个字符
    • 对于变长字段varchar(n),则有n个字符 + 两个字节
    • 不同字符集,一个字符数占用的字节数不同,latin1编码的,一个字符占1个字节,gbk编码的,一个字符占2个字节,utf8编码的,一个字符占3个字节

    索引匹配方式

    全值匹配

    和索引中的所有的列进行匹配

    explain select * from staffs where name='张三' and age=23 and pos ='dev'

    匹配最左前缀

    只匹配前面的几列,最左优先,以最左边的为起点任何连续的索引都能匹配上,同时遇到范围查询(>,<,between,like)就会停止匹配。

    例如:如果查询条件b = 2 建立(a,b)顺序的索引,是匹配不到(a,b)索引的;但是如果查询条件是a = 1 and b = 2或者a=1(又或者是b = 2 and a = 1)就可以,因为优化器会自动调整a,b的顺序。再比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,因为c字段是一个范围查询,它之后的字段会停止匹配。

    最左匹配原则都是针对联合索引来说的,所以我们有必要了解一下联合索引的原理,我们都知道索引的底层是一颗B+树,那么联合索引当然还是一颗B+树,只不过联合索引的健值数量不是一个,而是多个。构建一颗B+树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建B+树。

    例子:假如创建一个(a,b)的联合索引,那么它的索引树是这样的

    可以看到a的值是有顺序的,1,1,2,2,3,3,而b的值是没有顺序的1,2,1,4,1,2。所以b = 2这种查询条件没有办法利用索引,因为联合索引首先是按a排序的,b是无序的。同时我们还可以发现在a值相等的情况下,b值又是按顺序排列的,但是这种顺序是相对的。所以最左匹配原则遇上范围查询就会停止,剩下的字段都无法使用索引。例如a = 1 and b = 2 a,b字段都可以使用索引,因为在a值确定的情况下b是相对有序的,而a>1 and b=2,a字段可以匹配上索引,但b值不可以,因为a的值是一个范围,在这个范围中b是无序的。

    匹配列前缀

    可以匹配某一列值的开头部分

    --使用索引: 
    
    explain select * from staffs where name like 'j%';
    ​
    --索引失效: 
    explain select * from staffs where name like '%j%';

    匹配范围值

    可以查找某一范围的值

    explain select * from staffs where name >'Mary'

    精确匹配某一列并范围匹配另外一列

    可以查询第一列的全部和第二列的部分

    explain select * from staffs where name='张三' and age > 23

    只访问索引的查询

    查询的时候只需要访问索引,不需要访问数据行,本质上就是索引覆盖

    explain select name,age,pos from staffs where name='张三' and age=23 and pos ='dev'