Java面试题总结 | Java面试题总结6-MYSQL模块(持续更新)

Mysql

文章目录

关系型数据库和非关系型数据库的区别

  • 关系型数据库的优点
    • 容易理解。因为它采用了关系模型来组织数据。
    • 可以保持数据的一致性。
    • 数据更新的开销比较小。
    • 支持复杂查询(带where子句的查询)
  • 非关系型数据库的优点
    • 不需要经过SQL层的解析,读写效率高。
    • 基于键值对,数据的扩展性很好。
    • 可以支持多种类型数据的存储,如图片,文档等等。

什么是ORM?-mybatis

对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。

  • 优点:
    • 1)提高开发效率,降低开发成本
    • 2)使开发更加对象化
    • 3)可移植
    • 4)可以很方便地引入数据缓存之类的附加功能
  • 缺点:
    • 1)自动化进行关系数据库的映射需要消耗系统性能。其实这里的性能消耗还好啦,一般来说都可以忽略之。
    • 2)在处理多表联查、where条件复杂之类的查询时,ORM的语法会变得复杂。

如何评估一个索引创建的是否合理?

建议按照如下的原则来设计索引:

  1. 避免对经常更新的表进行过多的索引,并且索引中的列要尽可能少。应该经常用于查询的字段创建索引,但要避免添加不必要的字段。
  2. 数据量小的表最好不要使用索引,由于数据较少,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。
  3. 在条件表达式中经常用到的不同值较多的列上建立索引,在不同值很少的列上不要建立索引。比如在学生表的“性别”字段上只有“男”与“女”两个不同值,因此就无须建立索引,如果建立索引不但不会提高查询效率,反而会严重降低数据更新速度。
  4. 当唯一性是某种数据本身的特征时,指定唯一索引。使用唯一索引需能确保定义的列的数据完整性,以提高查询速度。
  5. 在频繁进行排序或分组(即进行group by或order by操作)的列上建立索引,如果待排序的列有多个,可以在这些列上建立组合索引。

Count函数

执行效果上:

count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL
count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL
count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。

执行效率上:

列名为主键,count(列名)会比count(1)快
列名不为主键,count(1)会比count(列名)快
如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
如果有主键,则 select count(主键)的执行效率是最优的
如果表只有一个字段,则 select count( *)最优。

count(可空字段) < count(非空字段) = count(主键 id) < count(1) ≈ count(*)

count(主键)和count(列名)

主键不为null,如果列名当中有null值,那么他们的返回是不一样的

数据库的三大范式

第一范式(1NF):要求数据库表的每一列都是不可分割的原子数据项。

第二范式(2NF):在 1NF 的基础上,有主键,非主键字段依赖于主键字段,要求数据库表中的每个实例或记录必须可以被唯一地区分

第三范式(3NF):在 2NF 基础上,任何非主属性不依赖于其它非主属性(在 2NF 基础上消除传递依赖) 第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关

Mysql中char和varchar的区别

char 是固定长度的,varchar 是可变长度的

char 如果某个长度小于M,MySQL就会在它的右边用空格补足,使长度达到M

varchar 每个值只占用刚好够用的字节,再加上一个用来记录其长度的字节,当长度小于255时,长度记录位占一个字节,大于时,占用两个字节

数据库设计或者功能开发中,要考虑到什么细节

  • 数据库和表的字符集统一使用 UTF8
  • 所有表和字段都需要添加注释
  • 尽量做到冷热数据分离,减小表的宽度
  • 尽可能把所有列定义为 NOT NUL
  • 禁止在数据库中存储图片,文件等大的二进制数据
  • 优先选择符合存储需要的最小的数据类型
  • 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据,可以将text数据单独成表

数据库创建表语句

CREAT TABLE 'table_name'{
	'ID' INT UNSIGNED AUTO_INCREMENT,
	'字段1' varchar(100) NOT NULL,
}ENGINE = InnoDB DEFUALT CHAREST = utf-8

如何评估一个索引创建的是否合理?

建议按照如下的原则来设计索引:

  1. 避免对经常更新的表进行过多的索引,并且索引中的列要尽可能少。应该经常用于查询的字段创建索引,但要避免添加不必要的字段。
  2. 数据量小的表最好不要使用索引,由于数据较少,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。
  3. 在条件表达式中经常用到的不同值较多的列上建立索引,在不同值很少的列上不要建立索引。比如在学生表的“性别”字段上只有“男”与“女”两个不同值,因此就无须建立索引,如果建立索引不但不会提高查询效率,反而会严重降低数据更新速度。
  4. 当唯一性是某种数据本身的特征时,指定唯一索引。使用唯一索引需能确保定义的列的数据完整性,以提高查询速度。
  5. 在频繁进行排序或分组(即进行group by或order by操作)的列上建立索引,如果待排序的列有多个,可以在这些列上建立组合索引。

Mysql索引的分类

主键索引、唯一索引、联合索引、普通索引、全文索引、空间索引

非聚集索引的分类

辅助索引、MyiSam的索引

请你说说MySQL索引,以及它们的好处和坏处

索引是一个单独的、存储在磁盘上的数据结构,包含着对数据表里所用记录的引用指针,使用索引可以提高查询效率,索引是在存储引擎中实现的,因此每一种存储引擎的索引都不完全相同。常见的索引结果有Hash和Btree+。

索引的好处:

  • 提高查询速度
  • 在使用分组和排序子句进行数据查询时,可以显著减少查询中分组和排序的时间
  • 通过创建唯一索引,可以保证数据库表中的每一行数据的唯一性

缺点:

  • 创建索引和维护索引是需要浪费时间的,随着数据量的增加耗费的时间也会随之增加
  • 索引需要占用磁盘空间
  • 当对表进行插入,删除和修改等操作时,索引也需要动态的维护,降低了数据维护的速度

