• 后端研发工程师面经——数据库


    4. 数据库

    4.1 数据库的四个特性以及隔离机制
    4.1.1 数据库的四特性
    • 原子性:事务内包含的所有操作要么全部成功,要么全部失败回滚;
      • 实现:日志,将所有的更新操作全部写入日志当中,若因为一些系统奔溃/断电等原因导致事务中的部分更新操作已经执行,部分操作未执行,则通过回溯日志,将操作回滚,使系统保证原子性以及一致性;
    • 一致性:不管任何时间有少个并发的事务,系统也必须保持一致;
    • 隔离性:多个并发的事务的操作,在同一时间只能有一个事务执行(及串行的执行);
    • 持久性:事务正确执行后,事务中对数据的操作不会回滚;
    • 数据库的四大特性都是以一致性为核心的,分为数据库外部一致性与数据库内部一致性,其中外部一致性是靠编码来保证的(及在同一个事务内保证操作的完整性,如果有问题则数据库无法解决),内部一致性及事务中的操作要保证一致性:即要么全部执行要么全部不执行,且事务执行过后要永久保存一致性(单个事务的情况下);当等多个事务并发进行的情况下,仅仅保证原子性与持久性是不够的,例如并发事务a与并发事务b同时操作一个数据(同时为记录b加100),正常结果为b记录为+200,但是若在a事务操作此数据时,b事务也对此记录+100,且执行完毕,a事务执行完之后,b记录的值只+100,因为a事务将b事务的结果覆盖了即更新丢失,所以为了保证并发状态下的一致性引入了隔离性,即保证每一个事务看到的数据都是一致的,且多个事务并发执行后的状态与他们执行前的状态是等价的;
    4.1.2 数据库的四隔离机制
    • Read uncommitted:一个事务可以读取另一个未提交事务的数据。
    • Read committed:一个事务可以读取另一个提交事务的数据。
    • Repeatable read:重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
    • Serializable 序列化:Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
    4.1.3 数据库不做隔离的问题
    • 脏读:事务A读取了事务B未提交的数据,假如事务B未正确执行,回滚,则事务A读取到的就是脏数据;
    • 不可重复读:一个事务范围内的俩个相同的查询却得到了不同的结果(原因是查询时其他事务修改提交导致的);
    • 幻读:幻读针对于记录的个数,而不可重复读针对于记录的内容;
    4.2 数据库索引
    4.2.1 数据库索引
    • B+树:B+树索引
      • B+树是应文件系统所需而出的一种B树的变型树。一棵m阶的B+树和m阶的B-树的差异在于:
      • 1.有n棵子树的结点中含有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点。
      • 2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
      • 3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。
      • 4.通常在B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
      • 可以从最小关键字起顺序查找,也可以从根结点开始,进行随机查找。
    • 散列索引
      • Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。
      • (1)Hash 索引仅仅能满足"=",“IN"和”<=>"查询,不能使用范围查询。由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关系,并不能保证和Hash运算前完全一样。
      • (2)Hash 索引无法被用来避免数据的排序操作。由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且Hash值的大小关系并不一定和 Hash 运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算;
      • (3)Hash 索引不能利用部分索引键查询。对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,而不是单独计算 Hash 值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。
      • (4)Hash 索引在任何时候都不能避免表扫描。前面已经知道,Hash 索引是将索引键通过 Hash 运算之后,将 Hash运算结果的 Hash 值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash 索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。
      • (5)Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。 对于选择性比较低的索引键,如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个 Hash 值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。
    • 位图索引
      • 位图索引是从oracle 7.3版本开始引入的。目前oracle企业版和个人版都支持位图索引,但是标准版不支持。位图索引是这样一种结构,其中用一个索引键条目存储指向多行的指针,这与B树结构不同,在b树结构中,索引键和表中的行存在着对应关系。在位图索引中,可能只有很少的索引条目,每个索引条目指向多行。
    4.2.2 索引失效的情况
    • 被索引字段实现了隐性的类型转换
    • 被索引字段使用了函数
    • 被索引字段了表达式计算
      • 以上三种索引失效的情况可以归于一类,进行一下总结,被索引字段的隐式转换、被索引字段的表达式计算、被索引字段使用函数,都会引起索引字段对应的索引发生失效,这是因为索引的使用是依赖于B-tree索引树的遍历,而索引树的遍历是依赖于索引树底层叶子节点的有序性,当被索引字段进行了隐式类型转换、表达式计算或函数计算后,有可能这个字段新的排列顺序和原来在索引树的叶子节点层的排列顺序不一样了,这就破坏了索引树叶子节点层的有序性,当SQL语句被执行时,MySQL数据库的SQL语句执行器就无法判断原来的索引树是否还能被检索使用,所以就是SQL执行器不使用该索引了,而我们看到的就是我们期望使用的索引失效了。
    • 在like关键字后面使用’%###’
    • 被使用的索引字段,不是联合索引的最左字段
      • 总结一下第四种情况和第五种情况,like关键字后使用了左模糊匹配或者使用了左右模糊匹配时,索引不会被SQL执行器使用,SQL查询字段不是联合索引的最左字段时,联合索引也不会被SQL执行器使用。这其中的原因是,MySQL中的索引索引树检索遵循最左匹配原则,B-tree索引树的叶子节点的有序性,也是建立在最左匹配的基础上的,如果直接使用索引键的中部或者后部进行SQL查询,由于违背了最左匹配原则,MySQL的SQL执行器无法利用这个索引树进行检索,所以给我们的直观感受就是索引失效了。
    4.2.3 最左匹配原则
    • 最左匹配原则:最左最先。以最左边为起点的任何连续的索引都能匹配上
      • (1)如果第一个字段是范围查询需要单独建一个索引;
      • (2)在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边;
    4.2.4 Mysql Innodb和MyISAM存储引擎
    • InnoDB支持事务,MyISAM不支持,这一点是非常之重要。事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而MyISAM就不可以了。
    • MyISAM适合查询以及插入为主的应用,InnoDB适合频繁修改以及涉及到安全性较高的应用
    • InnoDB支持外键,MyISAM不支持
    • MyISAM是默认引擎,InnoDB需要指定
    • InnoDB不支持FULLTEXT类型的索引
    • InnoDB中不保存表的行数,如select count() from table时,InnoDB需要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含where条件时MyISAM也需要扫描整个表
    • 对于自增长的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中可以和其他字段一起建立联合索引
    • 清空整个表时,InnoDB是一行一行的删除,效率非常慢。MyISAM则会重建表
    • InnoDB支持行锁(某些情况下还是锁整表,如 update table set a=1 where user like ‘%lee%’)
    4.3 Mysql的日志
    • bin log:记录的都是写入性操作的log。以二进制的形式保存在磁盘中。binlog 是 mysql的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志。
      • binlog 是通过追加的方式进行写入的,可以通过max_binlog_size 参数设置每个 binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。
      • 使用场景:
        • 主从复制 在 Master 端开启 binlog ,然后将 binlog发送到各个 Slave 端, Slave 端重放 binlog 从而达到主从数据一致。
        • 数据恢复 :通过使用 mysqlbinlog 工具来恢复数据。
    • redo log:事务的四大特性里面有一个是 持久性 ,具体来说就是只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态 。
      • 因此 mysql 设计了 redo log , 具体来说就是只记录事务对数据页做了哪些修改,这样就能完美地解决性能问题了
    • undo log:数据库事务四大特性中有一个是 原子性 ,具体来说就是 原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。
      • 实际上, 原子性 底层就是通过 undo log 实现的。undo log主要记录了数据的逻辑变化,比如一条 INSERT 语句,对应一条DELETE 的 undo log ,对于每个 UPDATE 语句,对应一条相反的 UPDATE 的 undo log ,这样在发生错误时,就能回滚到事务之前的数据状态。
      • 同时, undo log 也是 MVCC(多版本并发控制)实现的关键。
    4.4 Mysql主从复制的原理
    • 主要分为三步:
      • (1) Master的更新事件(update、insert、delete)会按照顺序写入bin-log中。当Slave连接到Master的后,Master机器会为Slave开启 binlog dump线程,该线程会去读取bin-log日志
      • (2) Slave连接到Master后,Slave库有一个I/O线程 通过请求binlog dump thread读取bin-log日志,然后写入从库的relay log日志中。
      • (3) Slave还有一个 SQL线程,实时监控 relay-log日志内容是否有更新,解析文件中的SQL语句,在Slave数据库中去执行。
    • 主从同步延时
      • 延时的因素
        • 1)主节点如果执行一个很大的事务,那么就会对主从延迟产生较大的影响
        • 2)网络延迟,日志较大,slave数量过多
        • 3)主上多线程写入,从节点只有单线程同步
        • 4)机器性能问题,从节点是否使用了“烂机器”
        • 5)锁冲突问题也可能导致从机的SQL线程执行慢
    4.5 MVCC的底层原理实现
    • MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
    • mvcc的实现原理主要依赖于记录中的三个隐藏字段,undolog,read view来实现的。
    • 隐藏字段:每行记录除了我们自定义的字段外,还有数据库隐式定义的DBTRXID,DBROLLPTR,DBROWID等字段
    • undolog被称之为回滚日志,表示在进行insert,delete,update操作的时候产生的方便回滚的日志
      • 当进行insert操作的时候,产生的undolog只在事务回滚的时候需要,并且在事务提交之后可以被立刻丢弃
      • 当进行update和delete操作的时候,产生的undolog不仅仅在事务回滚的时候需要,在快照读的时候也需要,所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除(当数据发生更新和删除操作的时候都只是设置一下老记录的deletedbit,并不是真正的将过时的记录删除,因为为了节省磁盘空间,innodb有专门的purge线程来清除deletedbit为true的记录,如果某个记录的deletedid为true,并且DBTRX_ID相对于purge线程的read view 可见,那么这条记录一定时可以被清除的)
    • Read View是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。
      • 其实Read View的最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据
      • Read View遵循的可见性算法主要是将要被修改的数据的最新记录中的DBTRXID(当前事务id)取出来,与系统当前其他活跃事务的id去对比,如果DBTRXID跟Read View的属性做了比较,不符合可见性,那么就通过DBROLLPTR回滚指针去取出undolog中的DBTRXID做比较,即遍历链表中的DBTRXID,直到找到满足条件的DBTRXID,这个DBTRXID所在的旧记录就是当前事务能看到的最新老版本数据。
    4.6 SQL语句的查询流程

    在这里插入图片描述

    4.7 Redis
    4.7.1 介绍
    • Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。
    • Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
    • 与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
    4.7.2 Redis的优缺点
    • 优点
      • 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
      • 支持数据持久化,支持AOF和RDB两种持久化方式。
      • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
      • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
      • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
    • 缺点
      • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
      • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
      • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
      • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
    4.7.3 Redis为什么快
    • 1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
    • 2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
    • 3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
    • 4、使用多路 I/O 复用模型,非阻塞 IO;
    • 5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
    4.7.4 Redis的数据结构
    • Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分的使用要求

    在这里插入图片描述

    4.7.5 雪崩、击穿、穿透
    • 雪崩
      • 定义:redis缓存中大量的key同时失效,此时又刚好有大量的请求打进来,直接打到数据库层,造成数据库阻塞甚至宕机。
      • 解决办法:缓存雪崩的事前事中事后的解决方案如下:
        • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
        • 原有的失效时间基础上增加一个随机值,比如1-5分钟随机
        • 加锁,当出现缓存失效的情况通过锁控制一定时间只有一个线程获取到资源从数据库拿到数据后写回缓存,后面基于缓存做查询
        • 缓存预热
        • 热点key设置永不过期。
        • 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
        • 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
    • 穿透
      • 定义:指查询一个缓存和数据库都不存在的数据,导致尽管数据不存在但是每次都会到数据库查询。在访问量大时可能DB就会挂掉。如果有人利用不存在的key频繁攻击,则这就形成了漏洞。
      • 解决办法:
        • 如果一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
        • 接口层增加校验,用户鉴权,id做基础校验。
        • 采用布隆过滤器,将所有可能存在的数据hash到一个足够大的bitmap中。(会有一定的出错率)
    • 击穿
      • 击穿与雪崩的不同在于缓存key失效的量级上。击穿是对于单个key值的缓存失效过期,雪崩则是大面积key同时失效。
      • 解决办法:
        • 若缓存数据基本不会发生更新,则可尝试将热点数据设置为永不过期。
        • 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
        • 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
  • 相关阅读:
    【车载开发系列】UDS诊断服务入门知识
    张驰咨询:家电企业用六西格玛项目减少客户非合理退货案例
    Apache Ant
    神经网络NLP基础 循环神经网络 LSTM
    Spark使用scala语言连接hive数据库
    深入理解MySQL分区技术
    Python- JSON使用初探
    从一次数据库误操作开始了解MySQL日志【bin log、redo log、undo log】
    不死马的利用与克制(基于条件竞争)及变种不死马
    点集合的三角剖分
  • 原文地址:https://blog.csdn.net/star_lord123/article/details/126928555