当前位置:   article > 正文

java基础巩固-宇宙第一AiYWM:为了维持生计,MySQL基础PartX优化篇(关系型数据库【常见优化思路】和非关系型数据库【Redis内存优化】、不仅仅局限于代码层面的性能优化)~整起_非关系式数据库查询优化

非关系式数据库查询优化

优化,谝一谝。查询优化或者SQL优化、索引优化、库表结构优化,或者说设计优化以及硬件优化需要齐头并进,一个不落。并且关系型数据库和非关系型数据库肯定都涉及到了优化:

PART0:定位SQL语句的性能问题

  • 首先呢,你不管用啥方法优化,咱们得大概先知道问题在哪吧,语法错误或者其他什么错误才能对症下药呗。
    • 如何定位SQL语句的性能问题?
      在这里插入图片描述
      • 对于低性能的SQL语句的定位,最重要也是最有效的方法就是使用执行计划MySQL提供了explain命令来查看语句的执行计划【执行计划就是显示数据库引擎对于SQL语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等】。 我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条SQL语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引
        • explain:EXPLAIN命令是查看查询优化器如何决定执行查询的主要方法。可以帮助咱们了解MySQL优化器是如何工作的。要使用EXPLAIN,只需在查询中的SELECT关键字之前增加EXPLAIN这个词。MySQL会在查询上设置一个标记。当执行查询时,这个标记会使其返回关于在执行计划中每一步的信息,而不是执行它。它会返回一行或多行信息,显示出执行计划中的每一部分和执行的次序。
          在这里插入图片描述