索引的实现原理

BTree+树索引和Hash索引

BTree+树的非叶子节点只存储了key信息,这样的话可以才存储更多的索引key,降低了BTree+树的高度,叶子节点存放key和value值,每一次查询都要从根节点进行查询到叶子节点,这样每一次查询的效率都是稳定的,同时叶子节点有指向相邻节点的指针,提高了范围查找的效率

Hash索引是根据字段的哈希值建立一个hash表,key 是哈希值,value 是该数据对应的地址

Mysql的底层结构

服务层:连接器、查询缓存、分析器、优化器、执行器

存储引擎:负责数据的存储和提取

请你说说MySQL索引,以及它们的好处和坏处

索引是一个单独的、存储在磁盘上的数据结构,包含着对数据表里所用记录的引用指针,使用索引可以提高查询效率,索引是在存储引擎中实现的,因此每一种存储引擎的索引都不完全相同。常见的索引结果有Hash和Btree+。

索引的好处:

  • 提高查询速度
  • 在使用分组和排序子句进行数据查询时,可以显著减少查询中分组和排序的时间
  • 通过创建唯一索引,可以保证数据库表中的每一行数据的唯一性

缺点:

  • 创建索引和维护索引是需要浪费时间的,随着数据量的增加耗费的时间也会随之增加
  • 索引需要占用磁盘空间
  • 当对表进行插入,删除和修改等操作时,索引也需要动态的维护,降低了数据维护的速度

sql语句如何指定索引

SELECT 字段名表 
FROM 表名表
WITH (INDEX(索引名))
WHERE 查询条件

Mysql中char和varchar的区别

char 是固定长度的,varchar 是可变长度的

char 如果某个长度小于M,MySQL就会在它的右边用空格补足,使长度达到M

varchar 每个值只占用刚好够用的字节,再加上一个用来记录其长度的字节,当长度小于255时,长度记录位占一个字节,大于时,占用两个字节

索引的实现原理

BTree+树索引和Hash索引

BTree+树的非叶子节点只存储了key信息,这样的话可以才存储更多的索引key,降低了BTree+树的高度,叶子节点存放key和value值,每一次查询都要从根节点进行查询到叶子节点,这样每一次查询的效率都是稳定的,同时叶子节点有指向相邻节点的指针,提高了范围查找的效率

Hash索引是根据字段的哈希值建立一个hash表,key 是哈希值,value 是该数据对应的地址

sql语句

学生成绩表(姓名,科目,成绩),查询每门课成绩都>80分的学生姓名。

select name from table group by name having min(score) > 80;
select name from table group by name having count(score) = sum(case when score > 80 then 1 else 0 end)

添加索引

ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )//主键索引
ALTER TABLE `table_name` ADD UNIQUE ( `column` )//唯一索引
ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )//联合索引
ALTER TABLE <表名> ADD INDEX (<字段>);

SQL注入

SQL注入即是指 web应用程序 对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的 SQL语句 ,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息

如何阻止

(1) 过滤用户输入参数中的特殊字符,从而降低被 SQL 注入入的风险。

(2) 禁止通过字符拼接的 SQL 语句,严恪使用参数绑定传入的 SQL 参数。

(3) 合理使用数据库访问框架提供的防注入机制。比如 MyBatis 提供的#{} 绑定参数,从而防止 SQL注入。同时谨慎使用${ } , ${} 相当于使用字符拼接 SQL。

数据库SQL调优的几种方式

(6条消息) 数据库SQL调优的几种方式_lss0555的博客-CSDN博客_sql调优

  1. 创建索引

    1. 要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
    2. 在经常需要进行检索的字段上创建索引,比如要按照表字段username进行检索,那么就应该在姓名字段上创建索引,如果经常要按照员工部门和员工岗位级别进行检索,那么就应该在员工部门和员工岗位级别这两个字段上创建索引。
      创建索引给检索带来的性能提升往往是巨大的,因此在发现检索速度过慢的时候应该首先想到的就是创建索引。
    3. 一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。
  2. 避免在索引列上做计算

  3. 使用预编译查询

  4. 调整Where字句中的连接顺序

    根据这个原理表连接最好写在其他where条件之前,那些可以过滤掉最大数量记录。

  5. 尽量将多条SQL语句压缩到一句SQL中

    每次执行SQL的时候都要建立网络连接、进行权限校验、进行SQL语句的查询优化、发送执行结果,这个过程
    是非常耗时的,因此应该尽量避免过多的执行SQL语句,能够压缩到一句SQL执行的语句就不要用多条来执行。

  6. 用where字句替换HAVING字句
    避免使用HAVING字句,因为HAVING只会在检索出所有记录之后才对结果集进行过滤,而where则是在聚合前刷选记录,如果能通过where字句限制记录的数目,那就能减少这方面的开销。HAVING中的条件一般用于聚合函数的过滤,除此之外,应该将条件写在where字句中

  7. 使用表的别名

    当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个列名上。这样就可以减少解析的时间并减
    少哪些友列名歧义引起的语法错误。

  8. 用union all替换union
    当SQL语句需要union两个查询结果集合时,即使检索结果中不会有重复的记录,如果使用union这两个结果集同样会尝试进行合并,然后在输出最终结果前进行排序,因此如果可以判断检索结果中不会有重复的记录时候,应该用union all,这样效率就会因此得到提高。

  9. 用varchar/nvarchar 代替 char/nchar
    尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
    不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL也包含在内),都是占用 100个字符的空间的,如果是varchar这样的变长字段, null 不占用空间。

  10. 查询select语句优化

  11. 更新Update语句优化

    如果只更改1、2个字段,不要Update全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志

  12. 插入Insert语句优化

    在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

SQL语句的执行顺序

