数据库面试问题汇总(超详细)

1、数据库范式

  • 第一范式:数据库表的每一列都是不可分割的原子数据项,而不能是集合,数组,记录等非原子数据项。eg:【联系人】(姓名,性别,电话),一个联系人有家庭电话和公司电话,那么这种表结构设计就没有达到 1NF;

  • 第二范式:有主键,保证完全依赖。通俗解释:任意一个字段都只依赖表中的同一个字段 。eg:订单明细表【OrderDetail】(OrderID,ProductID,UnitPrice,Discount,Quantity,ProductName),Discount(折扣),Quantity(数量)完全依赖(取决)于主键(OderID,ProductID),而 UnitPrice,ProductName 只依赖于 ProductID,不符合2NF;

  • 第三范式:无传递依赖(传递依赖:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况),通俗理解:一张表最多只存2层同类型信息 。eg:订单表【Order】(OrderID,OrderDate,CustomerID,CustomerName,CustomerAddr,CustomerCity)主键是(OrderID),CustomerName,CustomerAddr,CustomerCity 直接依赖的是 CustomerID(非主键列),而不是直接依赖于主键,它是通过传递才依赖于主键,所以不符合 3NF。
    user用户表,字段id,username,password
    role角色表,字段id,name
    user_role用户-角色中间表,id,user_id,role_id
    像这样,通过第三张表(中间表)来建立用户表和角色表之间的关系,同时又符合范式化的原则,就可以称为第三范式。

扩展: 反范式指的是通过增加冗余或重复的数据来提高数据库的读性能。 例如:在上例中的user_role用户-角色中间表增加字段role_name。 反范式化可以减少关联查询时,join表的次数。

2、数据库索引

  索引是对数据库表中一个或多个列的值进行排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B_TREE及其变种。索引加速了数据访问,因为存储引擎不会再去扫描整张表得到需要的数据;相反,它从根节点开始,根节点保存了子节点的指针,存储引擎会根据指针快速寻找数据。

           索引.png-36.9kB

  上图显示了一种索引方式。左边是数据库中的数据表,有col1和col2两个字段,一共有15条记录;右边是以col2列为索引列的B_TREE索引,每个节点包含索引的键值和对应数据表地址的指针,这样就可以都过B_TREE在 O(logn) 的时间复杂度内获取相应的数据,这样明显地加快了检索的速度。

1). 索引的底层实现原理和优化

MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTree索引,B+Tree索引,哈希索引,全文索引等等。

(1).哈希索引:

  只有memory(内存)存储引擎支持哈希索引,哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能。


(2).全文索引:

  FULLTEXT(全文)索引,仅可用于MyISAM和InnoDB,针对较大的数据,生成全文索引非常的消耗时间和空间。对于文本的大对象,或者较大的CHAR类型的数据,如果使用普通索引,那么匹配文本前几个字符还是可行的,但是想要匹配文本中间的几个单词,那么就要使用LIKE %word%来匹配,这样需要很长的时间来处理,响应时间会大大增加,这种情况,就可使用时FULLTEXT索引了,在生成FULLTEXT索引时,会为文本生成一份单词的清单,在索引时及根据这个单词的清单来索引。