PART1:关系型数据库优化:以MySQL为例

  • 提高 MySQL 性能的方法
    • 第一种方法:先处理掉那些占着连接但是不工作的线程。或者再考虑断开事务内空闲太久的连接。kill connection + id
    • 第二种方法:减少连接过程的消耗以及慢查询等过程的优化::慢查询性能问题在 MySQL 中主要是指会引发性能问题的慢查询【要找到最需要优化的 SQL 语句。要么是使用最频繁的语句,要么是效率较低的SQL语句并且优化后提高最明显的语句可以通过查询 MySQL 的慢查询日志来发现需要进行优化的 SQL 语句
      • 一般查询耗时超过 1 秒的 SQL 都被称为慢 SQL,有的公司运维组要求的可能更加严格,比如如果 SQL 的执行耗时超过 0.2s,也被称为慢 SQL,必须在限定的时间内尽快优化,不然可能会影响服务的正常运行和用户体验。
        • 默认的情况下呢,MySQL数据库是不开启慢查询日志(slow query log)呢。所以我们需要手动把它打开。
        • 慢查询日志查询相关命令:
          • 查看下慢查询日志配置,我们可以使用show variables like 'slow_query_log%'命令,如下:
            在这里插入图片描述
          • 查看超过多少时间才记录到慢查询日志,可以使用show variables like 'long_query_time’命令
            在这里插入图片描述
      • 慢查询日志:出现慢查询通常的排查手段是先使用慢查询日志功能查询出比较慢的 SQL 语句,然后再通过 Explain 来查询 SQL 语句的执行计划【MySQL将显示来自优化器的有关语句执行计划的信息,即MySQL解释了它将如何处理该语句,包括有关如何连接表以及以何种顺序连接表等信息】,最后分析并定位出问题的根源,再进行处理
        在这里插入图片描述
        • 慢查询日志指的是在 MySQL 中可以通过配置来开启慢查询日志的记录功能,超过long_query_time值的 SQL 将会被记录在日志中
          • 我们可以通过设置“slow_query_log=1”来开启慢查询
          • 需要注意的是,在开启慢日志功能之后,会对 MySQL 的性能造成一定的影响,因此在生产环境中要慎用此功能
        • 一般来说,用explain查看分析SQL的执行计划时我们需要重点关注type、rows、filtered、extra、key【Java中文社群的程序员小富老师关于Explain 执行计划详解
          • type:表示连接类型,查看索引执行情况的一个重要指标:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
            在这里插入图片描述
          • rows:rows列表示MySQL估算要找到我们所需的记录,需要读取的行数。对于InnoDB表,此数字是估计值,并非一定是个准确值
          • filtered:filtered列是一个百分比的值,表里符合条件的记录数的百分比。简单点说,这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例
          • extra:extra字段包含有关MySQL如何解析查询的其他信息,它一般会出现这几个值:
            在这里插入图片描述
          • key:key列表示实际用到的索引。一般配合possible_keys列一起看
        • profile 分析执行耗时:
          • explain只是看到SQL的预估执行计划,如果要了解SQL真正的执行线程状态及消耗的时间,需要使用profiling。开启profiling参数后,后续执行的SQL语句都会记录其资源开销,包括IO,上下文切换,CPU,内存等等,我们可以根据这些开销进一步分析当前慢SQL的瓶颈再进一步进行优化。profiling默认是关闭,我们可以使用show variables like '%profil%'查看是否开启
            在这里插入图片描述
          • 可以使用set profiling=ON开启。开启后,可以运行几条SQL,然后使用show profiles查看一下。
            在这里插入图片描述
          • show profiles会显示最近发给服务器的多条语句,条数由变量profiling_history_size定义,默认是15。如果我们需要看单独某条SQL的分析,可以show profile查看最近一条SQL的分析,也可以使用show profile for query id(其中id就是show profiles中的QUERY_ID)查看具体一条的SQL语句分析除了查看profile ,还可以查看cpu和io
            在这里插入图片描述
        • Optimizer Trace分析详情:profile只能查看到SQL的执行耗时,但是无法看到SQL真正执行的过程信息,即不知道MySQL优化器是如何选择执行计划。这时候,我们可以使用Optimizer Trace,它可以跟踪执行语句的解析优化执行的全过程
          • 可以使用set optimizer_trace="enabled=on"打开开关,接着执行要跟踪的SQL,最后执行select * from information_schema.optimizer_trace跟踪
            在这里插入图片描述
          • 可以查看分析其执行树,会包括三个阶段:
            在这里插入图片描述
            • join_preparation:准备阶段
            • join_optimization:分析阶段
            • join_execution:执行阶段
        • 在业务系统中,除了使用主键进行的查询,其他的开发人员都会 在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
        • 捡田螺的小男孩老师写的导致数据慢查询的常见原因文章
      • 慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?。所以优化也是针对这三个方向来的:
        • 优化方向一:首先分析 语句,看看 是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。【SQl没办法很好优化,可以改用ES的方式,或者数仓。】
          在这里插入图片描述
          • 咱们得考虑考虑优化查询过程中的数据访问
            • 访问数据太多导致查询性能下降,那咱们是不是要先确定应用程序 是否在检索大量超过需要的数据,可能是太多行或列
            • 确认MySQL服务器是否在分析大量不必要的数据行
            • 查询不需要的数据。解决办法:使用limit解决
            • 多表关联返回全部列。解决办法:指定列名
            • 总是返回全部列。解决办法:避免使用SELECT *
            • 重复查询相同的数据。解决办法:可以缓存数据,下次直接读取缓存
          • select 具体字段 来查询具体的字段而非全部字段:要尽量避免使用select *,而是查询需要的字段,这样可以提升速度,以及减少网络传输的带宽压力
            在这里插入图片描述
            • 这样以来咱们就可以只取需要的字段,就能节省资源并且减少网络开销。并且 select * 进行查询时,很可能就不会使用到覆盖索引了,就会造成回表查询
          • 如果知道查询结果只有一条或者只要最大/最小一条记录,建议用limit 1。加上limit 1后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高。但是呢,如果name是唯一索引的话,是不必要加上limit 1了,因为limit的存在主要就是为了防止全表扫描,从而提高性能, 如果一个语句本身可以预知不用全表扫描,有没有limit ,性能的差别并不大
            在这里插入图片描述
          • 优化子查询:尽量使用 Join 语句来替代子查询,因为子查询是嵌套查询,而嵌套查询会新创建一张临时表,而临时表的创建与销毁会占用一定的系统资源以及花费一定的时间,同时对于返回结果集比较大的子查询,其对查询性能的影响更大
            在这里插入图片描述
          • 小表驱动大表:我们要尽量使用小表驱动大表的方式进行查询,也就是如果 B 表的数据小于 A 表的数据,那执行的顺序就是先查 B 表再查 A 表,具体查询语句如下:
            sql select name from A where id in (select id from B);
            • 小的循环在外层,对于数据库连接来说就只连接5次,进行5000次操作,如果1000在外,则需要进行1000次数据库连接,从而浪费资源,增加消耗。这就是为什么要小表驱动大表
          • 适当增加冗余字段:增加冗余字段可以减少大量的连表查询,因为多张表的连表查询性能很低,所有可以适当的增加冗余字段【冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。】,以减少多张表的关联查询,这是以空间换时间的优化策略
            • 优化关联查询:
            • order by 走文件排序导致的慢查询
              • 如果order by 使用到文件排序,则会可能会产生慢查询:select name,age,city from staff where city = ‘深圳’ order by age limit 10;//查询前10个,来自深圳员工的姓名、年龄、城市,并且按照年龄小到大排序。
                在这里插入图片描述
              • order by使用文件排序,效率会低一点。我们怎么优化呢?
                • 因为数据是无序的,所以就需要排序。如果数据本身是有序的,那就不会再用到文件排序啦。而索引数据本身是有序的,我们通过建立索引来优化order by语句
                • 我们还可以通过调整max_length_for_sort_data、sort_buffer_size等参数优化;
              • order by文件排序效率为什么较低
                在这里插入图片描述
                在这里插入图片描述
                • rowid排序:rowid排序,一般需要回表去找满足条件的数据,所以效率会慢一点。以下这个SQL,使用rowid排序:select name,age,city from staff where city = ‘深圳’ order by age limit 10;
                  在这里插入图片描述
                  在这里插入图片描述
                • 全字段排序:同样的SQL,如果是走全字段排序是这样的:select name,age,city from staff where city = ‘深圳’ order by age limit 10;
                  在这里插入图片描述
            • group by使用临时表:如果不注意group by,很容易产生慢SQL
              在这里插入图片描述
              • group by是怎么使用到临时表和排序了呢?这个SQL的执行流程:
                在这里插入图片描述
                • 临时表的排序是怎样的呢?
                  在这里插入图片描述
              • group by可能会慢在哪里?group by使用不当,很容易就会产生慢SQL问题。因为它既用到临时表,又默认用到排序。有时候还可能用到磁盘临时表
                在这里插入图片描述
              • 如何优化group by呢
                在这里插入图片描述
            • 但是呢,设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差
          • SQL 语句没写好时,就得结合上面几点考虑SQL优化:通过优化 SQL 语句以及索引来提高 MySQL 数据库的运行效率
            • 分页优化:比如
              • 优化方案1:延迟关联。先通过where条件提取出主键,再将该表与原数据表关联,通过主键id提取数据行而不是通过原来的二级索引提取数据行
              • 优化方案2:书签方式说白了就是找到limit第一个参数对应的主键值,再根据这个主键值再去过滤并limit
                select * from table where type = 2 and level = 9 order by id asc limit 190289,10;
                
                select a.* from table a, (select id from table where type = 2 and level = 9 order by id asc limit 190289,10 ) b where a.id = b.id
                
                select * from table where id > (select * from table where type = 2 and level = 9 order by id asc limit 190289, 1) limit 10;
                
                • 1
                • 2
                • 3
                • 4
                • 5
              • 日常做分页需求时,一般会用 limit 实现,但是当偏移量特别大的时候,查询效率就变得低下。当偏移量最大的时候,查询效率就会越低,因为Mysql并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的
                在这里插入图片描述
              • 超大分页怎么处理?【解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可.】
                在这里插入图片描述
                在这里插入图片描述
                • 数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于select * from table where age > 20 limit 1000000,10 这种查询其实也是有可以优化的余地的. 这条语句需要 load1000000 数据然后基本上全部丢弃,只取 10 条当然比较慢. 当时我们 可以修改为select * from table where id in (select id from table where age > 20 limit 1000000,10).这样虽然也 load 了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快
                • 捡田螺的小男孩老师的解决MySQL深分页问题的文章
            • Join优化:MySQL的join语句连接表使用的是nested-loop join算法,这个过程类似于嵌套循环简单来说,就是遍历驱动表(外层表),每读出一行数据,取出连接字段到被驱动表(内层表)里查找满足条件的行,组成结果行。要提升join语句的性能,就要尽可能减少嵌套循环的循环次数
              • join的具体实现看这里,ctrl+F搜索join
              • 一个显著优化方式是对被驱动表的join字段建立索【进行连接操作时,能使用被驱动表的索引】,利用索引能快速匹配到对应的行,避免与内层表每一行记录做比较,极大地减少总循环次数。另一个优化点,就是连接时用小结果集驱动大结果集,在索引优化的基础上能进一步减少嵌套循环的次数
              • 小表做驱动表【小的循环在外层,对于数据库连接来说就只连接5次,进行5000次操作,如果1000在外,则需要进行1000次数据库连接,从而浪费资源,增加消耗。这就是为什么要小表驱动大表。】。如果难以判断哪个是大表,哪个是小表,可以用inner join连接,MySQL会自动选择小表去驱动大表
                • Inner join 、left join、right join,优先使用Inner join【如果inner join是等值连接,或许返回的行数比较少,所以性能相对会好一点。】,如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。【使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少。】
                  • Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
                  • left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
                  • right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
              • 增大 join buffer 的大小,避免使用JOIN关联太多的表:对于 MySQL 来说,是存在关联缓存的,缓存的大小可以由join_buffer_size参数进行设置。在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。如果程序中大量的使用了多表关联的操作,同时join_buffer_size设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性
                在这里插入图片描述
              • 不要用 * 作为查询列表,只返回需要的列
            • 排序优化:利用索引扫描做排序,在设计索引时,尽可能使用同一个索引既满足排序又用于查找行。MySQL有两种方式生成有序结果:其一是对结果集进行排序的操作,其二是按照索引顺序扫描得出的结果自然是有序的。但是如果索引不能覆盖查询所需列,就不得不每扫描一条记录回表查询一次,这个读操作是随机IO,通常会比顺序全表扫描还慢
              --建立索引(date,staff_id,customer_id),只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向都一样时,才能够使用索引来对结果做排序
              select staff_id, customer_id from test where date = '2010-01-01' order by staff_id,customer_id;
              
              • 1
              • 2
            • UNION优化:MySQL处理union的策略是先创建临时表,然后将各个查询结果填充到临时表中最后再来做查询,很多优化策略在union查询中都会失效,因为它无法利用索引。最好手工将where、limit等子句下推到union的各个子查询中,以便优化器可以充分利用这些条件进行优化。此外,除非确实需要服务器去重,一定要使用union all如果不加all关键字,MySQL会给临时表加上distinct选项,这会导致对整个临时表做唯一性检查,代价很高
              在这里插入图片描述
              • 在字段很多的时候慎用distinct关键字:distinct 关键字一般用来过滤重复记录,以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时,给查询带来优化效果。但是在字段很多的时候使用distinct,却会大大降低查询效率。因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较、过滤的过程会占用系统资源,cpu时间。
          • 设计优化:【捡田螺的小男孩有关于数据库表设计经验准则:命名规范、设计表时,我们需要选择合适的字段类型、主键设计要合理,最好是毫无意义的一串独立不重复的数字,比如UUID,又或者Auto_increment自增的主键,或者是雪花算法生成的主键等等、数据库字段长度表示字符长度还是字节长度、优先考虑逻辑删除,而不是物理删除、每个表都需要添加这几个通用字段如主键、create_time、modifed_time 等、区分度不高的字段,不能加索引,如性别、不得使用外键与级联,一切外键概念必须在应用层解决。、数据库库、表、开发程序等都需要统一字符集,通常中英文环境用utf8、通过增加第三张表,把N:N修改为两个 1:N、】
            • 尽量避免使用NULL:NULL在MySQL中不好处理,存储需要额外空间,运算也需要特殊的运算符,含有NULL的列很难进行查询优化。应当指定列为not null,用0、空串或其他特殊的值代替空值,比如定义为int not null default 0
              在这里插入图片描述
              • where子句中考虑使用默认值代替null,并不是说使用了is null 或者 is not null 就会不走索引了,这个跟mysql版本以及查询成本都有关。如果mysql优化器发现,走索引比不走索引成本还要高,肯定会放弃索引,这些条件 !=,>isnull,isnotnull经常被认为让索引失效,其实是因为一般情况下,查询的成本高,优化器自动放弃索引的。如果把null值,换成默认值,很多时候让走索引成为可能,同时,表达意思会相对清晰一点。
                在这里插入图片描述
                • 索引字段上使用is null, is not null,索引可能失效
                  在这里插入图片描述
                • 索引字段上使用(!= 或者 < >),索引可能失效
                  在这里插入图片描述
            • 最小数据长度:越小的数据类型长度通常在磁盘、内存和CPU缓存中都需要更少的空间,处理起来更快
            • 使用最简单数据类型:简单的数据类型操作代价更低,比如:能使用 int 类型就不要使用 varchar 类型,因为 int 类型比 varchar 类型的查询效率更高
            • 尽量少定义 text 类型:text 类型的查询效率很低,如果必须要使用 text 定义字段,可以把此字段分离成子表,需要查询此字段时使用联合查询,这样可以提高主表的查询效率
            • 数据库命令规范要遵守好:因为如果因为命名问题而导致咱们的索引失效,就得不偿失了
              在这里插入图片描述
              • 命名就要做到见名知意,让别人一看命名,就知道这个字段表示什么意思。
                在这里插入图片描述
          • 左右连接,关联的字段编码格式不一样:一个表的name字段编码是utf8mb4,而另一个表的name字段编码为utf8。执行左外连接查询,user_job表还是走全表扫描。如果把它们的name字段改为编码一致,相同的SQL,还是会走索引
          • 常见类型选择:
            • 整数类型宽度设置:MySQL可以为整数类型指定宽度,例如int(11),实际上并没有意义,它并不会限制值的范围,对于存储和计算来说,int(1)和int(20)是相同的
            • VARCHAR和CHAR类型:char类型是定长的,而varchar存储可变字符串,比定长更省空间,但是varchar需要额外1或2个字节记录字符串长度,更新时也容易产生碎片
              • 需要结合使用场景来选择VARCHAR和CHAR类:
                • 如果字符串列最大长度比平均长度大很多,或者列的更新很少,选择varchar较合适
                • 如果要存很短的字符串,或者字符串值长度都相同,比如MD5值,或者列数据经常变更,选择使用char类型
            • DATETIME和TIMESTAMP类型:datetime的范围更大,能表示从1001到9999年,timestamp只能表示从1970年到2038年。datetime与时区无关,timestamp显示值依赖于时区。在大多数场景下,这两种类型都能良好地工作,但是建议使用timestamp,因为datetime占用8个字节,timestamp只占用了4个字节,timestamp空间效率更高
            • BLOB和TEXT类型:blob和text都是为存储很大数据而设计的字符串数据类型,分别采用二进制和字符方式存储。在实际使用中,要慎用这两种类型,它们的查询效率很低,如果字段必须要使用这两种类型,可以把此字段分离成子表,需要查询此字段时使用联合查询,这样可以提高主表的查询效率
              • 对于需要经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询
          • 范式化:当数据较好范式化时,修改的数据更少,而且范式化的表通常要小,可以有更多的数据缓存在内存中,所以执行操作会更快;缺点则是查询时需要更多的关联
            • 第一范式:字段不可分割,数据库默认支持
            • 第二范式:消除对主键的部分依赖,可以在表中加上一个与业务逻辑无关的字段作为主键,比如用自增id
            • 第三范式:消除对主键的传递依赖,可以将表拆分,减少数据冗余
          • 一条 Sql 语句查询偶尔慢会是什么原因?
            在这里插入图片描述
        • 优化方向二:分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引【多数慢SQL都跟索引有关,比如不加索引,索引不生效、不合理等,这时候,我们可以优化索引。】
          在这里插入图片描述
          • 换句话说不就是你,索引没有设计好嘛
            • 可以在这篇文章中ctrl+F搜索一下“索引失效”
              • 索引不宜太多,一般5个以内。
                在这里插入图片描述
            • 假如我们没有添加索引,那么在查询时就会触发全表扫描,因此查询的数据就会很多,并且查询效率会很低,为了提高查询的性能,我们就需要给最常使用的查询字段上,添加相应的索引,这样才能提高查询的性能
            • 建立覆盖索引:InnoDB使用辅助索引查询数据时会回表,但是如果索引的叶节点中已经包含要查询的字段,那它没有必要再回表查询了,这就叫覆盖索引
              select name from test where city='上海'
              //我们将被查询的字段建立到联合索引中,这样查询结果就可以直接从索引中获取
              alter table test add index idx_city_name (city, name);
              
              • 1
              • 2
              • 3
            • 在 MySQL 5.0 之前的版本尽量避免使用or查询:在 MySQL 5.0 之前的版本要尽量避免使用 or 查询,可以使用 union 或者子查询来替代,因为早期的 MySQL 版本使用 or 查询可能会导致索引失效,在 MySQL 5.0 之后的版本中引入了索引合并。索引合并简单来说就是把多条件查询,比如or或and查询对多个索引分别进行条件扫描,然后将它们各自的结果进行合并,因此就不会导致索引失效的问题了。如果从Explain执行计划的type列的值是index_merge可以看出MySQL使用索引合并的方式来执行对表的查询
              • 对应同一列进行 or 判断时,使用 in 代替 or。in 的值不要超过 500 个,in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引
                • in元素过多也会出现问题(分批),如果使用了in,即使后面的条件加了索引,还是要注意in后面的元素不要过多哈。in元素一般建议不要超过200个,如果超过了,建议分组,每次200一组进行哈:select user_id,name from user where user_id in (1,2,3...200);
                  • in查询为什么慢呢?
                    在这里插入图片描述
                • 比如,select user_id,name from user where user_id in (1,2,3...1000000),如果我们对in的条件不做任何限制的话,该查询语句一次性可能会查询出非常多的数据,很容易导致接口超时。尤其有时候,我们是用的子查询,in后面的子查询,你都不知道数量有多少那种,更容易采坑.如下这种子查询:
                  在这里插入图片描述
            • 避免在 where 查询条件中使用 != 或者 <> 操作符:SQL中,不等于操作符会导致查询引擎放弃索引索引,引起全表扫描,即使比较的字段上有索引。所以解决方法就是通过把不等于操作符改成or,可以使用索引,避免全表扫描。例如,把column<>’aaa’,改成column>’aaa’ or column<’aaa’,就可以使用索引了
            • 尽量避免在where子句中使用or来连接条件,使用or可能会使索引失效,从而全表扫描
              在这里插入图片描述
            • 适当使用前缀索引:MySQL 是支持前缀索引的,也就是说我们可以定义字符串的一部分来作为索引。我们知道索引越长占用的磁盘空间就越大,那么在相同数据页中能放下的索引值也就越少,这就意味着搜索索引需要的查询时间也就越长,进而查询的效率就会降低,所以我们可以适当的选择使用前缀索引,以减少空间的占用和提高查询效率;使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。比如,邮箱的后缀都是固定的“@xxx.com”,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引。需要注意的是,前缀索引也存在缺点,MySQL无法利用前缀索引做order by和group by 操作,也无法作为覆盖索引
              alter table test add index index2(email(6));
              
              • 1
              • 对查询进行优化,应考虑在where及order by涉及的列上建立索引,尽量避免全表扫描
            • 不要在列上进行运算操作:不要在列字段上进行算术运算或其他表达式运算,否则可能会导致查询引擎无法正确使用索引,从而影响了查询的效率
              select * from test where id + 1 = 50;
              select * from test where month(updateTime) = 7;
              //一个很容易踩的坑:隐式类型转换:skuId这个字段上有索引,但是explain的结果却显示这条语句会全表扫描,原因在于skuId的字符类型是varchar(32),比较值却是整型,故需要做类型转换
              select * from test where skuId=123456
              
              • 1
              • 2
              • 3
              • 4
              • 隐式转换:
                在这里插入图片描述
            • 正确使用联合索引:使用了 B+ 树的 MySQL 数据库引擎,比如 InnoDB 引擎,在每次查询复合字段【使用联合索引时】时是从左往右匹配数据的,因此在创建联合索引的时候需要注意索引创建的顺序,一般遵循最左匹配原则【联合索引不满足最左原则,索引一般会失效,但是这个还跟Mysql优化器有关的。】当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则
              • 例如,我们创建了一个联合索引是idx(name,age,sex),那么 当我们使用,姓名+年龄+性别、姓名+年龄、姓名等这种最左前缀查询条件时,就会触发联合索引进行查询;然而如果非最左匹配的查询条件,例如,性别+姓名这种查询条件就不会触发联合索引,换句话说就是索引不生效
              • 没有特殊要求时所有表必须使用 InnoDB 存储引擎
            • delete + in子查询不走索引:当delete遇到in子查询时,即使有索引,也是不走索引的。而对应的select + in子查询,却可以走索引
              在这里插入图片描述
              • 为什么select + in子查询会走索引,delete + in子查询却不会走索引呢?
                在这里插入图片描述
        • 优化方向三:如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大如果是的话可以进行横向或者纵向的分表
          • 拆分复杂的大 SQL 为多个小 SQL:
            在这里插入图片描述
          • 适当分表、分库策略
            • 分表是指当一张表中的字段更多时,可以尝试将一张大表拆分为多张子表,把使用比较高频的主信息放入主表中,其他的放入子表,这样我们大部分查询只需要查询字段更少的主表就可以完成了,从而有效的提高了查询的效率
              • 对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢
            • 分库是指将一个数据库分为多个数据库。比如我们把一个数据库拆分为了多个数据库,一个主数据库用于写入和修改数据,其他的用于同步主数据并提供给客户端查询,这样就把一个库的读和写的压力,分摊给了多个库,从而提高了数据库整体的运行效率
            • 或者可以理解成为对表结构的一种优化:
              在这里插入图片描述
          • 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作
            在这里插入图片描述
            • 比如假设数据库是MySQL,数据量在100万以上,分页查询下,每次查询客户表时最多返回 100 条数据时,随着起点值【select * from xxx表 order by id limit 起点值,100】位置越大,分页查询效率成倍的下降,当起点位置在 1000000 以上的时候,对于百万级数据体量的单表,查询耗时基本上以秒为单位。而且很关键的点是 这还只是数据库层面的耗时,还没有算后端服务的处理链路时间,以及返回给前端的数据渲染时间,以百万级的单表查询为例,如果数据库查询耗时 1 秒,再经过后端的数据封装处理,前端的数据渲染处理,以及网络传输时间,没有异常的情况下,差不多在 3~4 秒之间
              • limit深分页为什么会变慢呢? 咱用图中的SQL语句对图中的表进行查询,可以知道SQL语句的执行流程大概如下:
                在这里插入图片描述
                • 这个(select id,name,balance from account where create_time> '2020-09-19' limit 100000,10;)SQL的执行流程是这样的:
                  在这里插入图片描述
              • 用上面的例子来说,limit深分页,导致SQL变慢原因有两个:
                • limit语句会先扫描offset+n行,然后再丢弃掉前offset行,返回后n行数据。也就是说limit 100000,10,就会扫描100010行,而limit 0,10,只扫描10行。【limit 100000,10 扫描更多的行数,也意味着回表更多的次数。】
            • 当单表数据量到达百万级的时候,查询效率急剧下降,如何优化提升呢?提升方法如下:
              在这里插入图片描述
              • 方案一:查询的时候,只返回主键 ID,也就是 将select *改成select id,通过简化返回的字段,可以很显著的成倍提升查询效率。实际的操作思路就是先通过分页查询满足条件的主键 ID,然后通过主键 ID 查询部分数据,可以显著提升查询效果
                在这里插入图片描述
              • 方案二:查询的时候,通过主键 ID 过滤 。这种方案有一个要求就是主键ID,必须是数字类型【但如果当前表的主键 ID 是字符串类型,比如 uuid 这种,就没办法实现这种排序特性,而且搜索性能也非常差,因此不建议大家采用 uuid 作为主键ID,具体的数值类型主键 ID 的生成方案有很多种,比如自增、雪花算法等等,都能很好的满足我们的需求。】实践的思路就是取上一次查询结果的 ID 最大值,作为过滤条件,而且排序字段必须是主键 ID,不然分页排序顺序会错乱。带上主键 ID 作为过滤条件,查询性能非常的稳定,基本上在20 ms内可以返回。这种方案还是非常可行的,如果当前业务对排序要求不多,可以采用这种方案,性能也非常杠
                • 但是如果当前业务对排序有要求,比如通过客户最后修改时间、客户最后下单时间、客户最后下单金额等字段来排序,那么上面介绍的【方案一】,比【方案二】查询效率更高
                • 总的来说,就是通过减少回表次数来优化。一般有标签记录法和延迟关联法。
                  在这里插入图片描述
                  select  acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.create_time > '2020-09-19' limit 100000, 10) AS acct2 on acct1.id= acct2.id;
                  
                  • 1
              • 方案三:采用 elasticSearch 作为搜索引擎。当数据量越来越大的时候,尤其是出现分库分表的数据库,以上通过主键 ID 进行过滤查询,效果可能会不尽人意,例如订单数据的查询,这个时候 比较好的解决办法就是将订单数据存储到 elasticSearch 中,通过 elasticSearch 实现快速分页和搜索,效果提升也是非常明显