SQL语句执行顺序

  • FROM:对SQL语句执行查询时,首先对关键字两边的表以笛卡尔积的形式执行连接,并产生一个虚表V1。虚表就是视图,数据会来自多张表的执行结果。
  • ON:对FROM连接的结果进行ON过滤,并创建虚表V2
  • JOIN:将ON过滤后的左表添加进来,并创建新的虚拟表V3
  • WHERE:对虚拟表V3进行WHERE筛选,创建虚拟表V4
  • GROUP BY:对V4中的记录进行分组操作,创建虚拟表V5
  • HAVING:对V5进行过滤,创建虚拟表V6
  • SELECT:将V6中的结果按照SELECT进行筛选,创建虚拟表V7
  • DISTINCT:对V7表中的结果进行去重操作,创建虚拟表V8,如果使用了GROUP BY子句则无需使用DISTINCT,因为分组的时候是将列中唯一的值分成一组,并且每组只返回一行记录,所以所有的记录都h是不同的。
  • ORDER BY:对V8表中的结果进行排序。

如何优化SQL查询

使用索引:

如果查询时没有使用索引,查询语句将扫描表中的所有记录。在数据量大的情况下,这样查询的速度会很慢。如果使用索引进行查询,查询语句可以根据索引快速定位到待查询记录,从而减少查询的记录数,达到提高查询速度的目的。

优化子查询:

使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作。

子查询虽然可以使查询语句很灵活,但执行效率不高。执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表。然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表。因此,子查询的速度会受到一定的影响。如果查询的数据量比较大,这种影响就会随之增大。

在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引,性能会更好。

Mysql删除数据

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

DELETE FROM <表名> [WHERE 子句] [ORDER BY 子句] [LIMIT 子句]
DELETE FROM tb_name;  删除表中的所有数据

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

TRUNCATE [TABLE] 表名

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

TRUNCATE 和 DELETE 的区别

从逻辑上说,TRUNCATE 语句与 DELETE 语句作用相同,但是在某些情况下,两者在使用上有所区别。

  • DELETE 是 DML 类型的语句;TRUNCATE 是 DDL 类型的语句。它们都用来清空表中的数据。
  • DELETE 是逐行一条一条删除记录的;TRUNCATE 则是直接删除原来的表,再重新创建一个一模一样的新表,而不是逐行删除表中的数据,执行数据比 DELETE 快。因此需要删除表中全部的数据行时,尽量使用 TRUNCATE 语句, 可以缩短执行时间。
  • DELETE 删除数据后,配合事件回滚可以找回数据;TRUNCATE 不支持事务的回滚,数据删除后无法找回。
  • DELETE 删除数据后,系统不会重新设置自增字段的计数器;TRUNCATE 清空表记录后,系统会重新设置自增字段的计数器。
  • DELETE 的使用范围更广,因为它可以通过 WHERE 子句指定条件来删除部分数据;而 TRUNCATE 不支持 WHERE 子句,只能删除整体。
  • DELETE 会返回删除数据的行数,但是 TRUNCATE 只会返回 0,没有任何意义。

MySQL如何优化?

针对查询,我们可以通过使用索引、使用连接代替子查询的方式来提高查询速度。

针对慢查询,我们可以通过分析慢查询日志,来发现引起慢查询的原因,从而有针对性的进行优化。

针对插入,我们可以通过禁用索引、禁用检查等方式来提高插入速度,在插入之后再启用索引和检查。

针对数据库结构,我们可以通过将字段很多的表拆分成多张表、增加中间表、增加冗余字段等方式进行优化。

explain关注什么?

重点要关注如下几列:

列名 备注
type 本次查询表联接类型,从这里可以看到本次查询大概的效率。
key 最终选择的索引,如果没有索引的话,本次查询效率通常很差。
key_len 本次查询用于结果过滤的索引实际长度。
rows 预计需要扫描的记录数,预计需要扫描的记录数越小越好。
Extra 额外附加信息,主要确认是否出现 Using filesort、Using temporary 这两种情况。

另外,Extra列需要注意以下的几种情况:

关键字 备注
Using filesort 将用外部排序而不是按照索引顺序排列结果,数据较少时从内存排序,否则需要在磁盘完成排序,代价非常高,需要添加合适的索引。
Using temporary 需要创建一个临时表来存储结果,这通常发生在对没有索引的列进行GROUP BY时,或者ORDER BY里的列不都在索引里,需要添加合适的索引。
Using index 表示MySQL使用覆盖索引避免全表扫描,不需要再到表中进行二次查找数据,这是比较好的结果之一。注意不要和type中的index类型混淆。
Using where 通常是进行了全表/全索引扫描后再用WHERE子句完成结果过滤,需要添加合适的索引。
Impossible WHERE 对Where子句判断的结果总是false而不能选择任何数据,例如where 1=0,无需过多关注。
Select tables optimized away 使用某些聚合函数来访问存在索引的某个字段时,优化器会通过索引直接一次定位到所需要的数据行完成整个查询,例如MIN()\MAX(),这种也是比较好的结果之一。

表中有几千万条数据怎么办

建议按照如下顺序进行优化:

  1. 优化SQL和索引;
  2. 增加缓存,如memcached、redis;
  3. 读写分离,可以采用主从复制,也可以采用主主复制;
  4. 使用MySQL自带的分区表,这对应用是透明的,无需改代码,但SQL语句是要针对分区表做优化的;
  5. 做垂直拆分,即根据模块的耦合度,将一个大的系统分为多个小的系统;
  6. 做水平拆分,要选择一个合理的sharding key,为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表。

MySQL的慢查询优化有了解吗?

优化MySQL的慢查询,可以按照如下步骤进行:

开启慢查询日志:

MySQL中慢查询日志默认是关闭的,可以通过配置文件my.ini或者my.cnf中的log-slow-queries选项打开,也可以在MySQL服务启动的时候使用--log-slow-queries[=file_name]启动慢查询日志。