(3). B-Tree(平衡多路查找树)

  B_TREE是一种平衡多路查找树,是一种动态查找效率很高的树形结构。B_TREE中所有结点的孩子结点的最大值称为B_TREE的阶,B_TREE的阶通常用m表示,简称为m叉树。一般来说,应该是m>=3。一颗m阶的B_TREE或是一颗空树,或者是满足下列条件的m叉树:

  • 设树的度为2d(d>1),高度为h,那么BTree要满足以一下条件:
  • 每个叶子结点的高度一样,等于h;
  • 每个非叶子结点由n-1个key和n个指针point组成,其中d<=n<=2d,key和point相互间隔,结点两端一定是key;
  • 叶子结点指针都为null;
  • 非叶子结点的key都是[key,data]二元组,其中key表示作为索引的键,data为键值所在行的数据;

  • BTree的结构如下:

    在这里插入图片描述

  • 所有的叶结点都在同一层上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

  • 下图是一棵4阶B_TREE,4叉树结点的孩子结点的个数范围[2,4]。其中,有2个结点有4个孩子结点,有1个结点有3个孩子结点,有5个结点有2个孩子结点。

              4阶B_TREE.jpg-24.1kB

      B_TREE的查找类似二叉排序树的查找,所不同的是B-树每个结点上是多关键码的有序表,在到达某个结点时,先在有序表中查找,若找到,则查找成功;否则,到按照对应的指针信息指向的子树中去查找,当到达叶子结点时,则说明树中没有对应的关键码。由于B_TREE的高检索效率,B-树主要应用在文件系统和数据库中,对于存储在硬盘上的大型数据库文件,可以极大程度减少访问硬盘次数,大幅度提高数据检索效率。


    (4). B+Tree : InnoDB存储引擎的索引实现

      B+Tree是应文件系统所需而产生的一种B_TREE树的变形树。设d为树的度数,h为树的高度,B+Tree和BTree的不同主要在于:

    • B+Tree中的非叶子结点不存储数据,只存储键值;
    • B+Tree的叶子结点没有指针,所有键值都会出现在叶子结点上,且key存储的键值对应data数据的物理地址;
    • B+Tree的每个非叶子节点由n个键值key和n个指针point组成;

    • B+树的结构:

      在这里插入图片描述

      下图为一棵3阶的B+树。通常在B+树上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点。因此可以对B+树进行两种查找运算:一种是从最小关键字起顺序查找,另一种是从根节点开始,进行随机查找。
      在B+树上进行随机查找、插入和删除的过程基本上与B-树类似。只是在查找时,若非终端结点上的关键码等于给定值,并不终止,而是继续向下直到叶子结点。因此,对于B+树,不管查找成功与否,每次查找都是走了一条从根到叶子结点的路径。

                一棵3阶的B+树.jpg-29.9kB


      (5). 为什么说B+树比B 树更适合实际应用中操作系统的文件索引和数据库索引?

      • B+tree的磁盘读写代价更低:B+tree的内部结点并没有指向关键字具体信息的指针(红色部分),因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多,相对来说IO读写次数也就降低了;

      • B+tree的查询效率更加稳定:由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引,所以,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当;

      • 数据库索引采用B+树而不是B树的主要原因:B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。


      (6). 文件索引和数据库索引为什么使用B+树?

        文件与数据库都是需要较大的存储,也就是说,它们都不可能全部存储在内存中,故需要存储到磁盘上。而所谓索引,则为了数据的快速定位与查找,那么索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数,因此B+树相比B树更为合适。数据库系统巧妙利用了局部性原理与磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入,而红黑树这种结构,高度明显要深的多,并且由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性。最重要的是,B+树还有一个最大的好处:方便扫库。B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持range-query非常方便,而B树不支持,这是数据库选用B+树的最主要原因。


      2). 索引的优点

      • 大大加快数据的检索速度,这也是创建索引的最主要的原因;

      • 加速表和表之间的连接;

      • 根据索引分组和排序,可以加快分组和排序;

      • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性;


      3). 什么情况下设置了索引但无法使用?

      • 在组合索引中不能有列的值为NULL,如果有,那么这一列对组合索引就是无效的。
      • 在一个SELECT语句中,索引只能使用一次,如果在WHERE中使用了,那么在ORDER BY中就不要用了。
      • LIKE操作中,'%aaa%'不会使用索引,也就是索引会失效,但是‘aaa%’可以使用索引。
      • 在索引的列上使用表达式或者函数会使索引失效,例如:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<’2007-01-01′。其它通配符同样,也就是说,在查询条件中使用正则表达式时,只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。
      • 在查询条件中使用不等于,包括<符号、>符号和!=会导致索引失效。特别的是如果对主键索引使用!=则不会使索引失效,如果对主键索引或者整数类型的索引使用<符号或者>符号不会使索引失效。
      • 在查询条件中使用IS NULL或者IS NOT NULL会导致索引失效。
      • 字符串不加单引号会导致索引失效。更准确的说是类型不一致会导致失效,比如字段email是字符串类型的,使用WHERE email=99999 则会导致失败,应该改为WHERE email='99999'。
      • 在查询条件中使用OR连接多个条件会导致索引失效,除非OR链接的每个条件都加上索引,这时应该改为两次查询,然后用UNION ALL连接起来。
      • 如果排序的字段使用了索引,那么select的字段也要是索引字段,否则索引失效。特别的是如果排序的是主键索引则select * 也不会导致索引失效。
      • 尽量不要包括多列排序,如果一定要,最好为这队列构建组合索引;

      4). 什么样的字段适合创建索引?

      • 经常作查询选择的字段

      • 经常作表连接的字段

      • 经常出现在order by, group by, distinct 后面的字段


      5). 创建索引时需要注意什么?

      • 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;

      • 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;

      • 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。


      6). 索引的缺点

      • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度;

      • 空间方面:索引需要占物理空间。


      7). 索引的分类

      常见的索引类型有:主键索引、唯一索引、普通索引、全文索引、组合索引

      ①、主键索引:即主索引,根据主键pk_clolum(length)建立索引,不允许重复,不允许空值;

      ALTER TABLE ‘table_name’ ADD PRIMARY KEY pk_index(‘col’);
      ②、唯一索引:用来建立索引的列的值必须是唯一的,允许空值

      ALTER TABLE ‘table_name’ ADD UNIQUE index_name(‘col’);
      ③、普通索引:用表中的普通列构建的索引,没有任何限制

      ALTER TABLE ‘table_name’ ADD INDEX index_name(‘col’);
      ④、全文索引:用大文本对象的列构建的索引(下一部分会讲解)

      ALTER TABLE ‘table_name’ ADD FULLTEXT INDEX ft_index(‘col’);
      ⑤、组合索引:用多个列组合构建的索引,这多个列中的值不允许有空值

      ALTER TABLE ‘table_name’ ADD INDEX index_name(‘col1’,‘col2’,‘col3’);


      遵循“最左前缀”原则,把最常用作为检索或排序的列放在最左,依次递减,组合索引相当于建立了col1,col1col2,col1col2col3三个索引,而col2或者col3是不能使用索引的。

      在使用组合索引的时候可能因为列名长度过长而导致索引的key太大,导致效率降低,在允许的情况下,可以只取col1和col2的前几个字符作为索引。

      ALTER TABLE ‘table_name’ ADD INDEX index_name(col1(4),col2(3));
      表示使用col1的前4个字符和col2的前3个字符作为索引

      8). 主键、自增主键、主键索引与唯一索引概念区别

      主键:指字段 唯一不为空值 的列;

      主键索引:指的就是主键,主键是索引的一种,是唯一索引的特殊类型。创建主键的时候,数据库默认会为主键创建一个唯一索引;

      自增主键:字段类型为数字、自增、并且是主键;

      唯一索引:索引列的值必须唯一,但允许有空值。主键是唯一索引,这样说没错;但反过来说,唯一索引也是主键就错误了,因为唯一索引允许空值,主键不允许有空值,所以不能说唯一索引也是主键。


      9). 主键就是聚集索引吗?主键和索引有什么区别?

        主键是一种特殊的唯一性索引,其可以是聚集索引,也可以是非聚集索引。在SQLServer中,主键的创建必须依赖于索引,默认创建的是聚集索引,但也可以显式指定为非聚集索引。InnoDB作为MySQL存储引擎时,默认按照主键进行聚集,如果没有定义主键,InnoDB会试着使用唯一的非空索引来代替。如果没有这种索引,InnoDB就会定义隐藏的主键然后在上面进行聚集。所以,对于聚集索引来说,你创建主键的时候,自动就创建了主键的聚集索引。


      10).MyISAM——非聚簇索引
    • MyISAM存储引擎采用的是非聚簇索引,非聚簇索引的主索引和辅助索引几乎是一样的,只是主索引不允许重复,不允许空值,他们的叶子结点的key都存储指向键值对应的数据的物理地址。
    • 非聚簇索引的数据表和索引表是分开存储的。
    • 非聚簇索引中的数据是根据数据的插入顺序保存。因此非聚簇索引更适合单个数据的查询。插入顺序不受键值影响。
    • 只有在MyISAM中才能使用FULLTEXT索引。(mysql5.6以后innoDB也支持全文索引)

    • *最开始我一直不懂既然非聚簇索引的主索引和辅助索引指向相同的内容,为什么还要辅助索引这个东西呢,后来才明白索引不就是用来查询的吗,用在那些地方呢,不就是WHERE和ORDER BY 语句后面吗,那么如果查询的条件不是主键怎么办呢,这个时候就需要辅助索引了。

      11).InnoDB——聚簇索引

    • 聚簇索引的主索引的叶子结点存储的是键值对应的数据本身,辅助索引的叶子结点存储的是键值对应的数据的主键键值。因此主键的值长度越小越好,类型越简单越好。
    • 聚簇索引的数据和主键索引存储在一起。
    • 聚簇索引的数据是根据主键的顺序保存。因此适合按主键索引的区间查找,可以有更少的磁盘I/O,加快查询速度。但是也是因为这个原因,聚簇索引的插入顺序最好按照主键单调的顺序插入,否则会频繁的引起页分裂,严重影响性能。
    • 在InnoDB中,如果只需要查找索引的列,就尽量不要加入其它的列,这样会提高查询效率。


    • *使用主索引的时候,更适合使用聚簇索引,因为聚簇索引只需要查找一次,而非聚簇索引在查到数据的地址后,还要进行一次I/O查找数据。

      *因为聚簇辅助索引存储的是主键的键值,因此可以在数据行移动或者页分裂的时候降低成本,因为这时不用维护辅助索引。但是由于主索引存储的是数据本身,因此聚簇索引会占用更多的空间。

      *聚簇索引在插入新数据的时候比非聚簇索引慢很多,因为插入新数据时需要检测主键是否重复,这需要遍历主索引的所有叶节点,而非聚簇索引的叶节点保存的是数据地址,占用空间少,因此分布集中,查询的时候I/O更少,但聚簇索引的主索引中存储的是数据本身,数据占用空间大,分布范围更大,可能占用好多的扇区,因此需要更多次I/O才能遍历完毕。

      下图可以形象的说明聚簇索引和非聚簇索引的区别

      在这里插入图片描述


      3、数据库事务

        事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。

      (1). 事务的特征ACID

      • 原子性(Atomicity):事务所包含的一系列数据库操作要么全部成功执行,要么全部回滚;

      • 一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态;

      • 隔离性(Isolation):并发执行的事务之间不能相互影响;

      • 持久性(Durability):事务一旦提交,对数据库中数据的改变是永久性的。

      (2). 事务并发带来的问题

      • 脏读:一个事务读取了另一个事务未提交的数据;

      • 不可重复读:不可重复读的重点是修改,同样条件下两次读取结果不同,也就是说,被读取的数据可以被其它事务修改;

      • 幻读:幻读的重点在于新增或者删除,同样条件下两次读出来的记录数不一样。

      (3). 隔离级别

        隔离级别决定了一个session中的事务可能对另一个session中的事务的影响。ANSI标准定义了4个隔离级别,MySQL的InnoDB都支持,分别是:

      • 读未提交(READ UNCOMMITTED):最低级别的隔离,通常又称为dirty read,它允许一个事务读取另一个事务还没commit的数据,这样可能会提高性能,但是会导致脏读问题;

      • 读已提交(READ COMMITTED):在一个事务中只允许对其它事务已经commit的记录可见,该隔离级别不能避免不可重复读问题;

      • 可重复读(REPEATABLE READ):在一个事务开始后,其他事务对数据库的修改在本事务中不可见,直到本事务commit或rollback。但是,其他事务的insert/delete操作对该事务是可见的,也就是说,该隔离级别并不能避免幻读问题。在一个事务中重复select的结果一样,除非本事务中update数据库。

      • 串行化(SERIALIZABLE):最高级别的隔离,只允许事务串行执行。

        MySQL默认的隔离级别是可重复读(REPEATABLE READ)

      (4)、MySQL的事务支持

        MySQL的事务支持不是绑定在MySQL服务器本身,而是与存储引擎相关:

      • MyISAM:不支持事务,用于只读程序提高性能;
      • InnoDB:支持ACID事务、行级锁、并发;
      • Berkeley DB:支持事务。

      4、实践中如何优化MySQL

        实践中,MySQL的优化主要涉及SQL语句及索引的优化、数据表结构的优化、系统配置的优化和硬件的优化四个方面,如下图所示:

                    Mysql性能优化-82.5kB

      1、SQL语句及索引的优化

      ①SQL语句的优化

        SQL语句的优化主要包括三个问题,即如何发现有问题的SQL、如何分析SQL的执行计划以及如何优化SQL,下面将逐一解释。


      a. 怎么发现有问题的SQL?(通过MySQL慢查询日志对有效率问题的SQL进行监控)

        MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10s以上的语句。慢查询日志的相关参数如下所示:

              慢查询日志相关参数.png-20.4kB

        通过MySQL的慢查询日志,我们可以查询出执行的次数多占用的时间长的SQL、可以通过pt_query_disgest(一种mysql慢日志分析工具)分析Rows examine(MySQL执行器需要检查的行数)项去找出IO大的SQL以及发现未命中索引的SQL,对于这些SQL,都是我们优化的对象。


      b. 通过explain查询和分析SQL的执行计划

        使用 EXPLAIN 关键字可以知道MySQL是如何处理你的SQL语句的,以便分析查询语句或是表结构的性能瓶颈。通过explain命令可以得到表的读取顺序、数据读取操作的操作类型、哪些索引可以使用、哪些索引被实际使用、表之间的引用以及每张表有多少行被优化器查询等问题。当扩展列extra出现Using filesort和Using temporay,则往往表示SQL需要优化了。


      c. SQL语句的优化

      • 优化insert语句:一次插入多值;

      • 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描;

      • 应尽量避免在 where 子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描;

      • 优化嵌套查询:子查询可以被更有效率的连接(Join)替代;

      • 很多时候用 exists 代替 in 是一个好的选择。


      ②.索引优化

        建议在经常作查询选择的字段、经常作表连接的字段以及经常出现在order by、group by、distinct 后面的字段中建立索引。但必须注意以下几种可能会引起索引失效的情形:

      • 以“%(表示任意0个或多个字符)”开头的LIKE语句,模糊匹配;

      • OR语句前后没有同时使用索引;

      • 数据类型出现隐式转化(如varchar不加单引号的话可能会自动转换为int型);

      • 对于多列索引,必须满足最左匹配原则(eg,多列索引col1、col2和col3,则 索引生效的情形包括col1或col1,col2或col1,col2,col3)。

      2. 数据库表结构的优化

        数据库表结构的优化包括选择合适数据类型、表的范式的优化、表的垂直拆分和表的水平拆分等手段。

      (1). 选择合适数据类型

      • 使用较小的数据类型解决问题;

      • 使用简单的数据类型(mysql处理int要比varchar容易);

      • 尽可能的使用not null 定义字段;

      • 尽量避免使用text类型,非用不可时最好考虑分表;

      (2). 表的范式的优化

        一般情况下,表的设计应该遵循三大范式。

      (3). 表的垂直拆分

        把含有多个列的表拆分成多个表,解决表宽度问题,具体包括以下几种拆分手段:

      • 把不常用的字段单独放在同一个表中;

      • 把大字段独立放入一个表中;

      • 把经常使用的字段放在一起;
          
        这样做的好处是非常明显的,具体包括:拆分后业务清晰,拆分规则明确、系统之间整合或扩展容易、数据维护简单。

      (4). 表的水平拆分

        表的水平拆分用于解决数据表中数据过大的问题,水平拆分每一个表的结构都是完全一致的。一般地,将数据平分到N张表中的常用方法包括以下两种:

      • 对ID进行hash运算,如果要拆分成5个表,mod(id,5)取出0~4个值;
      • 针对不同的hashID将数据存入不同的表中;

        表的水平拆分会带来一些问题和挑战,包括跨分区表的数据查询、统计及后台报表的操作等问题,但也带来了一些切实的好处:

      • 表分割后可以降低在查询时需要读的数据和索引的页数,同时也降低了索引的层数,提高查询速度;

      • 表中的数据本来就有独立性,例如表中分别记录各个地区的数据或不同时期的数据,特别是有些数据常用,而另外一些数据不常用。

      • 需要把数据存放到多个数据库中,提高系统的总体可用性(分库,鸡蛋不能放在同一个篮子里)。

      3. 系统配置的优化

      • 操作系统配置的优化:增加TCP支持的队列数

      • mysql配置文件优化:Innodb缓存池设置(innodb_buffer_pool_size,推荐总内存的75%)和缓存池的个数(innodb_buffer_pool_instances)

      4. 硬件的优化

      • CPU:核心数多并且主频高的
      • 内存:增大内存
      • 磁盘配置和选择:磁盘性能

      5、NOSQL数据库 —— Redis

        Redis是一款基于内存的且支持持久化、高性能的Key-Value NoSQL 数据库,其支持丰富数据类型(string,list,set,sorted set,hash),常被用作缓存的解决方案。Redis具有以下显著特点:

      • 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

      • 支持丰富数据类型,支持string,list,set,sorted set,hash;

      • 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行;

      • 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除。

        Redis作查询缓存需要注意考虑以下几个问题,包括防止脏读、序列化查询结果、为查询结果生成一个标识和怎么使用四个问题,具体如下:

      (1). 防止脏读

        对一张表的查询结果放在一个哈希结构里,当对这个表进行修改、删除或者更新时,删除该哈希结构。对这张表所有的操作方法,使用注解进行标记,例如:

      Hash   KEY(表名)  k(查询结果标识) : v()   k:v   k:v   k:v   k:v
      
      // 表示该方法需要执行 (缓存是否命中 ? 返回缓存并阻止方法调用 : 执行方法并缓存结果)的缓存逻辑
      @RedisCache(type = JobPostModel.class)
      JobPostModel selectByPrimaryKey(Integer id);
      
      // 表示该方法需要执行清除缓存逻辑
      @RedisEvict(type = JobPostModel.class)
      int deleteByPrimaryKey(Integer id);
      

        我们缓存了查询结果,那么一旦数据库中的数据发生变化,缓存的结果就不可用了。为了实现这一保证,可以在执行相关表的更新查询(update,delete,insert)查询前,让相关的缓存过期。这样下一次查询时程序就会重新从数据库中读取新数据缓存到redis中。那么问题来了,在执行一条insert前我怎么知道应该让哪些缓存过期呢?对于Redis,我们可以使用Hash结构,让一张表对应一个Hash,所有在这张表上的查询都保存到该Hash下。这样当表数据发生变动时,直接让Set过期即可。我们可以自定义一个注解,在数据库查询方法上通过注解的属性注明这个操作与哪些表相关,这样在执行过期操作时,就能直接从注解中得知应该让哪些Set过期了。

      (2). 序列化查询结果

        利用JDK自带的ObjectInputStream/ObjectOutputStream将查询结果序列化成字节序列,即需要考虑Redis的实际存储问题。

      (3). 为查询结果生成一个标识

        被调用的方法所在的类名,被调用的方法的方法名,该方法的参数三者共同标识一条查询结果。也就是说,如果两次查询调用的类名、方法名和参数值相同,我们就可以确定这两次查询结果一定是相同的(在数据没有变动的前提下)。因此,我们可以将这三个元素组合成一个字符串做为key,就解决了标识问题。

      (4). 以 AOP 方式使用Redis

      • 方法被调用之前,根据类名、方法名和参数值生成Key;

      • 通过Key向Redis发起查询;

      • 如果缓存命中,则将缓存结果反序列化作为方法调用的返回值 ,并将其直接返回;

      • 如果缓存未命中,则继续向数据库中查询,并将查询结果序列化存入redis中,同时将查询结果返回。

        例如,插入删除缓存逻辑如下:

      /**
           * 在方法调用前清除缓存,然后调用业务方法
           * @param jp
           * @return
           * @throws Throwable
           */
          @Around("execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.insert*(..))" +
                  "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.update*(..))" +
                  "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.delete*(..))" +
                  "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.increase*(..))" +
                  "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.decrease*(..))" +
                  "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.complaint(..))" +
                  "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.set*(..))")
          public Object evictCache(ProceedingJoinPoint jp) throws Throwable {}
      

      6、什么是存储过程?有哪些优缺点?

        存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合。进一步地说,存储过程是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。存储过程具有以下特点:

      • 存储过程只在创建时进行编译,以后每次执行存储过程都不需再重新编译,而一般 SQL 语句每执行一次就编译一次,所以使用存储过程可提高数据库执行效率;

      • 当SQL语句有变动时,可以只修改数据库中的存储过程而不必修改代码;

      • 减少网络传输,在客户端调用一个存储过程当然比执行一串SQL传输的数据量要小;

      • 通过存储过程能够使没有权限的用户在控制之下间接地存取数据库,从而确保数据的安全。


      7、简单说一说drop、delete与truncate的区别

        SQL中的drop、delete、truncate都表示删除,但是三者有一些差别:

      • Delete用来删除表的全部或者一部分数据行,执行delete之后,用户需要提交(commmit)或者回滚(rollback)来执行删除或者撤销删除, delete命令会触发这个表上所有的delete触发器;

      • Truncate删除表中的所有数据,这个操作不能回滚,也不会触发这个表上的触发器,TRUNCATE比delete更快,占用的空间更小;

      • Drop命令从数据库中删除表,所有的数据行,索引和权限也会被删除,所有的DML触发器也不会被触发,这个命令也不能回滚。

      因此,在不再需要一张表的时候,用drop;在想删除部分数据行时候,用delete;在保留表而删除所有数据的时候用truncate。


      8、 什么叫视图?游标是什么?

        视图是一种虚拟的表,通常是有一个表或者多个表的行或列的子集,具有和物理表相同的功能,可以对视图进行增,删,改,查等操作。特别地,对视图的修改不影响基本表。相比多表查询,它使得我们获取数据更容易。

        游标是对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。

        在操作mysql的时候,我们知道MySQL检索操作返回一组称为结果集的行。这组返回的行都是与 SQL语句相匹配的行(零行或多行)。使用简单的 SELECT语句,例如,没有办法得到第一行、下一行或前 10行,也不存在每次一行地处理所有行的简单方法(相对于成批地处理它们)。有时,需要在检索出来的行中前进或后退一行或多行。这就是使用游标的原因。游标(cursor)是一个存储在MySQL服务器上的数据库查询,它不是一条 SELECT语句,而是被该语句检索出来的结果集。在存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。


      9、什么是触发器?

        触发器是与表相关的数据库对象,在满足定义条件时触发,并执行触发器中定义的语句集合。触发器的这种特性可以协助应用在数据库端确保数据库的完整性。


      10、MySQL中的悲观锁与乐观锁的实现

        悲观锁与乐观锁是两种常见的资源并发锁设计思路,也是并发编程中一个非常基础的概念。

      (1). 悲观锁

        悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为所有的操作均会导致并发安全问题,因此要先确保获取锁成功再进行业务操作。通常来讲,在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现悲观锁。当数据库执行select … for update时会获取被select中的数据行的行锁,因此其他并发执行的select … for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

        这里需要特别注意的是,不同的数据库对select… for update的实现和支持都是有所区别的,例如oracle支持select for update no wait,表示如果拿不到锁立刻报错,而不是等待,mysql就没有no wait这个选项。另外,mysql还有个问题是: select… for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此,如果在mysql中用悲观锁务必要确定使用了索引,而不是全表扫描。

      (2). 乐观锁

        乐观锁的特点先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过,若未被更新过,则更新成功;否则,失败重试。乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号或者时间戳,然后按照如下方式实现:

      1. SELECT data AS old_data, version AS old_version FROM …;
      2. 根据获取的数据进行业务操作,得到new_data和new_version
      3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
      if (updated row > 0) {
          // 乐观锁获取成功,操作完成
      } else {
          // 乐观锁获取失败,回滚并重试
      }
      

        乐观锁是否在事务中其实都是无所谓的,其底层机制是这样:在数据库内部update同一行的时候是不允许并发的,即数据库每次执行一条update语句时会获取被update行的写锁,直到这一行被成功更新后才释放。因此在业务操作进行前获取需要锁的数据的当前版本号,然后实际更新数据时再次对比版本号确认与之前获取的相同,并更新版本号,即可确认这其间没有发生并发的修改。如果更新失败,即可认为老版本的数据已经被并发修改掉而不存在了,此时认为获取锁失败,需要回滚整个业务操作并可根据需要重试整个过程。

      (3). 悲观锁与乐观锁的应用场景

        一般情况下,读多写少更适合用乐观锁,读少写多更适合用悲观锁。乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能。


      11、JDBC 对事务的支持

        对于JDBC而言,每条单独的语句都是一个事务,即每个语句后都隐含一个commit。实际上,Connection 提供了一个auto-commit的属性来指定事务何时结束。当auto-commit为true时,当每个独立SQL操作的执行完毕,事务立即自动提交,也就是说,每个SQL操作都是一个事务;当auto-commit为false时,每个事务都必须显式调用commit方法进行提交,或者显式调用rollback方法进行回滚。auto-commit默认为true。

      try {  
          conn.setAutoCommit(false);  //将自动提交设置为false        
          ps.executeUpdate("修改SQL"); //执行修改操作  
          ps.executeQuery("查询SQL");  //执行查询操作                 
          conn.commit();      //当两个操作成功后手动提交     
      } catch (Exception e) {  
          conn.rollback();    //一旦其中一个操作出错都将回滚,使两个操作都不成功  
          e.printStackTrace();  
      } 
      

        为了能够将多条SQL当成一个事务执行,必须首先通过Connection关闭auto-commit模式,然后通过Connection的setTransactionIsolation()方法设置事务的隔离级别,最后分别通过Connection的commit()方法和rollback()方法来提交事务和回滚事务。


      12、MySQL存储引擎中的MyISAM和InnoDB区别详解

        在MySQL 5.5之前,MyISAM是mysql的默认数据库引擎,其由早期的ISAM(Indexed Sequential Access Method:有索引的顺序访问方法)所改良。虽然MyISAM性能极佳,但却有一个显著的缺点: 不支持事务处理。不过,MySQL也导入了另一种数据库引擎InnoDB,以强化参考完整性与并发违规处理机制,后来就逐渐取代MyISAM。

        InnoDB是MySQL的数据库引擎之一,其由Innobase oy公司所开发,2006年五月由甲骨文公司并购。与传统的ISAM、MyISAM相比,InnoDB的最大特色就是支持ACID兼容的事务功能,类似于PostgreSQL。目前InnoDB采用双轨制授权,一是GPL授权,另一是专有软件授权。具体地,MyISAM与InnoDB作为MySQL的两大存储引擎的差异主要包括:

      • 存储结构:每个MyISAM在磁盘上存储成三个文件:第一个文件的名字以表的名字开始,扩展名指出文件类型。.frm文件存储表定义,数据文件的扩展名为.MYD (MYData),索引文件的扩展名是.MYI (MYIndex)。InnoDB所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。

      • 存储空间:MyISAM可被压缩,占据的存储空间较小,支持静态表、动态表、压缩表三种不同的存储格式。InnoDB需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。

      • 可移植性、备份及恢复:MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便,同时在备份和恢复时也可单独针对某个表进行操作。InnoDB免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了。

      • 事务支持:MyISAM强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型更快,但是不提供事务支持。InnoDB提供事务、外键等高级数据库功能,具有事务提交、回滚和崩溃修复能力。

      • AUTO_INCREMENT:在MyISAM中,可以和其他字段一起建立联合索引。引擎的自动增长列必须是索引,如果是组合索引,自动增长可以不是第一列,它可以根据前面几列进行排序后递增。InnoDB中必须包含只有该字段的索引,并且引擎的自动增长列必须是索引,如果是组合索引也必须是组合索引的第一列。

      • 表锁差异:MyISAM只支持表级锁,用户在操作MyISAM表时,select、update、delete和insert语句都会给表自动加锁,如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。InnoDB支持事务和行级锁。行锁大幅度提高了多用户并发操作的新能,但是InnoDB的行锁,只是在WHERE的主键是有效的,非主键的WHERE都会锁全表的。

      • 全文索引:MyISAM支持 FULLTEXT类型的全文索引;InnoDB不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。

      • 表主键:MyISAM允许没有任何索引和主键的表存在,索引都是保存行的地址。对于InnoDB,如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见),数据是主索引的一部分,附加索引保存的是主索引的值。

      • 表的具体行数:MyISAM保存表的总行数,select count() from table;会直接取出出该值;而InnoDB没有保存表的总行数,如果使用select count() from table;就会遍历整个表,消耗相当大,但是在加了wehre条件后,myisam和innodb处理的方式都一样。

      • CURD操作:在MyISAM中,如果执行大量的SELECT,MyISAM是更好的选择。对于InnoDB,如果你的数据执行大量的INSERT或UPDATE,出于性能方面的考虑,应该使用InnoDB表。DELETE从性能上InnoDB更优,但DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除,在innodb上如果要清空保存有大量数据的表,最好使用truncate table这个命令。

      • 外键:MyISAM不支持外键,而InnoDB支持外键。

        通过上述的分析,基本上可以考虑使用InnoDB来替代MyISAM引擎了,原因是InnoDB自身很多良好的特点,比如事务支持、存储过程、视图、行级锁、外键等等。尤其在并发很多的情况下,相信InnoDB的表现肯定要比MyISAM强很多。另外,必须需要注意的是,任何一种表都不是万能的,合适的才是最好的,才能最大的发挥MySQL的性能优势。如果是不复杂的、非关键的Web应用,还是可以继续考虑MyISAM的,这个具体情况具体考虑。


      如果你觉得还行,可以看看我其他面试问题汇总:


      操作系统面试问题汇总,你想知道的都在这

      计算机网络面试问题汇总,你想知道的都在这

      数据库面试问题汇总,你想知道的都这

      Linux常见面试问题指令汇总,你想知道的都在这

      Spring面试问题汇总,你想知道的都在这

      SpringMVC面试问题汇总,你想知道的都在这

      Mybatis面试问题汇总,你想知道的都在这

    • 37
      点赞
    • 356
      收藏
      觉得还不错? 一键收藏
    • 2
      评论

    “相关推荐”对你有帮助么?

    • 非常没帮助
    • 没帮助
    • 一般
    • 有帮助
    • 非常有帮助
    提交
    评论 2
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值