感觉其实差不多,咱们在Java中一般测试哪个语句或者哪个算法快不快慢不慢,不就是开始测一下时间并且结束测一下时间,减一下就知道你这个花费了多少时间了呗。一样,咱们MySQL这里也可以测量服务器的时间咋花费的,花在哪里了…在具体一点,就是数据库服务器的性能用查询的响应时间来度量,单位是每个查询花费的时间

  • 完成一项任务所需要的时间可以分成两部分:
    • 执行时间
      • 如果要优化任务的执行时间,最好的办法是通过测量定位不同的子任务花费的时间,然后优化去掉一些子任务、降低子任务的执行频率或者提升子任务的效率
    • 等待时间
      • 而优化任务的等待时间则相对要复杂一些,因为等待有可能是由其他系统间接影响导致,任务之间也可能由于争用磁盘或者CPU资源而相互影响。根据时间是花在执行还是等待上的不同,诊断也需要不同的工具和技术。
        • MySQL数据库cpu飙升到500%的话他怎么处理?
          在这里插入图片描述
          • 当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 MySQLd 占用导致的,如果不是,找出占用高的进程,并进行相关处理
          • 如果是 MySQLd 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成
            • 一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改 sql、改内存参数)之后,再重新跑这些 SQL。
            • 也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等。
              • 不要有超过5个以上的表连接,连表越多,编译的时间和开销也就越大。把连接表拆开成较小的几个执行,可读性更高。如果一定需要连接很多表才能得到数据,那么意味着糟糕的设计了