启动慢查询日志时,需要在my.ini或者my.cnf文件中配置long_query_time选项指定记录阈值,如果某条查询语句的查询时间超过了这个值,这个查询过程将被记录到慢查询日志文件中。

分析慢查询日志:

直接分析mysql慢查询日志,利用explain关键字可以模拟优化器执行SQL查询语句,来分析sql慢查询语句。

常见慢查询优化:

  1. 索引没起作用的情况

    • 在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置,索引才会起作用。
    • MySQL可以为多个字段创建索引。一个索引可以包括16个字段。对于多列索引,只有查询条件中使用了这些字段中的第1个字段时索引才会被使用。
    • 查询语句的查询条件中只有OR关键字,且OR前后的两个条件中的列都是索引时,查询中才使用索引。否则,查询将不使用索引。
  2. 优化数据库结构

    • 对于字段比较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。
    • 对于需要经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,把需要经常联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询,以此来提高查询效率。
  3. 分解关联查询

    很多高性能的应用都会对关联查询进行分解,就是可以对每一个表进行一次单表查询,然后将查询结果在应用程序中进行关联,很多场景下这样会更高效。

  4. 优化LIMIT分页

    当偏移量非常大的时候,例如可能是limit 10000,20这样的查询,这是mysql需要查询10020条然后只返回最后20条,前面的10000条记录都将被舍弃,这样的代价很高。优化此类查询的一个最简单的方法是尽可能的使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候这样做的效率会得到很大提升。

关系型和非关系型数据库的区别你了解多少?

  • 关系型数据库的优点
    • 容易理解。因为它采用了关系模型来组织数据。
    • 可以保持数据的一致性。
    • 数据更新的开销比较小。
    • 支持复杂查询(带where子句的查询)
  • 非关系型数据库的优点
    • 不需要经过SQL层的解析,读写效率高。
    • 基于键值对,数据的扩展性很好。
    • 可以支持多种类型数据的存储,如图片,文档等等

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQXRO6Qw-1682647034826)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220907112634255.png)]

Mysql查询语句加锁

lock in share mode 加入共享锁

for update 加入排他锁

最左匹配原则

在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。也就是如果你的 SQL 语句中用到了联合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个联合索引去进行匹配

缓存与数据库的一致性

先删除redis,再更新数据库;先更新数据库,再删除redis。两种各有各的好处。如果选择第一种,那么一般就是延时双删和订阅binlog;如果选择第二种,实现简单,但是短暂不一致。

1) 先删除缓存,后更新数据库

如果有 2 个线程要并发「读写」数据,可能会发生以下场景:

  1. 线程 A 要更新 X = 2(原值 X = 1)
  2. 线程 A 先删除缓存
  3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
  4. 线程 A 将新值写入数据库(X = 2)
  5. 线程 B 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。

可见,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况。

2) 先更新数据库,后删除缓存

依旧是 2 个线程并发「读写」数据:

  1. 缓存中 X 不存在(数据库 X = 1)
  2. 线程 A 读取数据库,得到旧值(X = 1)
  3. 线程 B 更新数据库(X = 2)
  4. 线程 B 删除缓存
  5. 线程 A 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。

这种情况「理论」来说是可能发生的,但实际真的有可能发生吗?

其实概率「很低」,这是因为它必须满足 3 个条件:

  1. 缓存刚好已失效
  2. 读请求 + 写请求并发
  3. 更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间短(步骤 2 和 5)

仔细想一下,条件 3 发生的概率其实是非常低的。

因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。

这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。

所以,我们应该采用这种方案,来操作数据库和缓存。

好,解决了并发问题,我们继续来看前面遗留的,第二步执行「失败」导致数据不一致的问题

如何保证第二步成功

异步重试,订阅数据库变更日志,再操作缓存

具体来讲就是,我们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。

那什么时候操作缓存呢?这就和数据库的「变更日志」有关了。

拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。

图片

订阅变更日志,目前也有了比较成熟的开源中间件,例如阿里的 canal,使用这种方案的优点在于:

  • 无需考虑写消息队列失败情况:只要写 MySQL 成功,Binlog 肯定会有
  • 自动投递到下游队列:canal 自动把数据库变更日志「投递」给下游的消息队列

当然,与此同时,我们需要投入精力去维护 canal 的高可用和稳定性。

至此,我们可以得出结论,想要保证数据库和缓存一致性,推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做

主从库延迟和延迟双删问题

到这里,还有 2 个问题,是我们没有重点分析过的。

第一个问题,还记得前面讲到的「先删除缓存,再更新数据库」方案,导致不一致的场景么?

这里我再把例子拿过来让你复习一下:

2 个线程要并发「读写」数据,可能会发生以下场景:

  1. 线程 A 要更新 X = 2(原值 X = 1)
  2. 线程 A 先删除缓存
  3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
  4. 线程 A 将新值写入数据库(X = 2)
  5. 线程 B 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。

第二个问题:是关于「读写分离 + 主从复制延迟」情况下,缓存和数据库一致性的问题。

在「先更新数据库,再删除缓存」方案下,「读写分离 + 主从库延迟」其实也会导致不一致:

  1. 线程 A 更新主库 X = 2(原值 X = 1)
  2. 线程 A 删除缓存
  3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
  4. 从库「同步」完成(主从库 X = 2)
  5. 线程 B 将「旧值」写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。

看到了么?这 2 个问题的核心在于:缓存都被回种了「旧值」

那怎么解决这类问题呢?

最有效的办法就是,把缓存删掉

但是,不能立即删,而是需要「延迟删」,这就是业界给出的方案:缓存延迟双删策略

按照延时双删策略,这 2 个问题的解决方案是这样的:

解决第一个问题:在线程 A 删除缓存、更新完数据库之后,先「休眠一会」,再「删除」一次缓存。

