一、Mysql两种常用存储引擎myisam和innodb的文件存储结构
    1. 区别:
    myisam 不支持事物,只支持表级锁,不支持外键,物理结构(三个文件),非聚簇索引。
    innodb 支持事务和行级锁,支持外键,物理结构(两个文件),聚簇索引。
    如果系统读多写少,对原子性要求低,那么MyISAM最好的选择。且MyISAM恢复速度快,可直接用备份覆盖恢复。如果系统读少写多的时候,尤其是并发写入高的时候,InnoDB就是首选了。
    2. myism物理文件结构为:
    .frm文件:与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等。
    .myd文件:用于存储myisam表的数据
    .myi文件:用于存储myisam表的索引相关信息
    3. innodb的物理文件结构为:
    .frm文件:与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等。
    .ibd文件和.ibdata文件:
    这两种文件都是存放innodb数据的文件,之所以用两种文件来存放innodb的数据,是因为innodb的数据存储方式能够通过配置来决定是使用共享表空间存放存储数据,还是用独享表空间存放存储数据。
    独享表空间存储方式使用.ibd文件,并且每个表一个ibd文件
    共享表空间存储方式使用.ibdata文件,所有表共同使用一个ibdata文件
    使用哪种方式的参数在mysql的配置文件中 innodb_file_per_table
    4.索引不同
    (1)myisam(data存的是数据地址。索引是索引,数据是数据。)
    image.png
    (2)innodb(data存的是数据本身,索引也是数据。)
    image.png
    索引的总结:
    从历史上来说MyISAM历史更加久远,所以InnoDB性能也就更好了,在这我们需要考虑当我们修改数据库中的表的时候,数据库发生了变化,那么他们的主键的地址也就发生了变化,这样你的MyISAM的主索引和辅助索引就需要重新建立索引。而InnoDB只需要改变主索引,因为它的辅助索引是存主键的,所以这样考虑InnoDB更加高效。

    5. 常见问题
    5.1 数据库如何应对大规模写入和读取
    (1)传统的关系型数据库已经无法满足快速查询与插入数据的需求。这个时候NoSQL的出现暂时解决了这一危机。它通过降低数据的安全性,减少对事务的支持,减少对复杂查询的支持,来获取性能上的提升。但是,在有些场合NoSQL一些折衷是无法满足使用场景的,就比如有些使用场景是绝对要有事务与安全指标的。这个时候NoSQL肯定是无法满足的,所以还是需要使用关系性数据库。
    (2)缓存和读写分离
    (3)分库分表
    水平切分:不修改数据库表结构,通过对表中数据的拆分而达到分片的目的,一般水平拆分在查询数据库的时候可能会用到 union 操作(多个结果并)。
    可以根据hash或者日期进行进行分表。
    垂直切分:修改表结构,按照访问的差异将某些列拆分出去,一般查询数据的时候可能会用到 join 操作;把常用的字段放一个表,不常用的放一个表,把字段比较大的比如text的字段拆出来放一个表里面。
    分库:分表能够解决单表数据量过大带来的查询效率下降的问题,但是,却无法给数据库的并发处理能力带来质的提升。
    面对高并发的读写访问,当数据库 master 服务器无法承载写操作压力时,不管如何扩展 slave 服务器,此时都没有意义了。这个时候就需要对数据库进行拆分,从而提高数据库写入能力,这就是所谓的分库。分库可以采用对一个关键字取模的方式,来对数据访问进行路由。
    image.png
    大量数据如何分页显示:
    (1)在千万级分页时使用 limit
    image.png
    (2)设置一个自增的字段
    在待查询的数据库表上增加一个用于查询的自增字段(比如“queryId”),先按照大小顺序的倒序查出所有的queryId,因为只是查询queryId字段,即使表格中的数据量很大,该查询也会很快得到结果。然后将得到的queryId保存在应用服务器的一个数组中。
    用户在客户端进行翻页操作时,客户端将待查询的 页号 page 作为参数传递给应用服务器,服务器通过页号和queyId数组算出待查询的queyId最大和最小值,然后进行查询。

    image.png

    5.2 读写分离(MySql Proxy可以实现)
    读写分离就是在主服务器上修改数据,数据会同步到从服务器,从服务器只能提供读取数据,不能写入,实现备份的同时也实现了数据库性能的优化,以及提升了服务器安全。
    如何保证数据一致性:
    (1)主节点
    保证每次事务提交后,都能实时刷新到磁盘中,尤其是确保每次事务对应的binlog都能及时刷新到磁盘中,只要有了binlog,InnoDB就有办法做数据恢复,不至于导致主从复制的数据丢失。
    (2)从节点
    开启 relay log 自动修复机制,发生 crash 时,会自动判断哪些 relay log 需要重新从master 上抓取回来再次应用,以此避免部分数据丢失的可能性。

    优点:
    1.增加冗余
    2.增加了机器的处理能力
    3.对于读操作为主的应用,使用读写分离是最好的场景,因为可以确保写的服务器压力更小,而读又可以接受点时间上的延迟。
    image.png
    5.3 数据库的缓存重建
    (1)用一些可以提供持久化功能的缓存来实现,比如Redis
    (2)MongoDB与上面的方式不太一样,MongoDB采用mmap来将数据文件映射到内存中,所以当MongoDB重启时,这些映射的内存并不会清掉,因为它们是由操作系统维护的(所以当操作系统重启时,MongoDB才会有相同问题)。相对于其它一些自己维护Cache的数据库,MongoDB在重启后并不需要进行缓存重建与预热。
    (3)缓存重建加锁的方式,也能部分解决此问题。简单来说就是缓存重建时,当多个客户端对同一个缓存数据发起请求时,会在客户端采用加锁等待的方式,对同一个Cache的重建需要获取到相应的锁才行,只有一个客户端能拿到锁,并且只有拿到锁的客户端才能访问数据库重建缓存,其它的客户端都需要等待这个拿到锁的客户端重建好缓存后直接读缓存,其结果是对同一个缓存数据,只进行一次数据库重建访问。但是如果访问分散比较严重,还是会瞬间对数据库造成非常大的压力。
    5.4 mysql5.6和5.7的区别
    安全性

    • 用户表 mysql.user 的 plugin字段不允许为空, 默认值是 mysql_native_password,而不是 mysql_old_password,不再支持旧密码格式;
    • 增加密码过期机制,过期后需要修改密码,否则可能会被禁用,或者进入沙箱模式;
    • 增加密码过期机制,过期后需要修改密码,否则可能会被禁用,或者进入沙箱模式;
    • 提供了更为简单SSL安全访问配置,并且默认连接就采用SSL的加密方式。

    灵活性

    • MySQL数据库从5.7.8版本开始,也提供了对JSON的支持。
    • 可以混合存储结构化数据和非结构化数据,同时拥有关系型数据库和非关系型数据库的优点
    • 能够提供完整的事务支持
    • generated column是MySQL 5.7引入的新特性,所谓generated column,就是数据库中这一列由其他列计算而得

    易用性

    • 在MySQL 5.7 之前,如果用户输入了错误的SQL语句,按下 ctrl+c ,虽然能够”结束”SQL语句的运行,但是,也会退出当前会话,MySQL 5.7对这一违反直觉的地方进行了改进,不再退出会话。
    • MySQL 5.7可以explain一个正在运行的SQL,这对于DBA分析运行时间较长的语句将会非常有用。
    • sys schema是MySQL 5.7.7中引入的一个系统库,包含了一系列视图、函数和存储过程, 该项目专注于MySQL的易用性。

    例如:如何查看数据库中的冗余索引;如何获取未使用的索引;如何查看使用全表扫描的SQL语句。
    可用性

    • 在线设置 复制的过滤规则 不再需要重启MySQL,只需要停止SQLthread,修改完成以后,启动SQLthread。
    • 在线修改buffer pool的大小。
    • Online DDL MySQL 5.7支持重命名索引和修改varchar的大小,这两项操作在之前的版本中,都需要重建索引或表。
    • 在线开启GTID ,在之前的版本中,由于不支持在线开启GTID,用户如果希望将低版本的数据库升级到支持GTID的数据库版本,需要先关闭数据库,再以GTID模式启动,所以导致升级起来特别麻烦。

    性能

    • 临时表的性能改进。

      1. 临时表只在当前会话中可见<br /> 临时表的生命周期是当前连接(MySQL宕机或重启,则当前连接结束)
    • 只读事务性能改进。

    MySQL 5.7通过 避免为只读事务分配事务ID ,不为只读事务分配回滚段,减少锁竞争等多种方式,优化了只读事务的开销,提高了数据库的整体性能。

    • 加速连接处理。

    在MySQL 5.7之前,变量的初始化操作(THD、VIO)都是在连接接收线程里面完成的,现在将这些工作下发给工作线程,以减少连接接收线程的工作量,提高连接的处理速度。这个优化对那些频繁建立短连接的应用,将会非常有用。

    • 复制性能的改进 (支持多线程复制(Multi-Threaded Slaves, 简称MTS)

    MySQL的默认配置是库级别的并行复制,为了充分发挥MySQL 5.7的并行复制的功能,我们需要将slave-parallel-type配置成LOGICAL_CLOCK。

    • 支持多源复制(Multi-source replication)

    严格性改变

    • 默认启用 STRICT_TRANS_TABLES 模式。
    • 对 ONLY_FULL_GROUP_BY 模式实现了更复杂的特性支持,并且也被默认启用。
    • 其他被默认启用的sql mode还有 NO_ENGINE_SUBSTITUTION。

    默认参数的改变

    • 默认binlog格式调整为ROW格式
    • 默认binlog错误后的操作调整为ABORT_SERVER

    在先前的选项下(binlog_error_action=IGNORE_ERROR),如果一个错误发生,导致无法写入binlog,mysql-server会在错误日志中记录错误并强制关闭binlog功能。这会使mysql-server在不记录binlog的模式下继续运行,导致从库无法继续获取到主库的binlog。

    • 默认开启mysql崩溃时的binlog安全。
    • 默认调低slave_net_timeout。

    5.5 数据库事务及其隔离级别
    MySql 默认的事务级别是可重复读。
    5.5.1 定义:一组SQL语句要么同时执行成功,要么同时执行失败,是数据库操作的一个执行单元。
    5.5.2 特性:
    (1)原子性(atomicity) :表示一个事务内的所有操作都是一个整体,要么全部成功,要么全部失败。
    为了保证事务操作的原子性,必须实现基于日志的REDO/UNDO机制。
    比如数据库突然断电重启,数据库处于不一致的状态,就会读取REDO日志将已经执行完成但是没有写入磁盘的操作重复执行(保持持久性),再通过UNDO日志文件撤销 执行了一部分但是没有提交的任务(保证原子性)。
    (2)一致性(consistency) :表示一个事务内有一个操作失败了,所有更改过的数据都必须回滚到修改前的状态。
    原子性和一致性是相辅相成的,在单线程的情况下,原子性可以保证数据一致性;但是在个事务同时访问数据库的情况下,仍然可能出现数据不一致的情况,就需要引入隔离性来保证数据的一致性。
    (3)隔离性(isoation) :多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
    隔离性的实现有两种:悲观锁和乐观锁。
    (4)持久性(durability) :事务完成之后,它对于系统的影响是永久性的。
    5.5.3 名词解释
    (1)脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
    (2)不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时结果不一致。
    (3)幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
    小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

    间隙锁:是innodb中行锁的一种, 但是这种锁锁住的却不止一行数据,他锁住的是多行,是一个数据范围,间隙锁的主要作用是为了防止出现幻读。

    意向锁:数据库在申请表锁的时候可能出现另外一个事务A锁住了表中某一行的情况,但是挨个遍历每一个行判断是否被锁的效率不高,所以就需要意向锁发挥作用了。需要先判段是否含有意向锁,发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的锁会被阻塞。
    申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们使用代码来申请。
    5.5.4 数据库事务隔离级别
    image.png

    (1)这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。
    (2)保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。
    (3)读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
    (4)花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。在每个读的数据行上加锁,使之不可能相互冲突,会导致大量的超时现象
    5.5.5 MySql事务和Redis事务的区别
    MySQL会默认开启一个事务,且缺省设置是自动提交,即每成功执行一个SQL,一个事务就会马上 COMMIT。Redis默认不会开启事务,即command会立即执行,而不会排队。
    Redis与MySQL中事务的区别其根本原因就是实现方式不同造成的。
    MySQL:

    • MySQL实现事务,是基于UNDO/REDO日志。
      • UNDO日志记录修改前状态,ROLLBACK基于UNDO日志实现;
      • REDO日志记录修改后的状态 ,COMMIT基于REDO日志实现;
    • 在MySQL中无论是否开启事务,SQL都会被立即执行并返回执行结果。只是事务开启后执行后的状态只是记录在REDO日志,执行COMMIT之后,数据才会被写入磁盘。

    Redis:

    • Redis实现事务,是基于COMMANDS队列。
    • 如果没有开启事务,command将会被立即执行并返回执行结果,并且直接写入磁盘;
    • 如果事务开启,command不会被立即执行,而是排入队列并返回排队状态。调用 EXCE 才会执行 COMMANDS 队列。

    5.6 行式存储和列式存储
    image.png
    行式存储下一张表的数据都是放在一起的,但列式存储下都被分开保存了。所以它们就有了如下这些优缺点:
    image.png
    5.7 如何设计一个数据库连接池?
    使用栈来存放数据库连接,每次都从上面取出连接,使用完也放回上面。假如使用的频率特别低会导致栈底部的连接长时间未使用,则可以直接释放以节省资源。
    连接容器中超时连接的释放有两种方式:(1)在往容器中添加或者取出连接的时候释放。(2)单独开一个线程不断轮询所有连接释放超时的连接。一般采用第一种方式。
    栈中连接的使用时间是有序的。所以每次释放的时候,只需要从底部向上开始扫描,遇到超时的连接则进行释放,遇上非超时的连接则停止扫描,如果栈中连接均未超时,则只需要扫描最后一个就可以了。
    5.8 JDBC连接数据库执行查询的全过程
    (1)加载数据库驱动
    (2)创建数据连接对象
    通过 DriverManager 类创建数据库连接对象Connection。DriverManager类作用于Java程序和JDBC驱动程序之间,用于检查所加载的驱动程序是否可以建立连接,然后通过它的 getConnection方法,根据数据库的URL、用户名和密码,创建一个JDBC Connection 对象。
    image.png
    (3)创建 Statement 对象(更推荐PreparedStatement
    Statement 类主要是用于执行静态 SQL 语句并返回它所生成结果的对象。
    image.png
    (4)调用 Statement 对象的相关方法执行相对应的 SQL 语句
    通过调用Statement对象的 executeQuery() 方法进行数据的查询,而查询结果会得到 ResulSet 对象,ResulSet 表示执行查询数据库后返回的数据的集合,ResulSet对象具有可以指向当前数据行的指针。通过该对象的 next() 方法,使得指针指向下一行,然后将数据以列号或者字段名取出。如果当next()方法返回null,则表示下一行中没有数据存在。
    通过 execuUpdate() 方法用来数据的更新,包括插入和删除等操作。
    image.png

    image.png
    (5)关闭数据库连接
    使用完数据库或者不需要访问数据库时,通过 Connection 的 close() 方法及时关闭数据连接。
    image.png
    1.转换SQL。
    2.编译SQL。
    3.优化数据查询路径。
    4.执行最优化的查寻,并返回数据。(PreStatement已经做了前三部)

    5.9 PreparedStatement 和 Statement的用法区别
    (1)PreparedStatement 尽最大可能提高性能
    sql 语句被数据库的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中就会得到执行。
    PreparedStatement 会先初始化SQL,先把这个SQL提交到数据库中进行预处理,并缓存下来,多次使用可提高效率。如果仅仅执行了一次的话,和普通的对象差别不大,体现不出它预编译的优越性。
    Statement 不会初始化,没有预处理,每次都是从0开始执行SQL。
    (2)极大地提高了安全性,防止SQL注入式攻击
    在使用参数化查询的情况下,数据库系统不会将参数的内容视为 SQL 指令的一部分来处理,而是在数据库完成 SQL 指令的编译后,才套用参数运行,因此就算参数中含有破坏性的指令,也不会被数据库所运行。
    (3)代码的可读性和可维护性
    image.png

    一些其他的攻击方法:
    1、XSS攻击
    XSS攻击是Web攻击中最常见的攻击方法之一,它是通过对网页注入可执行代码且成功地被浏览器执行,达到攻击的目的,一旦攻击成功,它可以窃取cookies,可以获取用户的联系人列表,然后向联系人发送虚假诈骗信息,可以删除用户的日志等等。
    解决方法:
    (1)表单提交或者url参数传递前,对需要的参数进行过滤。
    (2)检查用户输入的内容中是否有非法内容。
    2、CSRF(Cross-site request forgery)跨站请求伪造
    攻击者盗用了你的身份,以你的名义发送恶意请求。
    解决方法:
    服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数。
    5.10 char 与 varchar 类型的区别
    (1)char 的长度是不可变的,而varchar的长度是可变的,取数据的时候,char类型的要用trim()去掉多余的空格,而varchar是不需要的。
    (2)char 类型的存取速度要比 varchar 要快得多,因为其长度固定,方便程序的存储与查找。
    (3)char 的存储方式是,对英文字符(ASCII)占用1个字节,对一个汉字占用两个字节;而 varchar 的存储方式是,对每个英文字符占用2个字节,汉字也占用2个字节。
    image.png
    5.11 MVCC
    MVCC的全称是Multi-Version Concurrency Control,通常用于数据库等场景中实现多版本的并发控制。
    MVCC是通过保存数据的多个版本来实现并发控制,当需要更新某条数据时,实现了MVCC的存储系统不会立即用新数据覆盖原始数据,而是创建该条记录的一个新的版本。
    多版本的存在允许了读和写的分离,读操作是需要读取某个版本之前的数据即可,和写操作不冲突,大大提高了性能。
    5.12 基本概念
    (1)视图
    视图是虚拟表,本身不存储数据,而是按照指定的方式进行查询。使用视图和使用表完全一样,只需要把视图当成一张表就OK了。
    可以通过视图插入数据,但是只能基于一个基础表进行插入,不能跨表更新数据。
    (2)左连接和右链接
    左连接:结果集包括连接子句中指定的左表的所有行,而不仅仅是联接列所匹配的行。如果左表的某行在右表中没有匹配行,则在相关联的结果集行中右表的所有选择列表列均为空值。
    右连接:将返回右表的所有行。如果右表的某行在左表中没有匹配行,则将为左表返回空值。
    内连接:内连接是只有符合条件的才显示。
    全连接:返回左表和右表中的所有行,MySql不支持全连接FUll JOIN,不过可以通过联合UNION来模拟(把左右连接的结果并起来)。
    交叉联接:返回左表中的所有行,左表中的每一行与右表中的所有行组合。交叉联接也称作笛卡尔积。
    (3)主键、外键
    主键是能唯一确定一条记录的标识。
    外键用于与另一张表的关联,是能确定另一张表记录的字段,用于保持数据的一致性。
    (4)Sql语句的执行顺序
    (1) from
    (3) join
    (2) on
    (4) where
    (5) group by
    (6) avg,sum….
    (7) having
    (8) select
    (9) distinct
    (10) order by
    (5) 随机选取若干条数据
    SELECT FROM 表名 ORDER BY RAND() LIMIT 10。
    二、索引
    索引是存储表中一个特定列的值的数据结构(采用B+树实现)。
    1. 索引的优缺点
    (1)优点
    创建索引可以大大提高系统的性能。
    第一、可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
    第二、通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
    第三、可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
    第四、在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
    第五、通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
    (2)缺点
    第一、创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
    第二、索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
    第三、当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
    **2. B树(B-树/B_树)、B+树、B

    2.1 Mysql索引为什么使用B+树
    mysql为什么选取B+树,本质上是因为mysql数据是存放在外部存储的。B+树是为磁盘或其他直接存取的辅助存储设备而设计的一种数据结构。
    (1)只有叶子节点才记录数据,非叶子节点只包含索引(叶子节点的最小值),这样,一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
    (2)能够提供稳定高效的范围扫描(range-query)功能;这也是为什么数据库和操作系统中的文件系统通常会采用b+树作为数据索引的原因,这个特点主要因为所有叶子节点相互连接,并且叶子节点本身依关键字的大小自小而大顺序链接。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
    2.2 B树和B+树的区别
    2.2.1 B树
    B树是为了提高磁盘或外部存储设备查找效率而产生的一种多路平衡查找树。
    每一个节点都是有key和data两个域组成。
    一棵最小度为t的B树是满足如下四个条件:
    (1)每个节点最多包含2t−1个关键字。
    (2)一个节点u中的关键字按非降序排列。
    (3)每个节点的关键字对其子树的范围分割。
    (4)所有叶子节点具有相同的深度,即树的高度h。
    image.png
    特性:
    (1)关键字集合分布在整颗树中;
    (2)任何一个关键字出现且只出现在一个结点中;
    (3)搜索有可能在非叶子结点结束;
    2.2.2 B+树**
    B+树为B树的变形结构,用于大多数数据库或文件系统的存储。
    以m阶B+树为例:
    (1)除根节点外的内部节点,每个节点最多有m个关键字,最少有⌈_m/_2⌉个关键字。
    (2)根节点要么没有子树,要么至少有2棵子树;
    (3)所有的叶子节点包含了全部的关键字以及这些关键字指向文件的指针。
    image.png

    区别:
    (1). B+树中只有叶子节点会带有全部的关键字信息(指向记录的指针ROWID),内部节点仅仅起到索引的作用。而B树则所有节点都带有要查找的有效信息,在内部节点出现的索引项不会再出现在叶子节点中。
    (2). B+树中所有叶子节点都是通过指针连接在一起,而B树不会。
    2.2.3 B*树
    B树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
    B
    树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3(代替B+树的1/2);
    B
    树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);
    如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
    所以,B树分配新结点的概率比B+树要低,空间使用率更高。
    image.png
    3.索引树的维护
    在每个数据库的查询操作中,查询优化器会根据当前数据库的综合情况(例如,cache,index,表数据等)提供最优的执行计划。
    (1) 更新索引统计信息
    (2)清理索引以及表碎片
    *4. 索引失效场景

    (1).WHERE子句的查询条件里有不等于号(WHERE column!=…),MYSQL将无法使用索引。
    (2).WHERE子句的查询条件里使用了函数(如:WHERE DAY(column)=…),MYSQL将无法使用索引
    (3).在JOIN操作中(需要从多个数据表提取数据时),MYSQL只有在主键和外键的数据类型相同时才能使用索引,否则即使建立了索引也不会使用
    (4).如果WHERE子句的查询条件里使用了比较操作符LIKE和REGEXP,MYSQL只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。比如说,如果查询条件是LIKE ‘abc%’,MYSQL将使用索引;如果条件是LIKE ‘%abc’,MYSQL将不使用索引。
    (5).在ORDER BY操作中,排序的列同时也在WHERE中时,MYSQL将无法使用索引。
    (6).如果某个数据列里包含着许多重复的值,就算为它建立了索引也不会有很好的效果。
    (7).如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)。
    注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引。
    (8).列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。
    (9).NOT IN 和 <> 操作都不会使用索引将进行全表扫描。NOT IN 可以 NOT EXISTS代替,id<>3则可使用id>3 or id<3来代替。

    (7).索引有用的情况下就太多了。基本只要建立了索引,除了上面提到的索引不会使用的情况下之外,其他情况只要是使用在WHERE条件里,ORDER BY 字段,联表字段,一般都是有效的。 建立索引要的就是有效果。 不然还用它干吗? 如果不能确定在某个字段上建立的索引是否有效果,只要实际进行测试下比较下执行时间就知道。

    (9).如果列类型是字符串,那一定要在条件中将数据使用引号引起来,否则不使用索引。
    (10).如果 mysql 估计使用全表扫描要比使用索引快,则不使用索引。
    5. 聚簇索引、非聚簇索引、联合索引
    5.1 聚簇索引(Innodb使用)
    表中数据按照索引的顺序来存储的,也就是说索引项的顺序与表中记录的物理顺序一致。对于聚集索引,叶子结点即存储了真实的数据行(直达),不再有另外单独的数据页。 在一张表上最多只能创建一个聚集索引,因为真实数据的物理顺序只能有一种。
    Innodb中的每张表都会有一个聚集索引,如果一个主键被定义了,那么这个主键就是聚集索引;如果没有主键被定义,那么该表的第一个唯一非空索引被作为聚集索引;如果没有主键也没有合适的唯一索引,那么 innodb 内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,该列的值会随着数据的插入自增。
    5.2 非聚簇索引(Myisam使用)
    表中数据存储顺序与索引顺序无关。对于非聚集索引,叶结点包含索引字段值及指向数据页数据行的逻辑指针(不能直达),其行数量与数据表行数据量一致。一个表可以有多个非聚簇索引。
    5.3 联合索引
    联合索引又叫复合索引,两个或更多个列上的索引被称作复合索引,对于复合索引: Mysql 从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index (a,b,c). 可以支持a | a,b| a,b,c 3种组合进行查找,但不支持 b,c进行查找 。当最左侧字段是常量引用时,索引就十分有效。
    6. 哪些列适合建立索引?
    (1)表的主键、外键必须有索引; (2)经常与其他表进行连接的表,在连接字段上应该建立索引; (3)经常出现在Where子句中的字段,特别是大表的字段,应该建立索引; (4)索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
    (5)频繁进行数据操作的表,不要建立太多的索引; (6)列中含有null值,则不要建立索引。
    7. 索引是越多越好吗?
    索引固然可以提高相应的 select 效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
    8. 索引的分类
    8.1 物理存储角度
    (1)聚集索引(clustered index)
    (2)非聚集索引(non-clustered index)
    8.2 逻辑角度
    (1)普通索引:仅加速查询
    (2)唯一索引:加速查询 + 列值唯一(可以有null)
    (3)主键索引:加速查询 + 列值唯一(不可以有null)+ 表中只有一个
    (4)组合(多列)索引:多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。
    (5)空间索引:空间索引是对空间数据类型的字段建立的索引。
    8.3 数据结构角度
    (1)B+树索引
    将索引值按一定的算法,存入一个树形的数据结构中(二叉树),每次查询都是从树的入口root开始,依次遍历node,获取leaf。这是MySQL里默认和最常用的索引类型。
    (2)hash索引
    HASH索引可以一次定位,不需要像树形索引那样逐层查找,因此具有极高的效率。但是,这种高效是有条件的,即只在“=”和“in”条件下高效,对于范围查询、排序及组合索引仍然效率不高。
    (3)FULLTEXT索引
    对文本的内容进行分词,进行搜索。
    (4)RTREE
    RTREE在MySQL很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有MyISAM、BDb、InnoDb、NDb、Archive几种。
    三、Mysql的优化、存储过程、触发器
    Mysql的优化,大体可以分为三部分:索引的优化,sql语句的优化,表的优化。同时可以使用缓存,增加效率。
    1. 索引的优化

    • 只要列中含有NULL值,就最好不要在此列设置索引,复合索引如果有NULL值,此列在使用时也不会使用索引。
    • 尽量使用短索引,如果可以,应该制定一个前缀长度。
    • 对于经常在where子句使用的列,最好设置索引,这样会加快查找速度
    • 对于有多个列where或者order by子句的,应该建立复合索引
    • 对于like语句,以%或者‘-’开头的不会使用索引,以%结尾会使用索引
    • 尽量不要在列上进行运算(函数操作和表达式操作)
    • 尽量不要使用not in 和 <> 操作(不等于)

    2. sql语句的优化
    查询优化就要尽可能避免全表扫描。

    • 查询时,能不要就不用,尽量写全字段名,因为从数据库中读出越多的数据,查询就会变得越慢。
    • where 及 order by 涉及的列上建立索引。
    • 大部分情况连接效率远大于子查询(使用连接(JOIN)来代替子查询(Sub-Queries),之所以更有效率一些,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作)
    • 使用联合(UNION)来代替手动创建的临时表
    • 使用事务(它的作用是:要么语句块中每条语句都操作成功,要么都失败。换句话说,就是可以保持数据库中数据的一致性和完整性。事物以BEGIN关键字开始,COMMIT关键字结束。在这之间的一条SQL操作失败,那么,ROLLBACK命令就可以把数据库恢复到BEGIN开始之前的状态,事务的另一个重要作用是当多个用户同时使用相同的数据源时,它可以利用锁定数据库的方法来为用户提供一种安全的访问方式,这样可以保证用户的操作不被其它的用户所干扰
    • 多表连接时,尽量小表驱动大表,即小表 join 大表
    • 对于经常使用的查询,可以开启缓存

    3.表的优化

    • 表的字段尽可能用NOT NULL(选取最适用的字段属性
    • 字段长度固定的表查询会更快
    • 把数据库的大表按时间或一些标志分成小表
    • 将表分区

    4. MySql查询优化
    1)缓存,在持久层或持久层之上做缓存。
    2)恰当地使用索引。
    3)表的拆分。
    4)数据库表的大字段剥离。
    5)放弃关系数据库的某些特性,引入NoSQL数据库。
    5. 存储过程
    (1)定义
    存储过程 Procedure 是一组为了完成特定功能的SQL语句集合,经编译后存储在数据库中,用户通过指定存储过程的名称并给出参数来执行。
    说的简单一点,存储过程就像数据库中运行的方法。
    (2)优点
    1.存储过程只在创造时进行编译,以后每次执行存储过程都不需再重新编译,而一般 SQL语句每执行一次就编译一次,所以使用存储过程可提高数据库执行速度。 2.当对数据库进行复杂操作时(如对多个表进行Update,Insert,Query,Delete时),可将此复杂操作用存储过程封装起来与数据库提供的事务处理结合一起使用。这些操作,如果用程序来完成,就变成了一条条的 SQL语句,可能要多次连接数据库。而换成存储过程,只需要连接一次数据库就可以了。 3.存储过程可以重复使用(像调用方法一样),可减少数据库开发人员的工作量。 4.安全性高,可设定某些用户才具有对指定存储过程的使用权。
    (3)缺点
    调试麻烦、移植问题、重新编译问题。
    6. 触发器
    触发器是一种特殊类型的存储过程,它不同于一般的存储过程(一般的存储过程名称被直接调用),而触发器主要是通过事件进行触发而被执行,一般当表中数据发生变化时自动强制执行。
    触发器的主要作用是自动化操作,减少了手动操作以及出错的几率。
    四、数据库自增主键可能的问题
    使用自增主键对数据库做分库分表,可能出现一些诸如主键重复等的问题;在数据库导入的时候,可能会因为主键出现一些问题。主要业务表的主键应该配置一个合理的策略,尽量避免自增AUTO_INCREMENT。
    五、数据库锁
    1.锁的级别和类型
    1.1 MySQL有三种锁的级别:行级、页级、表级。
    行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
    页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
    表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
    1.2 锁的类型
    在数据库中有两种基本的锁类型:排它锁(Exclusive Locks,即X锁)和共享锁(Share Locks,即S锁)。当数据对象被加上排它锁时,其他的事务不能对它读取和修改。加了共享锁的数据对象可以被其他事务读取,但不能修改。数据库利用这两种基本的锁类型来对数据库的事务进行并发控制。
    1.3 共享锁和排他锁总结:
    (1)mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型 。
    (2)排他锁不能和其他锁共存
    (3)共享锁可以和其他锁共存(由于排他锁的特性,共享锁只能和共享锁共存)
    1.4 乐观锁的实现
    1.4.1 数据版本
    这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
    1.4.2 时间戳
    第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
    2.数据库的死锁
    2.1 概念
    当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。加锁是实现数据库并发控制的一个非常重要的技术。在实际应用中经常会遇到的与锁 相关的异常情况,当两个事务需要一组有冲突的锁,而不能将事务继续下去的话,就会出现死锁,严重影响应用的正常执行。
    2.2 死锁的案例分析和解决
    (1)一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这样死锁就产生了。
    解决:这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操作时, 尽量按照相同的顺序进行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。
    (2)如果在事务中执行了一条不满足条件的update语句,则执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。类似的情况还有当表中的数据量非常庞大而索引建的过少或不合适的时候,使得经常发生全表扫描,最终应用系统会越来越慢,最终发生阻塞或死锁。
    解决:SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。
    3.高并发下,如何做到安全的修改同一行数据。
    使用悲观锁; 悲观锁本质是当前只有一个线程执行操作,结束了唤醒其他线程进行处理。
    也可以缓存队列中锁定主键。
    4.乐观锁和悲观锁是什么,INNODB 的行级锁有哪 2 种,解释其含义
    乐观锁是设定每次修改都不会冲突,只在提交的时候去检查,悲观锁设定每次修改都会冲突,持有排他锁。
    行级锁分为共享锁和排他锁两种 共享锁又称读锁 排他锁又称写锁。

    六、Redis
    Redis是一个开源,使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value数据结构服务器,可用作数据库,高速缓存和消息队列代理。
    使用 redis 的好处:
    (1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)。
    速度快的原因:①绝大部分请求是纯粹的内存操作;②采用单进程单线程,避免了不必要的上下文切换和竞争条件;③非阻塞IO(多路复用io)。
    (2) 支持丰富数据类型,支持string,list,hash,set,sorted set。
    (3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行。
    (4) 丰富的特性:可用于缓存,消息队列,按key设置过期时间,过期后将会自动删除。

    单进程多线程模型:MySQL、Memcached、Oracle(Windows版本);
    多进程模型:Oracle(Linux版本)。

    1. redis的数据淘汰策略
    redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略:

    1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
    2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
    3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
    4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
    5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
    6. no-enviction(驱逐):禁止驱逐数据(永不回收数据)

    2. 数据库和缓存的数据一致性
    2.1 mySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据。
    数据淘汰策略
    2.2 redis缓存和mysql数据库同步
    (1)Cache Aside Pattern(先更新数据库,后删除缓存)
    失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
    命中:应用程序从cache中取数据,取到后返回。
    更新:先把数据存到数据库中,成功后,再让缓存失效(删除缓存)。
    原因
    ① 如果是先删除缓存,然后更新数据库,一读一写操作同时工作,读操作在缓存找不到数据,然后从数据库更新缓存,然后写操作更新数据库,使得缓存是脏数据;
    ② 先更新数据库,后更新缓存的方案遇到并发写操作会导致脏数据,A、B两个写操作,A先更新了数据库,B后更新了数据库,B更新了缓存,然后A更新了缓存,则缓存中会存在脏数据。
    虽然此种方法也会出现问题,但是概率很低。一个读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,会造成脏数据。
    或者是先写了库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,读操作直接从原来的缓存读取数据,则也会出现数据不一致情况。
    image.png

    image.png
    (2)Read Through
    查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出)由缓存服务自己从数据库加载数据。(Cache Aside是由调用方负责把数据加载入缓存)
    (3)Write Through
    当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)。
    image.png
    (4)Write Behind Caching Pattern
    更新数据的时候,只更新缓存,不更新数据库,缓存会异步批量的更新数据库。
    缺点:数据不是强一致性的,而且可能会丢失;实现逻辑比较复杂,因为需要检查有哪数据是被更新了的,需要刷到持久层上。
    3.Redis的缓存失效策略 缓存穿透和雪崩的解决办法
    3.1 3种过期策略
    (1)定时删除
    在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
    (2)惰性删除
    key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
    (3)定期删除
    每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作。
    总结:
    定时删除和定期删除为主动删除:Redis会定期主动淘汰一批已过期的key。
    惰性删除为被动删除:用到的时候才会去检验key是不是已过期,过期就删除。
    惰性删除为redis服务器内置策略。
    3.2 Redis采用的过期策略
    惰性删除+定期删除

    • 惰性删除流程
      • 在进行get或setnx等操作时,先检查key是否过期,
      • 若过期,删除key,然后执行相应操作;
      • 若没过期,直接执行相应操作
    • 定期删除流程(简单而言,对指定个数的每一个库随机删除小于等于指定个数的过期key)
      • 遍历每个数据库(就是redis.conf中配置的”database”数量,默认为16)
        • 检查当前库中的指定个数的key(默认是每个库检查20个key,注意相当于该循环执行20次)
          • 如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历
          • 随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key
          • 判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。

    3.3 缓存穿透
    3.3.1 定义
    用户查询的时候,在缓存中找不到,每次都要去数据库中查询。大并发的缓存穿透会导致缓存雪崩。
    3.3.2 解决方案
    (1)如果查询数据库也为空,直接设置一个默认值存放到缓存(但它的过期时间会很短,最长不超过五分钟),这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
    (2)根据缓存数据Key的规则进行过滤。在做缓存规划的时候,Key有一定规则的话,可以采取这种办法。这种办法只能缓解一部分的压力,过滤和系统无关的查询,但是无法根治。
    (3)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力。
    3.4 缓存雪崩
    3.4.1 定义
    缓存雪崩可能是因为数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。
    3.4.2 解决方案
    (1)采用加锁计数,或者使用合理的队列数量来避免缓存失效时对数据库造成太大的压力。这种办法虽然能缓解数据库的压力,但是同时又降低了系统的吞吐量。
    (2)尽量让失效时间点均匀分布(缓存失效时间分散开,可以在原有的失效时间基础上增加一个随机值),避免缓存雪崩的出现。
    (3)如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。
    4.缓存机器增删如何对系统影响最小,一致性哈希的实现
    4.1 实现原理
    一致性哈希算法,在不同的系统环境下,具有不同的实现方式。但是,实现的大致过程还是一致的。
    (1)静态映射 —> 动态映射
    普通的哈希算法,比如上面提到的(hash % N),由于数据和节点是静态绑定的。也就是说,进行哈希运算后,数据和节点之间的关系就确定了。一旦节点数发生变化,所有的哈希都失效了。
    一致性哈希算法,是如何解决这个问题的呢?一致性哈希算法引入了环的概念,并且最关键的创新点是:将节点的分配和数据的分配,拆分成了2个独立的过程。数据和节点的关联,不是通过哈希算法直接建立起来的。这样数据和节点就相对独立了,节点A的变化,并不会影响到整个分布式系统,因为此时不需要对所有数据进行哈希运算。
    一致性哈希算法的进步之处在于,把数据和节点的关联,从“静态”变成了“动态”。
    (2)顺时针就近查找节点
    一致性哈希算法是怎么把数据和节点关联起来的呢?把节点和数据都哈希到圆环上以后,数据通过顺时针方向查找的方式与节点建立关联。数据把顺时针找到的第一个节点作为自己的存储位置,这样一来,数据和节点就完美的关联起来了。
    image.png
    其中m代表缓存的节点,value代表需要缓存的数据。哈希值为900的点在顺时针距离表示0的那个蓝点最近,因此这个具有哈希值900的数据将记录在缓存实例m1中。如果其中的一个缓存实例失效了,那么需要由该实例所记录的数据将暂时失效,而其它实例所记录的数据仍然还在。
    image.png
    4.2 优缺点
    优点:相比于普通的哈希算法,一致性哈希算法对于节点的动态增删,具有一定的容错性和可扩展性。
    缺点:
    (1)在节点A挂掉的情况下,映射到节点A上的数据,会受到影响。因为之前映射到A节点的数据,现在按照顺时针查找,映射到了节点A的下一个节点。同样的,在增加一个节点时,也会影响一部分数据。
    (2)当集群中的节点数量很少时,会造成数据倾斜。数据倾斜的问题,可以通过虚拟节点的方式来解决。在虚拟节点和实际节点之间再增加一次映射。
    5. Redis持久化
    5.1 RDB持久化
    5.1.1 RDB持久化可以在指定的时间间隔内生成数据集的时间点快照,默认redis是会以快照的形式将数据持久化到磁盘的(一个二进制文件,dump.rdb,这个文件名字可以指定),在配置文件中的格式是:save N M表示在N秒之内,redis至少发生M次修改则redis抓快照到磁盘。当然我们也可以手动执行save或者bgsave(异步)做快照。
    5.1.2 优点
    (1)RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。
    (2)RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心,或者亚马逊 S3 中。
    (3)RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
    (4)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
    5.1.3 缺点
    (1)如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
    (2)每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork()可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。
    5.1.4 原理
    当redis需要做持久化时,redis会fork一个子进程;子进程将数据写到磁盘上一个临时RDB文件中;当子进程完成写临时文件后,将原来的RDB替换掉,这样的好处就是可以copy-on-write(写时复制)。
    5.2 AOF持久化
    5.2.1 AOF持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集,AOF文件中全部以redis协议的格式来保存,新命令会被追加到文件的末尾,redis还可以在后台对AOF文件进行重写,文件的体积不会超出保存数据集状态所需要的实际大小。
    5.2.2 优点
    (1)使用 AOF 持久化会让 Redis 变得非常耐久(much more durable):你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。
    (2)AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。
    (3)Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
    (4)AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单。
    5.2.3 缺点
    (1)对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
    (2)根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
    (3)AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。
    5.2.4 原理
    首先redis会fork一个子进程;子进程将最新的AOF写入一个临时文件;父进程增量的把内存中的最新执行的修改写入(这时仍写入旧的AOF,rewrite如果失败也是安全的);当子进程完成rewrite临时文件后,父进程会收到一个信号,并把之前内存中增量的修改写入临时文件末尾;这时redis将旧AOF文件重命名,临时文件重命名,开始向新的AOF中写入。
    6.Redis的并发竞争问题如何解决,Redis事务的CAS操作
    6.1 Redis事务的CAS操作
    (1)乐观锁介绍:
    watch指令在redis事物中提供了CAS的行为。为了检测被watch的keys是否有多个clients同时改变引起冲突,这些keys将会被监控。如果至少有一个被监控的key在执行exec命令前被修改,整个事物将会回滚,不执行任何动作,从而保证原子性操作,并且执行exec会得到null的回复。
    (2)乐观锁工作机制:
    watch 命令会监视给定的每一个key,当exec时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令,及客户端连接关闭都会清除连接中的所有监视。还有,如果watch一个不稳定(有生命周期)的key并且此key自然过期,exec仍然会执行事务队列的指令。
    6.2 并发竞争问题
    Redis的并发竞争问题,主要是发生在并发写竞争。
    (1)可以使用独占锁的方式,类似操作系统的mutex机制。(不过实现相对复杂,成本较高)
    (2)使用乐观锁的方式进行解决(成本较低,非阻塞,性能较高)
    本质上是假设不会进行冲突,使用redis的命令watch进行构造条件。
    watch price
    get price $price
    $price = $price + 10
    multi
    set price $price
    exec
    watch这里表示监控该key值,后面的事务是有条件的执行,如果从watch到exec语句执行时,watch的key对应的value值被修改了,则事务不会执行。
    该乐观锁机制可以简单明了的解决了写冲突的问题。如果同时有多个请求进行写操作,例如同一时刻有100个请求过来,那么只会有一个最终成功,其余99个全部会失败,效率不高。而且从业务层面,有些是不可接受的场景。在这种情况下,如果想让总体效率最大化,可以采用排队的机制进行。
    将所有需要对同一个key的请求进行入队操作,然后用一个消费者线程从队头依次读出请求,并对相应的key进行操作。
    这样对于同一个key的所有请求就都是顺序访问,正常逻辑下则不会有写失败的情况产生 。从而最大化写逻辑的总体效率。
    7. Redis集群
    image.png
    每一个蓝色的圈都代表着一个redis的服务器节点。它们任何两个节点之间都是相互连通的。客户端可以与任何一个节点相连接,然后就可以访问集群中的任何一个节点。对其进行存取和其他操作。
    image.png
    7.1 原理:
    首先,在redis的每一个节点上,都有这么两个东西,(1)一个是插槽(slot)。可以理解为是一个可以存储两个数值的一个变量,这个变量的取值范围是:0-16383。(2)还有一个就是cluster。我个人把这个cluster理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
    image.png
    7.2 redis的投票机制:
    每一个节点都存有这个集群所有主节点(master)以及从节点(slave)的信息。它们之间通过互相的ping-pong判断节点是否可以连接上。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点。如果某个节点和所有从节点全部挂掉,我们集群就进入fail状态。还有就是如果有一半以上的主节点宕机,那么我们集群同样进入fail状态。
    7.2.1 投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超时(cluster-node-timeout),认为当前master节点挂掉。
    7.2.2 什么时候整个集群不可用(cluster_state:fail)?
    (1)如果集群任意master挂掉,且当前master没有slave.集群进入fail状态。
    (2)如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态.
    7.3 redis集群的数据分片
    (1)最简单的执行分片的方式之一是范围分片(range partitioning),通过映射对象的范围到指定的 Redis 实例来完成分片。例如,我可以假设用户从 ID 0 到 ID 10000 进入实例 R0,用户从 ID 10001 到 ID 20000 进入实例 R1,等等。
    (2)哈希分片(hash partitioning)。
    8.用Redis实现一段恶意登录保护的代码,限制1小时内每用户Id最多只能登录5次
    (主要利用了设置key过期时间的功能)
    image.png
    9.Memcache与Redis的区别
    (1)存储方式Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis有部份存在硬盘上,这样能保证数据的持久性。
    (2)数据支持类型 Memcache对数据类型支持相对简单(只有string)。 Redis有复杂的数据类型。
    (3)使用底层模型不同它们之间底层实现方式以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
    (4)Redis的速度比memcached快很多;
    (5)Redis支持数据的备份,即 master-slave 模式的数据备份。
    (6)value大小:redis最大可以达到1GB,而memcache只有1MB
    10.消息队列
    10.1 redis和kafka做消息队列的区别
    (1)redis 消息推送(基于分布式 pub/sub)多用于实时性较高的消息推送,并不保证可靠。其他的mq和kafka保证可靠但有一些延迟。redis-pub/sub断电就清空,而使用redis-list作为消息推送虽然有持久化,但是又太弱智,也并非完全可靠不会丢。
    (2)redis 发布订阅除了表示不同的 topic 外,并不支持分组,比如kafka中发布一个东西,多个订阅者可以分组,同一个组里只有一个订阅者会收到该消息,这样可以用作负载均衡。
    10.2 消息队列的比较
    (1)RabbitMQ
    RabbitMQ在数据一致性、稳定性和可靠性方面比较优秀,对路由,负载均衡或者数据持久化都有很好的支持。而且直接或间接的支持多种协议,对多种语言支持良好。但是其性能和吞吐量差强人意,由于Erlang语言本身的限制,二次开发成本较高。
    (2)ZeroMQ
    因为它的简单灵活,如果我们想作为消息队列使用的话,需要开发大量代码。而且,ZeroMQ不支持消息持久化,其定位并不是安全可靠的消息传输,所以还需要自己编码保证可靠性。简而言之一句话,ZeroMQ很强大,但是想用好需要自己实现。
    (3)ActiveMQ(下一代Apollo)
    ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。
    (4)Kafka
    是一个高性能跨语言分布式Publish/Subscribe消息队列系统,以Pull的形式消费消息。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡。
    (5)RocketMQ
    RcoketMQ 是一款低延迟、高可靠、可伸缩、易于使用的消息中间件。具有以下特性:
    支持发布/订阅(Pub/Sub)和点对点(P2P)消息模型
    在一个队列中可靠的先进先出(FIFO)和严格的顺序传递
    支持拉(pull)和推(push)两种消息模式
    单一队列百万消息的堆积能力
    支持多种消息协议,如 JMS、MQTT 等
    分布式高可用的部署架构,满足至少一次消息传递语义
    提供 docker 镜像用于隔离测试和云集群部署
    提供配置、指标和监控等功能丰富的 Dashboard
    10.3 kafka和RocketMQ的对比
    (1)数据可靠性

    • Kafka使用异步刷盘方式,异步Replication
    • RocketMQ支持异步实时刷盘,同步刷盘,同步Replication,异步Replication

    (2)消息投递实时性

    • Kafka使用短轮询方式,实时性取决于轮询间隔时间
    • RocketMQ使用长轮询,同Push方式实时性一致,消息的投递延时通常在几个毫秒。

    (3)消费失败重试

    • Kafka消费失败不支持重试
    • RocketMQ消费失败支持定时重试,每次重试间隔时间顺延

    (4)严格的消息顺序

    • Kafka支持消息顺序,但是一台Broker宕机后,就会产生消息乱序
    • RocketMQ支持严格的消息顺序,在顺序消息场景下,一台Broker宕机后,发送消息会失败,但是不会乱序。

    (5)定时消息

    • Kafka不支持定时消息
    • RocketMQ支持两类定时消息
      • 开源版本RocketMQ仅支持定时Level
      • 阿里云ONS支持定时Level,以及指定的毫秒级别的延时时间

    (6)消息查询

    • Kafka不支持消息查询
    • RocketMQ支持根据Message Id查询消息,也支持根据消息内容查询消息。

    10.4 消息的幂等性
    10.4.1 幂等性
    幂等性是系统接口对外的一种承诺,指的是使用相同参数对同一资源重复调用某个接口的结果与调用一次的结果相同。
    10.4.2 实现方法
    image.png
    (1)上半场幂等(步骤1、2、3)
    ① 发送端MQ-client将消息发给服务端MQ-server
    ② 服务端MQ-server将消息落地
    ③ 服务端MQ-server回ACK给发送端MQ-client
    如果③丢失,发送端MQ-client超时后会重发消息,可能导致服务端MQ-server收到重复消息。
    此时重发是MQ-client发起的,消息的处理是MQ-server,为了避免步骤②落地重复的消息,对每条消息,MQ系统内部必须生成一个inner-msg-id,作为去重和幂等的依据,这个内部消息ID的特性是:
    (1)全局唯一
    (2)MQ生成,具备业务无关性,对消息发送方和消息接收方屏蔽
    有了这个inner-msg-id,就能保证上半场重发,也只有1条消息落到MQ-server的DB中,实现上半场幂等。
    (2)下半场幂等(步骤4、5、6)
    ④服务端MQ-server将消息(添加一个id)发给接收端MQ-client
    ⑤接收端MQ-client回ACK给服务端
    ⑥服务端MQ-server将落地消息删除
    需要强调的是,接收端MQ-client回ACK给服务端MQ-server,是消息消费业务方的主动调用行为,不能由MQ-client自动发起,因为MQ系统不知道消费方什么时候真正消费成功。
    如果⑤丢失,服务端MQ-server超时后会重发消息,可能导致MQ-client收到重复的消息。
    此时重发是MQ-server发起的,消息的处理是消息消费业务方,消息重发势必导致业务方重复消费,为了保证业务幂等性,业务消息体中,必须有一个biz-id,作为去重和幂等的依据,这个业务ID的特性是:
    (1)对于同一个业务场景,全局唯一
    (2)由业务消息发送方生成,业务相关,对MQ透明
    (3)由业务消息消费方负责判重,以保证幂等
    有了这个业务ID,才能够保证下半场消息消费业务方即使收到重复消息,也只有1条消息被消费,保证了幂等。
    10.5 消息队列存在的问题
    10.5.1 消息的顺序问题
    image.png
    将M1、M2发送到同一个Server上,将M1和M2发往同一个消费者,且发送M1后,需要消费端响应成功后才能发送M2。如果M1被发送到消费端后,消费端1没有响应,会重发M1到另外一个消费端2。存在的问题就是重复消费问题。
    10.5.2 消息的重复问题
    (1)消费端处理消息的业务逻辑保持幂等性。
    (2)保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。
    11. redis的数据类型的实现
    Redis共有五种对象的类型,分别是:
    image.png

    image.png
    为了提高存储效率与程序执行效率,每种对象的底层数据结构实现都可能不止一种。encoding就表示了对象底层所使用的编码。
    底层数据结构共有八种,如下表所示:
    image.png
    (1) string
    字符串对象的编码可以是int、raw或者embstr。
    如果一个字符串的内容可以转换为long,那么该字符串就会被转换成为long类型,对象的ptr就会指向该long,并且对象类型也用int类型表示。
    普通的字符串有两种,embstr和raw。embstr应该是Redis 3.0新增的数据结构,在2.8中是没有的。如果字符串对象的长度小于39字节,就用embstr对象。否则用传统的raw对象。
    (2) list
    列表对象的编码可以是ziplist或者linkedlist。
    ziplist是一种压缩列表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。但当数据量过大时ziplist就不是那么好用了。因为为了保证存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。
    linkedlist是一种双向链表。它的结构比较简单,节点中存放pre和next两个指针,还有节点相关的信息。当每增加一个node的时候,就需要重新malloc一块内存。
    (3)hash
    哈希对象的底层实现可以是ziplist或者hashtable。
    ziplist中的哈希对象是按照key1,value1,key2,value2这样的顺序存放来存储的。当对象数目不多且内容不大时,这种方式效率是很高的。
    hashtable的是由dict这个结构来实现的。
    (4)set
    集合对象的编码可以是intset或者hashtable。如果value能够表示为整数,则使用intset类型保存value。intset是一个整数集合,里面存的为某种同一类型的整数.intset使用和ziplist相似的实现方式保存整数;数据量大时,切换为使用hash table保存各个value。集合中最大的成员数为 2^32 - 1.
    (5)zset
    有序集合的编码可能两种,一种是ziplist,另一种是skiplist与dict的结合。
    ziplist作为集合和作为哈希对象的存储是一样的,member和score顺序存放。按照score从小到大顺序排列。它的结构不再复述。
    skiplist是一种跳跃表,它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。但它的实现比较简单,可以作为平衡树的替代品。

    12. redis辅助存储 + mysql主存储(排行榜功能的实现)
    image.png
    (1)程序同时写Redis和MySQL(靠谱,但性能一般)
    image.png

    方案存在的问题:无法应对大数据量的穿透、雪崩和热点Key问题。
    (2)程序写Redis,并将写放入MQ写MySQL(性能好,但是不稳定)
    image.png
    方案存在的问题:数据存在丢失风险,适合于将数据库作为最终持久化方法、数据可以少量丢失的场景。
    七、缓存
    7.1、guava chche
    7.1.1 底层存储LocalCache
    LocalCache是存储层,是真正意义上数据存放的地方,继承了java.util.AbstractMap同时也实现了ConcurrentMap接口,实现方式和ConcurrentHashMap的实现相同,都是采用分segment来细化管理HashMap中的节点Entry,采用了细粒度锁的方式来增大并发性能。

    八、分布式和负载均衡
    1、分布式、集群和负载均衡的概念
    (1)分布式:一个业务分拆多个子业务,部署在不同的服务器上;高性能和高并发。(配菜师,厨师)
    (2)集群:同一个业务,部署在多个服务器上;高可用。(两个厨艺一样的厨师)
    (3)负载均衡:网络的负载均衡是一种动态均衡技术,常见的实现方式是通过一些工具实时地分析数据包,掌握网络中的数据流量状况,把任务合理均衡地分配出去。负载均衡的核心就是“分摊压力”。
    2、 负载均衡
    【协议层】http重定向协议实现负载均衡
    【协议层】dns域名解析负载均衡
    我们提交的请求不会直接发送给想要访问的网站,而是首先发给域名服务器,它会把域名解析成IP地址并返回给我们,我们收到IP之后才会向该IP发起请求。
    DNS服务器有一个天然的优势,如果一个域名指向了多个IP地址,那么每次进行域名解析时,DNS只要选一个IP返回给用户,就能够实现服务器集群的负载均衡,DNS服务器把所有的请求平均分配给后端服务器。
    缺点:
    发现某一台后端服务器发生故障时,即使我们立即将该服务器从域名解析中去除,但由于DNS服务器会有缓存,该IP仍然会在DNS中保留一段时间,那么就会导致一部分用户无法正常访问网站。这是一个致命的问题,好在这个问题可以用动态DNS来解决。
    【协议层】反向代理负载均衡
    nginx
    【网络层】IP负载均衡
    【链路层】数据链路层负载均衡
    (1)LVS(Linux Virtual Server)
    image.png
    LVS架设的服务器集群系统由三个部分组成:最前端的负载均衡层(Loader Balancer),中间的服务器群组层,用Server Array表示,最底层的数据共享存储层,用Shared Storage表示。在用户看来所有的应用都是透明的,用户只是在使用一个虚拟服务器提供的高性能服务。
    优点:
    LVS的负载能力强,因为其工作方式逻辑非常简单,仅进行请求分发;而且工作在网络的第4层(传输层),没有流量,所以其效率不需要有过多的忧虑,所以LVS可以对几乎所有应用进行负载均衡,包括Web、数据库等。
    性能好,接近硬件设备的网络吞吐和连接负载能力。
    LVS的DR模式,支持通过广域网进行负载均衡,这个其他任何负载均衡软件目前都不具备。
    缺点
    LVS比较依赖网络环境。只有使用DR模式且服务器在同一网段内分流,效果才能得到保证。
    比较重型。另外社区不如nginx活跃。
    (2)NGINX
    image.png
    一个 master 进程(root用户身份认证),生成一个或者多个 worker 进程。master主要的作用只是启动worker,加载配置文件,负责系统的平滑升级。其它的工作是交给worker。当worker被启动之后,也只是负责一些web最简单的工作,而其它的工作都是由worker中调用的模块来实现的。
    优点:
    Nginx工作在网路第7层(应用层),所以可以对HTTP应用实施分流策略,比如域名、结构等。相比之下,LVS并不具备这样的功能,所以Nginx可使用的场合远多于LVS。
    性能好,可以负载超过1万的并发。
    功能多,除了负载均衡,还能作 Web 服务器,而且可以通过Geo模块来实现流量分配。
    社区活跃,第三方补丁和模块很多。
    支持gzip proxy。
    缺点:
    不支持 session 保持。
    对后端 realserver 的健康检查功能效果不好。而且只支持通过端口来检测,不支持通过 url 来检测。