PART2-1:非关系型数据库优化:以Redis为例

  • Redis如何做内存优化
    在这里插入图片描述
    • Java基基老师关于Redis的异步机制:Redis中有哪些阻塞点以及如何解决?
      • 有哪些影响redis性能的因素,从 Redis 内部及外部因素总结一下,主要有:
        在这里插入图片描述
      • 异步机制解决阻塞
    • Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度
      • 服务器配置有两个和慢查询日志相关的选项:
        • slowlog-log-slower-than选项指定执行时间超过多少微秒(1秒等于1 000 000微秒)的命令请求会被记录到日志上。
        • slowlog-max-len选项指定服务器最多保存多少条慢查询日志
          • 服务器使用先进先出的方式保存多条慢查询日志,当服务器存储的慢查询日志数量等于slowlog-max-len选项的值时,服务器在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除。
      • 慢查询记录的保存:服务器状态中包含了几个和慢查询日志功能有关的属性:
        • Redis服务器将所有的慢查询日志保存在服务器状态的slowlog链表中,每个链表节点都包含一个slowlogEntry结构,每个slowlogEntry结构代表一条慢查询日志
        struct redisServer { 
        	... 
        	//下一条慢查询日志的ID 。slowlog_entry_id属性的初始值为0,每当创建一条新的慢查询日志 时,这个属性的值就会用作新日志的id值,之后程序会对这个属性的值增一。
        	long long slowlog_entry_id; 
        	//保存了所有慢查询日志的链表 。slowlog链表保存了服务器中的所有慢查询日志,链表中的每个节点 都保存了一个slowlogEntry结构,每个slowlogEntry结构代表一条慢查询 日志:
        	list *slowlog; 
        	//服务器配置slowlog-log-slower-than 选项的值
        	long long slowlog_log_slower_than; 
        	// 服务器配置slowlog-max-len选项的值
        	unsigned long slowlog_max_len; 
        	... 
        };
        
        typedef struct slowlogEntry { 
        	//唯一标识符 
        	long long id; 
        	//命令执行时的时间,格式为UNIX 时间戳
        	time_t time; 
        	//执行命令消耗的时间,以微秒为单位 
        	long long duration; 
        	//命令与命令参数 
        	robj **argv; 
        	//命令与命令参数的数量 
        	int argc; 
        } slowlogEntry;
        		```		![在这里插入图片描述](https://img-blog.csdnimg.cn/cd26d5abdcdb4bc8a835c0feb3606d59.png)
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
        • 26
      在这里插入图片描述
      • 慢查询日志的阅览和删除(打印和删除慢查询日志可以通过遍历slowlog链表来完成,slowlog链表的长度就是服务器所保存慢查询日志的数量。):在每次执行命令的之前和之后,程序都会记录微秒格式的当前 UNIX时间戳,这两个时间戳之间的差就是服务器执行命令所耗费的时长,服务器会将这个时长作为参数之一传给slowlogPushEntryIfNeeded函数,而slowlogPushEntryIfNeeded函数则负责检查是否需要为这次执行的命令创建慢查询日志
        • slowlogPushEntryIfNeeded函数的作用有两个:
          • 1)检查命令的执行时长是否超过slowlog-log-slower-than选项所设置的时间,如果是的话,就为命令创建一个新的日志,并将新日志添加到slowlog链表的表头。
          • 2)检查慢查询日志的长度是否超过slowlog-max-len选项所设置的长度,如果是的话,那么将多出来的日志从slowlog链表中删除掉。
        • 新的慢查询日志会被添加到slowlog链表的表头,如果日志的数量超过slowlog-max-len选项的值,那么多出来的日志会被删除。
    • 如何用更少的内存保存更多的数据?
      • 为了保存数据【Redis 以 redisDb为中心存储键值对形式的数组】, Redis 需要先申请内存,数据过期或者内存淘汰需要回收内存,从而拓展出内存碎片优化。
        在这里插入图片描述
        在这里插入图片描述
        • Redis 使用dict结构来保存所有的键值对(key-value)数据,这是一个全局哈希表,所以对 key 的查询能以 O(1) 时间得到。key 的哈希值最终会映射到 ht_table 【ht_table[2]是dict结构中的两个dictEntry类型的全局哈希表指针数组,与渐进式 rehash 有关,dictEntry 类型的ht_table 数组每个位置我们也叫做哈希桶,就是这玩意保存了所有键值对**。】的一个位置,如果发生哈希冲突,则拉出一个哈希链表。
          在这里插入图片描述
        • 哈希桶的每个元素的结构由 dictEntry 定义:这个结构中的key 指向键值对的键的指针,key 都是 string 类型【咱们Redis的key不都是字符串对象嘛,这不刚好对上了】【void *key 和 void *value 指针指向的是 redisObject,Redis 中每个对象都是用 redisObject 表示。】
          在这里插入图片描述
        • 哈希桶并没有保存值本身,而是指向具体值的指针,从而实现了哈希桶能存不同数据类型的需求。哈希桶中,键值对的值都是由一个叫做 redisObject 的对象定义【**void key 和 void value 指针指向的是 redisObject,Redis 中每个对象都是用 redisObject 表示。】
          在这里插入图片描述
      • 具体优化措施:
        • 键值对优化
          • 当我们执行 set key value 的命令,*key指针指向 SDS 字符串保存 key,而 value 的值保存在 *ptr 指针指向的数据结构,消耗的内存:key + value。
          • 降低 Redis 内存使用的最粗暴的方式就是缩减键(key)【对于 key 的命名使用「业务模块名:表名:数据唯一id」这样的方式方便定位问题,所以对于 key 的优化就是使用单词简写方式优化内存占用】与值(value)【value优化的话主要就是:不要大而全的一股脑将所有信息保存,想办法去掉一些不必要的属性,比如缓存登录用户的信息,通常只需要存储昵称、性别、账号等;比如用户的会员类型:0 表示「屌丝」、1 表示 「VIP」、2表示「VVIP」。而不是存储 VIP 这个字符串;对数据的内容进行压缩,比如使用 GZIP、Snappy;使用性能好,内存占用小的序列化方式。比如 Java 内置的序列化不管是速度还是压缩比都不行,我们可以选择 protostuff,kryo等方式】的长度
        • 小数据集合编码优化
          • key 对象都是 string 类型,value 对象主要有五种基本数据类型:String、List、Set、Zset、Hash【同一数据类型会根据键的数量和值的大小也有不同的底层编码类型实现,其中ziplist 压缩列表由 quicklist 代替(3.2 版本引入),而双向链表由 listpack 代替】
        • 数据编码优化技巧
          • ziplist 存储 list 时每个元素会作为一个 entry,存储 hash 时 key 和 value 会作为相邻的两个 entry。存储 zset 时 member 和 score 会作为相邻的两个entry,当不满足上述条件时,ziplist 会升级为 linkedlist, hashtable 或 skiplist 编码。考虑了综合平衡空间碎片和读写性能两个维度所以使用了新编码 quicklist。【由于目前大部分Redis运行的版本都是在3.2以上,所以 List 类型的编码都是quicklist,考虑了综合平衡空间碎片和读写性能两个维度所以使用了新编码 quicklist。】
          • key 尽量控制在 44 字节以内,走 embstr 编码。
          • 集合类型的 value 对象的元素个数不要太多太大,充分利用 ziplist 编码实现内存压缩。
        • 对象共享池
          • 整数我们经常在工作中使用,Redis 在启动的时候默认生成一个 0 ~9999 的整数对象共享池用于对象复用,减少内存占用。【如果 value 可以使用整数表示的话尽可能使用整数,这样即使大量键值对的 value 大量保存了 0~9999 范围内的整数,在实例中,其实只有一份数据。】
            • Redis 中设置了 maxmemory 限制最大内存占用大小且启用了 LRU 策略(allkeys-lru 或 volatile-lru 策略),它会导致对象共享池失效【因为 LRU 需要记录每个键值对的访问时间,都共享一个整数 对象,LRU 策略就无法进行统计了。】
            • 集合类型的编码采用 ziplist 编码,并且集合内容是整数,也不能共享一个整数对象,它会导致对象共享池失效【使用了 ziplist 紧凑型内存结构存储数据,判断整数对象是否共享的效率很低。】
        • 使用 Bit 比特位或 byte 级别操作
          • 比如在一些二值状态统计【集合中的元素的值只有 0 和 1 两种,在签到打卡和用户是否登陆的场景中,只需记录签到(1)或 未签到(0),已登录(1)或未登陆(0)。】的场景下使用 Bitmap 实现,对于网页 UV 使用 HyperLogLog 来实现,大大减少内存占用。假如我们在判断用户是否登陆的场景中使用 Redis 的 String 类型实现(key -> userId,value -> 0 表示下线,1 - 登陆),假如存储 100 万个用户的登陆状态,如果以字符串的形式存储,就需要存储 100 万个字符串,内存开销太大【String 类型除了记录实际数据以外,还需要额外的内存记录数据长度、空间使用等信息
          • Bitmap 的底层数据结构用的是 String 类型的 SDS 数据结构来保存位数组,Redis 把每个字节数组的 8 个 bit 位利用起来,每个 bit 位 表示一个元素的二值状态(不是 0 就是 1)。8 个 bit 组成一个 Byte,所以 Bitmap 会极大地节省存储空间。 这就是 Bitmap 的优势。
        • 用 Hash 类型优化【用 Hash 类型的话,每个用户只需要设置一个 key。】
          • 尽可能把数据抽象到一个哈希表里。比如说系统中有一个用户对象,我们不需要为一个用户的昵称、姓名、邮箱、地址等单独设置一个 key,而是将这个信息存放在一个哈希表里
          • 使用 String 类型,为每个属性设置一个 key 会占用大量内存【因为 Redis 的数据类型有很多,不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等)。所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,用 *prt 指针指向实际数据。当我们为每个属性都创建 key,就会创建大量的 redisObejct 对象占用内存。】
        • 内存碎片优化
          • 碎片优化可以降低内存使用率,提高访问效率,在4.0以下版本,我们只能使用重启恢复:重启加载 RDB 或者通过高可用主从切换实现数据的重新加载减少碎片。在4.0以上版本,Redis提供了自动和手动的碎片整理功能,原理大致是把数据拷贝到新的内存空间,然后把老的空间释放掉,这个是有一定的性能损耗的。因为 Redis 是单线程,在数据拷贝时,Redis 只能等着,这就导致 Redis 无法处理请求,性能就会降低。】
            • 手动整理碎片:执行 memory purge命令即可
            • 自动整理内存碎片:使用 config set activedefrag yes 指令或者在 redis.conf 配置 activedefrag yes 将 activedefrag 配置成 yes 表示启动自动清理功能。
              在这里插入图片描述
          • Redis 释放的内存空间可能并不是连续的,这些不连续的内存空间很有可能处于一种闲置的状态。虽然有空闲空间,Redis 却无法用来保存数据,不仅会减少 Redis 能够实际保存的数据量,还会降低 Redis 运行机器的成本回报率。比如, Redis 存储一个整形数字集合需要一块占用 32 字节的连续内存空间,当前虽然有 64 字节的空闲,但是他们都是不连续的,导致无法保存。
            • 两个层面原因导致内存碎片的形成
              • 操作系统内存分配机制:内存分配策略决定了无法做到按需分配。因为分配器是按照固定大小来分配内存。
              • 键值对被修改和删除,从而导致内存空间的扩容和释放
        • 使用 32 位的 Redis
          • 使用32位的redis,对于每一个key,将使用更少的内存,因为32位程序,指针占用的字节数更少。但是32的Redis整个实例使用的内存将被限制在4G以下。我们可以通过 cluster 模式将多个小内存节点构成一个集群,从而保存更多的数据。另外小内存的节点 fork 生成 rdb 的速度也更快。RDB和AOF文件是不区分32位和64位的(包括字节顺序),所以你可以使用64位的Redis 恢复32位的RDB备份文件,相反亦然。

PART2-2:引入了缓存之后,就得考虑缓存和数据库数据不一致问题,如何解决?

  • 由于引入了缓存,那么在数据更新时,不仅要更新数据库,而且要更新缓存,这两个更新操作存在前后的问题:【无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象。】
    在这里插入图片描述
    • 先更新数据库,再更新缓存
      • 容易出现数据库和缓存的数据不一致的问题,造成缓存和数据库的数据不一致的现象,是因为并发问题【比如请求 A 和请求 B 两个请求,同时更新同一条数据,则可能出现这样的顺序:A 请求先将数据库的数据更新为 1,然后在更新缓存前,请求 B手来的快,将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1。此时,数据库中的数据是 2,而缓存中的数据却是 1,出现了缓存和数据库中的数据不一致的现象
    • 先更新缓存,再更新数据库
      • 依然还是存在并发的问题【比如请求 A 和请求 B 两个请求,同时更新同一条数据,则可能出现这样的顺序:A 请求先将缓存的数据更新为 1,然后在更新数据库前,B 请求来了, 将缓存的数据更新为 2,紧接着把数据库更新为 2,然后 A 请求将数据库的数据更新为 1。此时,数据库中的数据是 1,而缓存中的数据却是 2,出现了缓存和数据库中的数据不一致的现象。】
    • Cache Aside 策略,中文是叫旁路缓存策略:先更新数据库,然后直接删除缓存:这个策略是有名字的,是叫 Cache Aside 策略,中文是叫旁路缓存策略
      • 在更新数据时,不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中
        • Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
      • 这个Cache Aside策略(旁路缓存策略)可以细分为读策略和写策略
        • 写策略:那是“先删除缓存,再更新数据库”还是“先更新数据库,再删除缓存”呢【先更新数据库中的数据,然后删除缓存中的数据】
          • 先删除缓存,再更新数据库:假设某个用户的年龄是 20,请求 A 要更新用户年龄为 21,所以它会删除缓存中的内容。这时,另一个请求 B 要读取这个用户的年龄,它查询缓存发现未命中后,会从数据库中读取到年龄为 20,并且写入到缓存中,然后请求 A 继续更改数据库,将用户的年龄更新为 21。最终,该用户年龄在缓存中是 20(旧值),在数据库中是 21(新值),缓存和数据库的数据不一致。先删除缓存,再更新数据库,在「读 + 写」并发的时候,还是会出现缓存和数据库的数据不一致的问题
            • 请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 db 中读取数据->请求 1 再把 db 中的 A 数据更新,会出问题。
          • 先更新数据库,再删除缓存【先更新数据库 + 再删除缓存的方案,是可以保证数据一致性的。此外,为了确保万无一失,我们还可以给缓存数据加上过期时间,就算在这期间存在缓存数据不一致,有过期时间来兜底,这样也能达到最终一致。】:假如某个用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求 A 把从数据库中读到的年龄为 20 的数据写入到缓存中。最终,该用户年龄在缓存中是 20(旧值),在数据库中是 21(新值),缓存和数据库数据不一致。先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高【请求 1 从 db 读数据 A-> 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 ) -> 请求 1 将数据 A 写入 cache】
            • 先更新数据库, 再删除缓存其实是两个操作,前面的所有分析都是建立在这两个操作都能同时执行成功,而这次客户投诉的问题就在于,在删除缓存(第二个操作)的时候失败了,导致缓存中的数据是旧值。【如果在之前给缓存加上了过期时间,会出现过一段时间才更新生效的现象,假设如果没有这个过期时间的兜底,那后续的请求读到的就会一直是缓存中的旧数据(举个例子,比如应用要把数据 X 的值从 1 更新为 2,先成功更新了数据库,然后在 Redis 缓存中删除 X 的缓存,但是这个操作却失败了,这个时候数据库中 X 的新值为 2,Redis 中的 X 的缓存值为 1,出现了数据库和缓存数据不一致的问题。后续有访问数据 X 的请求,会先在 Redis 中查询,因为缓存并没有 诶删除,所以会缓存命中,但是读到的却是旧值 1。),这样问题就更大了
              • 如何保证「先更新数据库 ,再删除缓存」这两个操作能执行成功?其实不管是先操作数据库,还是先操作缓存,只要第二个操作失败都会出现数据一致的问题。有两种解决办法
                • 重试机制:引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了;如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试【增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。】
                • 订阅 MySQL binlog,再操作缓存:「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么**更新数据库成功,就会产生一条变更日志,记录在 binlog 里我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的**。
                  在这里插入图片描述
            • 先更新数据库,再删除缓存的方案虽然保证了数据库与缓存的数据一致性,但是每次更新数据的时候,缓存的数据都会被删除,这样会对缓存的命中率带来影响所以,如果我们的业务对缓存命中率有很高的要求,我们可以采用更新数据库 + 更新缓存的方案,因为更新缓存并不会出现缓存未命中的情况。这个方案在两个更新请求并发执行的时候,会出现数据不一致的问题,因为更新数据库和更新缓存这两个操作是独立的,而我们又没有对操作做任何并发控制,那么当两个线程并发更新它们的话,就会因为写入顺序的不同造成数据的不一致。所以我们得增加一些手段来解决这个问题,这里提供两种做法:
              • 在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
              • 在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。
        • 读策略
          • 如果读取的数据命中了缓存,则直接返回数据
          • 如果读取的数据 没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户
      • 缓存常用的3种读写策略:【三种模式各有优劣,不存在最佳模式,根据具体的业务场景选择适合自己的缓存读写模式
        • Cache Aside Pattern(旁路缓存模式):细节看上面去
          • Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景
          • Cache Aside Pattern(旁路缓存模式)缺点:
            • 缺陷 1:首次请求数据一定不在 cache 的问题
              • 解决办法:可以将热点数据可以提前放入 cache 中
            • 缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。
              • 数据库和缓存数据强一致场景 :更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题
              • 可以短暂地允许数据库和缓存数据不一致的场景 :更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小
        • Read/Write Through Pattern(读写穿透)
          • Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责
          • 这种缓存读写策略在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能
            • Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的
          • 写(Write Through):先查 cache,cache 中不存在,直接更新 db。cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。
          • 读(Read Through):从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 db 加载,写入到 cache 后返回响应。
        • Write Behind Pattern(异步缓存写入)
          • Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写
            • 但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。很明显,这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉
            • 这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。
          • Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量

PART3:不仅仅局限于代码层面的性能优化,性能优化通常是“时间”与“空间”的互换与取舍。

  • 关于性能方面,高性能软件系统也意味着更高的实现成本,有时候与其他质量属性甚至会冲突。,比如安全性、可扩展性、可观测性等等。鱼和熊掌不可得兼哦。所以大部分时候我们需要的是:在业务遇到瓶颈之前,利用常见的技术手段将系统优化到预期水平,。从以下几个方面来说:上面关系型数据库和非关系型数据库中也已经提到了一部分嘞。也算是对上面的一个总结回顾咯
    • 六种通用的“时间”与“空间”互换取舍的手段:
      • 索引:索引的原理是拿额外的存储空间换取查询时间增加了写入数据的开销,但使读取数据的时间复杂度一般从O(n)降低到O(logn)甚至O(1)
        • 索引不仅在数据库中广泛使用,前后端的开发中也在不知不觉运用。
        • 软件世界常见的索引有哪些数据结构,分别在什么场景使用呢?
          在这里插入图片描述
        • 数据库主键之争:自增长 vs UUID
          • 主键是很多数据库非常重要的索引,尤其是MySQL这样的RDBMS会经常面临这个难题:是用自增长的ID还是随机的UUID做主键?【自增长ID的性能最高但不好做分库分表后的全局唯一ID,自增长的规律可能泄露业务信息;而UUID不具有可读性且太占存储空间。】争执的结果就是找 一个兼具二者的优点的折衷方案用雪花算法生成分布式环境全局唯一的ID作为业务表主键,性能尚可、不那么占存储、又能保证全局单调递增,但引入了额外的复杂性,再次体现了取舍之道
      • 压缩:一个“时间换空间”的办法。压缩的原理消耗计算的时间,换一种更紧凑的编码方式来表示数据
        • 我们在代码中通常用的是无损压缩
          在这里插入图片描述
      • 缓存:缓存优化性能的原理和索引一样,是拿额外的存储空间换取查询时间
        • 我们在浏览器打开一篇文章,会有多少层缓存呢?
          在这里插入图片描述
        • Phil Karlton 曾说过:计算机科学中只有两件困难的事情:缓存失效和命名规范
          • 缓存的使用除了带来额外的复杂度以外,还面临如何处理缓存失效的问题。
            在这里插入图片描述
          • 除了通常意义上的缓存外,对象重用的池化技术,也可以看作是一种缓存的变体【在需要某个资源时从现有的池子里直接拿一个,稍作修改或直接用于另外的用途,池化重用也是性能优化常见手段】。常见的诸如JVM,V8这类运行时的常量池、数据库连接池、HTTP连接池、线程池、Golang的sync.Pool对象池等等。
      • 预取:预取通常搭配缓存一起用,其原理是在缓存空间换时间基础上更进一步,再加上一次“时间换时间”,也就是:用事先预取的耗时,换取第一次加载的时间当可以猜测出以后的某个时间很有可能会用到某种数据时,把数据预先取到需要用的地方,能大幅度提升用户体验或服务端响应速度【是否用预取模式就像自助餐餐厅与厨师现做的区别,在自助餐餐厅可以直接拿做好的菜品,一般餐厅需要坐下来等菜品现做。】
        • 预取在哪些实际场景会用呢?
          在这里插入图片描述
        • 预取也是有副作用的。正如烤箱预热需要消耗时间和额外的电费,在软件代码中做预取/预热的副作用通常是启动慢一些、占用一些闲时的计算资源、可能取到的不一定是后面需要的
      • 削峰填谷:是“时间换时间”,谷时换峰时。峰填谷与预取是反过来的:预取是事先花时间做,削峰填谷是事后花时间做就像三峡大坝可以抗住短期巨量洪水,事后雨停再慢慢开闸防水。软件世界的“削峰填谷”是类似的,只是不是用三峡大坝实现,而是用消息队列、异步化等方式
        • 常见的有这几类问题,我们分别来看每种对应的解决方案:
          在这里插入图片描述
      • 批量处理:可以看成“时间换时间”,其原理是减少了重复的事情,是一种对执行流程的压缩。以个别批量操作更长的耗时为代价,在整体上换取了更多的时间。【如果插入数据过多,考虑批量插入。】
        在这里插入图片描述
        • 多大一批可以确保单批响应时间不太长的同时让整体性能最高,是需要在实际情况下 做基准测试的,不能一概而论。而批量处理的副作用在于:处理逻辑会更加复杂,尤其是一些涉及事务、并发的问题;需要用数组或队列用来存放缓冲一批数据,消耗了额外的存储空间
          在这里插入图片描述
          在这里插入图片描述
    • 四种与提升并行能力有关:
      • 榨干计算资源
      • 水平扩容
      • 分片
      • 无锁

巨人的肩膀:
高性能mysql
mysql技术内幕
月伴飞鱼
mysql中文文档
码哥字节
javaguide
捡田螺的小男孩
程序员田螺
微观技术老师关于性能优化的全面的文章
捡田螺的小男孩老师的关于书写高质量SQL的30条建议
捡田螺的小男孩老师的设计MySQL表的经验准则
Java极客技术:作者鸭血粉丝Tang老师

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/557990
推荐阅读
相关标签
  

闽ICP备14008679号