解决第二个问题:线程 A 可以生成一条「延时消息」,写到消息队列中,消费者延时「删除」缓存。

这两个方案的目的,都是为了把缓存清掉,这样一来,下次就可以从数据库读取到最新值,写入缓存。

但问题来了,这个「延迟删除」缓存,延迟时间到底设置要多久呢?

  • 问题1:延迟时间要大于「主从复制」的延迟时间
  • 问题2:延迟时间要大于线程 B 读取数据库 + 写入缓存的时间

但是,这个时间在分布式和高并发场景下,其实是很难评估的

很多时候,我们都是凭借经验大致估算这个延迟时间,例如延迟 1-5s,只能尽可能地降低不一致的概率。

所以你看,采用这种方案,也只是尽可能保证一致性而已,极端情况下,还是有可能发生不一致。

所以实际使用中,我还是建议你采用「先更新数据库,再删除缓存」的方案,同时,要尽可能地保证「主从复制」不要有太大延迟,降低出问题的概率。

总结

关于缓存和数据库不一致的问题,可以采取两种方法第一种先删除缓存,在更新数据库,但是在并发情况下会出现不一致的情况,可以采用延迟双删的策略,延时双删的基本思路如下:

  1. 删除缓存;
  2. 更新数据库;
  3. sleep N毫秒;
  4. 再次删除缓存。

阻塞一段时间之后,再次删除缓存,就可以把这个过程中缓存中不一致的数据删除掉。而具体的时间,要评估你这项业务的大致时间,按照这个时间来设定即可。

如果采用先更新数据库再删除缓存的话,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据一致性;

订阅binlog当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qjxrcljm-1682647034827)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220903142730256.png)]

删除缓存失败的情况:增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可

在「先更新数据库,再删除缓存」方案下,「读写分离 + 主从库延迟」也会导致缓存和数据库不一致,缓解此问题的方案是「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率

使用先更新数据库,在删除缓存的操作,+cannel+消息队列

preview

流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。

Mysql的琐机制

  1. 「表锁」:是粒度最大的锁,表示当前的操作对整张表加锁,开销小,加锁快,不会出现死锁,但是由于粒度太大,因此造成锁的冲突机率大,并发性能低。Mysql中**「MyISAM储存引擎就支持表锁」,MyISAM的表锁模式有两种:「表共享读锁」「表独占写锁」**。表锁被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁。

  2. **「页锁」**的粒度是介于行锁和表锁之间的一种锁。

  3. **「行锁」**是粒度最小的锁机制,行锁的加锁开销性能大,加锁慢,并且会出现死锁,但是行锁的锁冲突的几率低,并发性能高。行锁是InnoDB默认的支持的锁机制,MyISAM不支持行锁,这个也是InnoDB和MyISAM的区别之一。

行锁在使用的方式上可以划分为:「共享读锁(S锁)和排它写锁(X锁)」

当一个事务对Mysql中的一条数据行加上了读锁,当前事务不能修改该行数据只能执行度操作,其他事务只能对该行数据加读锁不能加写锁。若是一个事务对一行数据加了写锁,该事物能够对该行数据执行读和写操作,其它事务不能对该行数据加任何的锁,既不能读也不能写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgXG81CQ-1682647034828)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220903144148596.png)]

乐观锁和悲观锁

  1. 乐观锁:乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1**;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。**

  2. 悲观锁:悲观锁的实现,往往依靠数据库提供的锁机制。悲观锁的实现:首先实现悲观锁时,我们必须先使用set autocommit=0; 关闭mysql的autoCommit属性。因为我们查询出数据之后就要将该数据锁定。关闭自动提交后,我们需要手动开启事务。

    mysql使用的共享锁和排它锁来实现悲观锁,select…lock in share mode(共享锁),select…for update(排他锁)

总结:读用乐观锁,写用悲观锁

数据冗余

数据冗余:在一个数据集合中重复的数据称为数据冗余

例如在设计数据库时,某一字段属于一个表,但它又同时出现在另一个或多个表,且完全等同于它在其本来所属表的意义表示,那么这个字段就是一个冗余字段。

缺点:

  • 存储空间的浪费。
  • 系统开销大,维护成本高
  • 数据的一致性难以保证:当冗余字段使用的多了,数据一致性就难以保证,为了保证一致性就会产生很大的性能开销,如果某些冗余字段是采用人工维护(开发人员),数据一致性就会难以得到保障.

好处:

  • 利用空间换时间,提高查询速度
  • 可以用于数据恢复

Mysql宕机之后怎么办,用哪个日志恢复,具体怎么恢复的知道吗?两阶段提交

使用redo log日志进行恢复

img

  • 两阶段提交的第一阶段 (prepare阶段):写rodo-log 并将其标记为prepare状态。
  • 紧接着写binlog
  • 两阶段提交的第二阶段(commit阶段):写bin-log 并将其标记为commit状态。

两段提交

1、prepare阶段,写redo log;

2、commit阶段,写binlog并且将redo log的状态改成commit状态;

mysql发生崩溃恢复的过程中,会根据redo log日志,结合 binlog 记录来做事务回滚:

1、如果redo log 和 binlog都存在,逻辑上一致,那么提交事务;

2、如果redo log存在而binlog不存在,逻辑上不一致,那么回滚事务;

最后大家可发现,这里的两阶段提交,实际是存在与redo log与binlog。所以当未开启binlog,那就是提交事务直接写到redo log里面。这也就是redo log事务两阶段提交,看场景区分的原因。

两段提交详细解释

在执行更新语句过程,会记录redo logbinlog两块日志,以基本的事务为单位**,redo log在事务执行过程中可以不断写入**,binlog只有在提交事务时才写入,所以redo logbinlog的写入时机不一样。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ocT4c6VB-1682647034829)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220912155444125.png)]

如果不小心删除了一个表,你知道用什么恢复吗?

找到bin log文件,当前正在使用的binlog文件里面就有我们要恢复的数据。一般生产环境中的binlog文件都是几百M乃至上G的大小,我们不能逐行去找被删除的数据在什么位置,所以记住误操作的时间很重要,我们可以通过mysqlbinlog命令的–start-datetime参数快速定位数据位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c9GuIdUv-1682647034829)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220820205350823.png)]

是什么是事务?事务的使用场景

事务就是一组逻辑操作,要么全部成功要么全部失败

场景一:如果实际的业务中,需要将一条数据同时存放到两张表中, 并且要求两张表中的数据同步,那么此时就需要使用事务管理机制,保证数据同步。如果出现错误情况,比如表一插入数据成功,表二插入数据失败,那么就回滚,终止数据持久化操作。

场景二:金融行业的软件开发严格重视事务处理,比如我们常见的转账操作,一方的账户金额减少,对应的是另一方的账户金额增加,这个过程需要使用到事务机制,不然转账不能成功

https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/)

事务的隔离级别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEgiFbv5-1682647034829)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220828103750928.png)]

  1. 脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。
  2. 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。
  3. 幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。

MVCC

MVCC全称多版本并发控制,它为每个数据都根据事务维护了多个版本使得其在并发事务中解决了读写冲突,同时使用快照读为mvcc提供非阻塞读功能,所以他是一个用于解决读写冲突的无锁并发控制机制

(1).隐藏列:InnoDB每行数据中都有隐藏列,隐藏列中包含了本行数据的事务id,指向undo log的指针等;

(2).基于undo log的版本链:每行数据的隐藏列中包含了指向undo log的指针,而每条undo log指向了更高级的undo log,从而形成了undo log的版本链;

(3).ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指针版本,但是具体恢复到哪个版本,需要根据ReadView确定; ReadView:指事务在某个时刻给整个事务系统打快照,之后在进行读写操作时,会将读取到的数据中的事务id于trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务是否可见

ACID和锁

A:原子性,实现原子性的关键就是undo log日志,事务在对数据库进行修改的时候,会将修改的操作记录到undo log日志当中,如果事务失败或者回滚的话,那么读取undo log中的信息进行恢复

D:持久性,实现一致性的关键就是redo log,当进行修改数据的时候,还会redo log将记录这次修改,当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复,并且redo log都是顺序写入的

I:隔离性,并发情形下事务之间互不干扰

第一方面,(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性。

隔离性是通过锁来完成的,事务在修改数据之前,需要先获得相应的锁。获得锁之后,事务便可以修改数据。该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

第二方面,(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性。

MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。它最大的优点是读不加锁,因此读写不冲突,并发性能好。

原理:维护了一个数据的多个版本,快照读为Mysql实现MVCC提供了一个阻塞功能

  1. 隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。
  2. 基于undo log的版本链:每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。
  3. ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。

C:一致性,就是事务前后,数据库的完整性约束不被破坏

可以说,一致性是事务追求的最终目标。前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。实现一致性的措施包括:

  • 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证。
  • 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等。
  • 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致。

间隙锁

间隙锁用于锁定一个范围,但不包含记录本身。它的作用是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生。

MVCC

MVCC多版本并发控制,控制了一个数据的多个版本号,实现了读写不冲突

  1. 隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。
  2. 基于undo log的版本链:每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。
  3. ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。

I:一致性,就是事务前后,数据库的完整性约束不被破坏

MVCC➕Next-key-Lock 防止幻读

InnoDB存储引擎在 RR 级别下通过 MVCCNext-key Lock 来解决幻读问题:

1、执行普通 select,此时会以 MVCC 快照读的方式读取数据

在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”

2、执行 select…for update/lock in share mode、insert、update、delete 等当前读

在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB 使用 Next-key Lockopen in new window 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读

什么时候会使用到间隙锁

只使用唯一索引查询,并且只锁定一条记录时,innoDB会使用行锁。

只使用唯一索引查询,但是检索条件是范围检索,或者是唯一检索然而检索结果不存在(试图锁住不存在的数据)时,会产生 Next-Key Lock。

使用普通索引检索时,不管是何种查询,只要加锁,都会产生间隙锁。

同时使用唯一索引和普通索引时,由于数据行是优先根据普通索引排序,再根据唯一索引排序,所以也会产生间隙锁。

说一说你对redo log、undo log、binlog的了解

binlog(Binary Log)

二进制日志文件就是常说的binlog。二进制日志记录了MySQL所有修改数据库的操作,然后以二进制的形式记录在日志文件中,其中还包括每条语句所执行的时间和所消耗的资源,以及相关的事务信息。

默认情况下,二进制日志功能是开启的,启动时可以重新配置--log-bin[=file_name]选项,修改二进制日志存放的目录和文件名称。

redo log

重做日志用来实现事务的持久性,即事务ACID中的D。它由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),它是持久的。

InnoDB是事务的存储引擎,它通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。这里的日志是指重做日志,在InnoDB存储引擎中,由两部分组成,即redo log和undo log。

redo log用来保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能。redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。

undo log

重做日志记录了事务的行为,可以很好地通过其对页进行“重做”操作。但是事务有时还需要进行回滚操作,这时就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。

redo存放在重做日志文件中,与redo不同,undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment),undo段位于共享表空间内。

数据库如何保证一致性?

分为两个层面来说。

  • 从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。
  • 从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据!

数据库如何保证原子性?

主要是利用 Innodb 的undo logundo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的 SQL语句,他需要记录你要回滚的相应日志信息。 例如

  • 当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据
  • 当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作
  • 当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作

undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子

数据库如何保证持久性?

主要是利用Innodb的redo log。重写日志, 正如之前说的,MySQL是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再写回到磁盘上。如果此时突然宕机,内存中的数据就会丢失。 怎么解决这个问题? 简单啊,事务提交前直接把数据写入磁盘就行啊。 这么做有什么问题?

  • 只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16kb大小,你只改其中一点点东西,就要将16kb的内容刷入磁盘,听着也不合理。
  • 毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。显然操作随机IO,速度会比较慢。

于是,决定采用redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo logbinlog内容决定回滚数据还是提交数据。

采用redo log的好处?

其实好处就是将redo log进行刷盘比对数据页刷盘效率高,具体表现如下:

  • redo log体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。
  • redo log是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快。

数据库的左链接和右链接

左链接:它会返回左表中的所有记录和右表中满足连接条件的记录。

右链接:它会返回右表中的所有记录和左表中满足连接条件的记录。

全外连接:返回左表和右表的所有记录以及满足条件的记录

自链接:返回左右表满足条件的记录

Mysql查询语句加锁

lock in share mode 加入共享锁

for update 加入排他锁

是什么是事务?事务的使用场景

事务就是一组逻辑操作,要么全部成功要么全部失败

场景一:如果实际的业务中,需要将一条数据同时存放到两张表中, 并且要求两张表中的数据同步,那么此时就需要使用事务管理机制,保证数据同步。如果出现错误情况,比如表一插入数据成功,表二插入数据失败,那么就回滚,终止数据持久化操作。

场景二:金融行业的软件开发严格重视事务处理,比如我们常见的转账操作,一方的账户金额减少,对应的是另一方的账户金额增加,这个过程需要使用到事务机制,不然转账不能成功

https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/)

Mysql常用的数据类型

数值类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C0fm9042-1682647034830)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220829112751233.png)]

日期类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CnkUBuRm-1682647034830)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220829112805686.png)]

字符串类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eXYXhBAY-1682647034830)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220829112816093.png)]

MySQL 数据类型 | 菜鸟教程 (runoob.com)

大表优化

读写分离

也是目前常用的优化,从库读主库写,一般不要采用双主或多主引入很多复杂性,尽量采用文中的其他方案来提高性能。同时目前很多拆分的解决方案同时也兼顾考虑了读写分离

缓存

垂直拆分

垂直分库是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联

水平拆分

水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的MySQL表或库,达主从复制读写分离的实现

  1. 部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
  2. 保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的主从复制
  3. 系统将写请求交给主数据库处理,读请求交给从数据库处理。

主从复制的原理

MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。因此,我们根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。

  1. 主库将数据库中数据的变化写入到 binlog
  2. 从库连接主库
  3. 从库会创建一个 I/O 线程向主库请求更新的 binlog
  4. 主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收
  5. 从库的 I/O 线程将接收的 binlog 写入到 relay log 中。
  6. 从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。

canal 的工具:canal 的原理就是模拟 MySQL 主从复制的过程,解析 binlog 将数据同步到其他的数据源。

复制的工作原理并不复杂,其实就是一个完全备份加上二进制日志备份的还原。不同的是这个二进制日志的还原操作基本上实时在进行中。这里特别需要注意的是,复制不是完全实时地进行同步,而是异步实时。这中间存在主从服务器之间的执行延时,如果主服务器的压力很大,则可能导致主从服务器延时较大。复制的工作原理如下图所示,其中从服务器有2个线程,一个是I/O线程,负责读取主服务器的二进制日志,并将其保存为中继日志;另一个是SQL线程,复制执行中继日志。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-okkvstw1-1682647034831)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220912151953665.png)]

项目中如何解决高并发

  • 使用redis缓存,缓存首页数据
  • 使用Nginx解决负载均衡的问题
  • 可以使用数据库的主从读写分离模式

到分布式的效果,能够支持非常大的数据量。前面的表分区本质上也是一种特殊的库内分表

redo log日志

redo log(重做日志)是InnoDB存储引擎独有的,它让MySQL拥有了崩溃恢复能力。

比如 MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性。

img

MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool 中。

后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。

更新表数据的时候,也是如此,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。

然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里,接着刷盘到 redo log 文件里。

img

图片笔误提示:第 4 步 “清空 redo log buffe 刷盘到 redo 日志中”这句话中的 buffe 应该是 buffer。

理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。

小贴士:每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成

InnoDB 存储引擎为 redo log 的刷盘策略提供了 innodb_flush_log_at_trx_commit 参数,它支持三种策略:

  • 0 :设置为 0 的时候,表示每次事务提交时不进行刷盘操作
  • 1 :设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值)
  • 2 :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache

innodb_flush_log_at_trx_commit 参数默认为 1 ,也就是说当事务提交时会调用 fsync 对 redo log 进行刷盘

另外,InnoDB 存储引擎有一个后台线程,每隔1 秒,就会把 redo log buffer 中的内容写到文件系统缓存(page cache),然后调用 fsync 刷盘。

MySQL三大日志(binlog、redo log和undo log)详解 | JavaGuide

为什么要有redolog日志,直接刷盘不行吗

数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。

如果是写 redo log,一行记录可能就占几十 Byte,只包含表空间号、数据页号、磁盘文件偏移量、更新值,再加上是顺序写,所以刷盘速度很快。

所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。

varchar和char的区别

1、CHAR的长度是不可变的,而VARCHAR的长度是可变的,也就是说,定义一个CHAR[10]和VARCHAR[10],如果存进去的是‘ABCD’, 那么CHAR所占的长度依然为10,除了字符‘ABCD’外,后面跟六个空格,而VARCHAR的长度变为4了,取数据的时候,CHAR类型的要用trim()去掉多余的空格,而VARCHAR类型是不需要的。

2、CHAR的存取速度要比VARCHAR快得多,因为其长度固定,方便程序的存储与查找;但是CHAR为此付出的是空间的代价,因为其长度固定,所以难免会有多余的空格占位符占据空间,可以说是以空间换取时间效率,而VARCHAR则是以空间效率为首位的。VARCHAR需要使用1或2个额外字节记录字符串的长度

3、CHAR的存储方式是,一个英文字符(ASCII)占用1个字节,一个汉字占用两个字节;而VARCHAR的存储方式是,一个英文字符占用2个字节,一个汉字也占用2个字节。

4、两者的存储数据都是非unicode的字符数据。

varchar(255)存多少个汉字

在字符集为UTF-8的情况下:
MySQL | version < 4.1
VARCHAR以字节为单位存储,假设全部为常用汉字,则VARCHAR(255)共可存放约85个汉字。

MySQL | version >= 4.1
VARCHAR以字符为单位存储,假设全部为常用汉字,则VARCHAR(255)可以存放255个汉字。

MySQL的一级缓存和二级缓存

一级缓存缓存的是 SQL 语句,二级缓存缓存的是结果对象。

一级缓存 指的是Session,作用域也是在Session级别 , 在操作数据库时需要构造SQLSession的对象,这个对象中可以存缓存数据,而不同的SQLSession缓存数据的区域是互不影响的,只能作用于在同一个Session中

一级缓存的工作原理:

第一次查询在数据库查,而后放在缓存里,第二次查询就直接在缓存里查

如果两次查询期间有增删改的操作,缓存里的数据会清空,再次查询需要去数据库里查

二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。二级缓存的作用范围更大。

二级缓存是mapper级别的缓存。使用二级缓存时,多个SqlSession使用同一个Mapper的sql语句去操作数据库,得到的数据会存在二级缓存区域,它同样是使用HashMap进行数据存储。相比一级缓存SqlSession,二级缓存的范围更大,多个Sqlsession可以共用二级缓存,二级缓存是跨SqlSession的。

二级缓存的作用域是mapper的同一个namespace。不同的sqlSession两次执行相同的namespace下的sql语句,且向sql中传递的参数也相同,即最终执行相同的sql语句,则第一次执行完毕会将数据库中查询的数据写到缓存,第二次查询会从缓存中获取数据,不再去底层数据库查询,从而提高效率。

二级缓存的工作原理:

img

Mybatis的一级缓存和二级缓存

了解MyBatis缓存机制吗?

参考答案

MyBatis的缓存分为一级缓存和二级缓存。

一级缓存:

一级缓存也叫本地缓存,它默认会启用,并且不能关闭。一级缓存存在于SqlSession的生命周期中,即它是SqlSession级别的缓存。在同一个 SqlSession 中查询时,MyBatis 会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession 中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map 缓存对象中己经存在该键值时,则会返回缓存中的对象。

二级缓存:

二级缓存存在于SqlSessionFactory 的生命周期中,即它是SqlSessionFactory级别的缓存。若想使用二级缓存,需要在如下两处进行配置。

在MyBatis 的全局配置settings 中有一个参数cacheEnabled,这个参数是二级缓存的全局开关,默认值是true ,初始状态为启用状态。

MyBatis 的二级缓存是和命名空间绑定的,即二级缓存需要配置在Mapper.xml 映射文件中。在保证二级缓存的全局配置开启的情况下,给Mapper.xml 开启二级缓存只需要在Mapper. xml 中添加如下代码:

<cache />

二级缓存具有如下效果:

  • 映射语句文件中的所有SELECT 语句将会被缓存。
  • 映射语句文件中的所有时INSERT 、UPDATE 、DELETE 语句会刷新缓存。
  • 缓存会使用Least Rece ntly U sed ( LRU ,最近最少使用的)算法来收回。
  • 根据时间表(如no Flush Int erv al ,没有刷新间隔),缓存不会以任何时间顺序来刷新。
  • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024 个引用。
    下的sql语句,且向sql中传递的参数也相同,即最终执行相同的sql语句**,则第一次执行完毕会将数据库中查询的数据写到缓存,第二次查询会从缓存中获取数据,不再去底层数据库查询,从而提高效率。

二级缓存的工作原理:

[外链图片转存中…(img-zE8vYANX-1682647034832)]

Mybatis的一级缓存和二级缓存

了解MyBatis缓存机制吗?

参考答案

MyBatis的缓存分为一级缓存和二级缓存。

一级缓存:

一级缓存也叫本地缓存,它默认会启用,并且不能关闭。一级缓存存在于SqlSession的生命周期中,即它是SqlSession级别的缓存。在同一个 SqlSession 中查询时,MyBatis 会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession 中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map 缓存对象中己经存在该键值时,则会返回缓存中的对象。

二级缓存:

二级缓存存在于SqlSessionFactory 的生命周期中,即它是SqlSessionFactory级别的缓存。若想使用二级缓存,需要在如下两处进行配置。

在MyBatis 的全局配置settings 中有一个参数cacheEnabled,这个参数是二级缓存的全局开关,默认值是true ,初始状态为启用状态。

MyBatis 的二级缓存是和命名空间绑定的,即二级缓存需要配置在Mapper.xml 映射文件中。在保证二级缓存的全局配置开启的情况下,给Mapper.xml 开启二级缓存只需要在Mapper. xml 中添加如下代码:

<cache />

二级缓存具有如下效果:

  • 映射语句文件中的所有SELECT 语句将会被缓存。
  • 映射语句文件中的所有时INSERT 、UPDATE 、DELETE 语句会刷新缓存。
  • 缓存会使用Least Rece ntly U sed ( LRU ,最近最少使用的)算法来收回。
  • 根据时间表(如no Flush Int erv al ,没有刷新间隔),缓存不会以任何时间顺序来刷新。
  • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024 个引用。
  • 缓存会被视为read/write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

猜你喜欢

转载自blog.csdn.net/qq_43167873/article/details/130420407