后端接口性能优化分析-数据库优化(下)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 后端接口性能优化分析-数据库优化

后端接口性能优化分析-数据库优化(中):https://developer.aliyun.com/article/1413678


Inner join 、left join、right join


Inner join 、left join、right join,优先使用Inner join,如果是left join,左边表结果尽量小


  • Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
  • left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
  • right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。


在SQL查询中,Inner join、Left join和Right join是三种主要的连接方式。它们的优先级并没有固定的规定,但在很多情况下,我们首选Inner join。原因如下:


  1. 结果集较小:Inner join返回的是两个表中共同拥有的记录,因此结果集相对较小,查询效率较高。而Left join和Right join会返回两个表中所有的记录,结果集较大,对于大数据量的查询,性能会受到影响。
  2. 准确性:Inner join只返回两个表中确实存在的记录,避免了可能出现的重复数据或错误数据。而Left join和Right join可能会返回一个表中的所有记录,即使它们在另一个表中没有匹配的记录,这可能会导致数据不一致或错误。
  3. 易于理解和维护:Inner join是最简单和直观的连接方式,容易理解和实现。相比之下,Left join和Right join在某些情况下可能需要额外的处理或解释,增加了代码的复杂性和维护成本。当然,这并不是说Inner join在所有情况下都是最佳选择。


根据实际需求和数据特点,Left join和Right join在某些场景下可能更合适。例如,当需要查询一个表中的所有记录,以及对另一个表进行关联查询时,可以使用Left join;而在需要查询一个表中的特定记录,以及对另一个表进行关联查询时,可以使用Right join。总之,要根据具体需求和数据特点来选择合适的连接方式。


都满足SQL需求的前提下,推荐优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。


反例:

select * from tab1 t1 left join tab2 t2  on t1.size = t2.size where t1.id > 2;

正例:

select * from (select * from tab1 where id > 2) t1 left join tab2 t2 on t1.size = t2.size;


在适当的时候,使用覆盖索引


覆盖索引能够使得你的SQL语句不需要回表,仅仅访问索引就能够得到所有需要的数据,大大提高了查询效率。


慎用distinct关键字


distinct 关键字一般用来过滤重复记录,以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时,给查询带来优化效果。但是在字段很多的时候使用,却会大大降低查询效率。


反例:

SELECT DISTINCT * from user;

正例:

select DISTINCT name from user;

理由:


  • 带distinct的语句cpu时间和占用时间都高于不带distinct的语句。因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较、过滤的过程会占用系统资源,cpu时间。


删除冗余和重复索引


反例:

KEY `idx_userId`(`userId`)
KEY `idx_userId_age`(`userId`,`age`)

正例:

//删除userId索引,因为组合索引(A,B)相当于创建了(A)和(A,B)索引
KEY `idx_userId_age`(`userId`,`age`)

理由:


  • 重复的索引需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能的。


where子句中考虑使用默认值代替null


select * from user where age is not null

正例:

//设置0为默认值
select * from user where age>0;

理由:


  • 并不是说使用了is null 或者 is not null 就会不走索引了,这个跟mysql版本以及查询成本都有关。

如果mysql优化器发现,走索引比不走索引成本还要高,肯定会放弃索引,这些条件 !=,>isnull,isnotnull经常被认为让索引失效,其实是因为一般情况下,查询的成本高,优化器自动放弃索引的。


如果把null值,换成默认值,很多时候让走索引成为可能,同时,表达意思会相对清晰一点。


exist&in的合理利用


假设表A表示某企业的员工表,表B表示部门表,查询所有部门的所有员工,很容易有以下SQL:

select * from A where deptId in (select deptId from B);

这样写等价于:

先查询部门表B
select deptId from B
再由部门deptId,查询A的员工
select * from A where A.deptId = B.deptId

可以抽象成这样的一个循环:

List<> resultSet;
for(int i = 0; i < B.length ; i++){
    for(int j = 0; j < A.length ; j++){
        if(A[i].id == B[j].id){
            resultSet.add(A[i]);
            break;
        }
    }
}

显然,除了使用in,我们也可以用exists实现一样的查询功能,如下:

select * from A where exists (select 1 from B where A.deptId = B.deptId);

因为exists查询的理解就是,先执行主查询,获得数据后,再放到子查询中做条件验证,根据验证结果(true或者false),来决定主查询的数据结果是否得意保留。


那么,这样写就等价于:

select * from A,先从A表做循环
select * from B where A.deptId = B.deptId,再从B表做循环.

同理,可以抽象成这样一个循环:

List<> resultSet;
for(int i = 0; i < A.length ; i++){
    for(int j = 0; j < B.length ; j++){
        if(A[i].id == B[j].id){
            resultSet.add(A[i]);
            break;
        }
    }
}

数据库最费劲的就是跟程序链接释放。假设链接了两次,每次做上百万次的数据集查询,查完就走,这样就只做了两次;相反建立了上百万次链接,申请链接释放反复重复,这样系统就受不了了。即mysql优化原则,就是小表驱动大表,小的数据集驱动大的数据集,从而让性能更优。


因此,我们要选择最外层循环小的,也就是,如果B的数据量小于A,适合使用in,如果B的数据量大于A,即适合选择exist。


索引不适合建在有大量重复数据的字段上,如性别这类型数据库字段。


因为SQL优化器是根据表中数据量来进行查询优化的,如果索引列有大量重复数据,Mysql查询优化器推算发现不走索引的成本更低,很可能就放弃索引了。


深分页


limit深分页问题,会导致慢查询。


limit深分页为什么会变慢


limit深分页为什么会导致SQL变慢呢?假设我们有表结构如下:

CREATE TABLE account (
  id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
  name varchar(255) DEFAULT NULL COMMENT '账户名',
  balance int(11) DEFAULT NULL COMMENT '余额',
  create_time datetime NOT NULL COMMENT '创建时间',
  update_time datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (id),
  KEY idx_name (name),
  KEY idx_create_time (create_time) //索引
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='账户表';

你知道以下SQL,执行过程是怎样的嘛?

select id,name,balance from account where create_time> '2020-09-19' limit 100000,10;

这个SQL的执行流程:


  1. 通过普通二级索引树idx_create_time,过滤create_time条件,找到满足条件的主键id
  2. 通过主键id,回到id主键索引树,找到满足记录的行,然后取出需要展示的列(回表过程)
  3. 扫描满足条件的100010行,然后扔掉前100000行,返回。

limit深分页,导致SQL变慢原因有两个:


  • limit语句会先扫描offset+n行,然后再丢弃掉前offset行,返回后n行数据。也就是说limit 100000,10,就会扫描100010行,而limit 0,10,只扫描10行。
  • limit 100000,10 扫描更多的行数,也意味着回表更多的次数


如何优化深分页问题?


我们可以通过减少回表次数来优化。一般有标签记录法和延迟关联法


标签记录法


就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。就好像看书一样,上次看到哪里了,你就折叠一下或者夹个书签,下次来看的时候,直接就翻到啦。


假设上一次记录到100000,则SQL可以修改为:

select  id,name,balance FROM account where id > 100000 limit 10;

这样的话,后面无论翻多少页,性能都会不错的,因为命中了id索引。但是这种方式有局限性:需要一种类似连续自增的字段。


延迟关联法


延迟关联法,就是把条件转移到主键索引树,然后减少回表。如下:

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;

优化思路就是,先通过idx_create_time二级索引树查询到满足条件的主键ID,再与原表通过主键ID内连接,这样后面直接走了主键索引了,同时也减少了回表。


大事务


为了保证数据库数据的一致性,在涉及到多个数据库修改操作时,我们经常需要用到事务。而使用spring声明式事务,又非常简单,只需要用一个注解就行@Transactional,如下面的例子:

@Transactional
public int createUser(User user){
    //保存用户信息
    userDao.save(user);
    passCertDao.updateFlag(user.getPassId());
    return user.getUserId();
}

这块代码主要逻辑就是创建个用户,然后更新一个通行证pass的标记。如果现在新增一个需求,创建完用户,调用远程接口发送一个email消息通知,很多小伙伴会这么写:

@Transactional
public int createUser(User user){
    //保存用户信息
    userDao.save(user);
    passCertDao.updateFlag(user.getPassId());
    sendEmailRpc(user.getEmail());
    return user.getUserId();
}

这样实现可能会有坑,事务中嵌套RPC远程调用,即事务嵌套了一些非DB操作。如果这些非DB操作耗时比较大的话,可能会出现大事务问题。


所谓大事务问题就是,就是运行时间长的事务。由于事务一致不提交,就会导致数据库连接被占用,即并发场景下,数据库连接池被占满,影响到别的请求访问数据库,影响别的接口性能。


大事务引发的问题主要有:接口超时、死锁、主从延迟等等。因此,为了优化接口,我们要规避大事务问题。我们可以通过这些方案来规避大事务:


  • RPC远程调用不要放到事务里面
  • 一些查询相关的操作,尽量放到事务之外
  • 事务中避免处理太多数据


海量数据


MySQL 换 ElasticSearch


在后台管理页面中,通常需要对列表页进行多条件检索。MySQL 无法满足多条件检索的需求,原因有两点。第一点是,拼接条件检索的查询SQL非常复杂且需要进行定制化,难以进行维护和管理。第二点是,条件检索的查询场景非常灵活,很难设计合适的索引来提高查询性能,并且难以保证查询能够命中索引。


相比之下,ElasticSearch是一种天然适合于条件检索场景的解决方案。无论数据量的大小,对于列表页查询和检索等场景,推荐首选ElasticSearch。


可以将多个表的数据异构到ElasticSearch中建立宽表,并在数据更新时同步更新索引。在进行检索时,可以直接从ElasticSearch中获取数据,无需再查询数据库,提高了检索性能。


MySQL 换 HBase


MySQL并不适合大数据量存储,若不对数据进行归档,数据库会一直膨胀,从而降低查询和写入的性能。针对大数据量的读写需求,可以考虑以下方法来存储订单数据。


首先,将最近1年的订单数据存储在MySQL数据库中。这样可以保证较高的数据库查询性能,因为MySQL对于相对较小的数据集来说是非常高效的。


其次,将1年以上的历史订单数据进行归档,并将这些数据异构(转储)到HBase中。HBase是一种分布式的NoSQL数据库,可以存储海量数据,并提供快速的读取能力。


在订单查询接口上,可以区分近期数据和历史数据,使得上游系统能够根据自身的需求调用适当的订单接口来查询订单详情。


在将历史订单数据存储到HBase时,可以设置合理的RowKey。RowKey是HBase中数据的唯一标识,在查询过程中可以通过RowKey来快速找到目标数据。通过合理地设置RowKey,可以进一步提高HBase的查询性能。


通过将订单数据分别存储在MySQL和HBase中,并根据需求进行区分查询,可以满足大数据量场景的读写需求。MySQL用于存储近期数据,以保证查询性能;而HBase用于存储归档的历史数据,并通过合理设置的RowKey来提高查询性能。


冗余设计


通过冗余更多的数据,我们可以提高查询性能,这是常见的优化方案。除了引入新的表外,还可以在表中冗余其他表的字段,以减少关联查询的次数。


历史数据归档


MySQL并不适合存储大数据量,如果不对数据进行归档,数据库会持续膨胀,从而降低查询和写入的性能。为了满足大数据量的读写需求,需要定期对数据库进行归档。


在进行数据库设计时,需要事先考虑到对数据归档的需求,为了提高归档效率,可以使用ctime(创建时间)进行归档,例如归档一年前的数据。


在将数据库数据归档之前,如果有必要,一定要将数据同步到Hive中,这样以后如果需要进行统计查询,可以使用Hive中的数据。如果归档的数据还需要在线查询,可以将过期数据同步到HBase中,这样数据库可以提供近期数据的查询,而HBase可以提供历史数据的查询。可参考上述MySQL转HBase的内容。


分库分表


单表数据量太大


一个表的数据量达到好几千万或者上亿时,加索引的效果没那么明显啦。性能之所以会变差,是因为维护索引的B+树结构层级变得更高了,查询一条数据时,需要经历的磁盘IO变多,因此查询性能变慢。


一棵B+树可以存多少数据量


大家是否还记得,一个B+树大概可以存放多少数据量呢?


InnoDB存储引擎最小储存单元是页,一页大小就是16k


B+树叶子存的是数据,内部节点存的是键值+指针。索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进而再去数据页中找到需要的数据;

假设B+树的高度为2的话,即有一个根结点和若干个叶子结点。这棵B+树的存放总记录数为=根结点指针数*单个叶子节点记录行数。


  • 如果一行记录的数据大小为1k,那么单个叶子节点可以存的记录数 =16k/1k =16.
  • 非叶子节点内存放多少指针呢?我们假设主键ID为bigint类型,长度为8字节(面试官问你int类型,一个int就是32位,4字节),而指针大小在InnoDB源码中设置为6字节,所以就是8+6=14字节,16k/14B =16*1024B/14B = 1170


因此,一棵高度为2的B+树,能存放1170 * 16=18720条这样的数据记录。同理一棵高度为3的B+树,能存放1170 *1170 *16 =21902400,也就是说,可以存放两千万左右的记录。B+树高度一般为1-3层,已经满足千万级别的数据存储。


如果B+树想存储更多的数据,那树结构层级就会更高,查询一条数据时,需要经历的磁盘IO变多,因此查询性能变慢。


如何解决单表数据量太大,查询变慢的问题


一般超过千万级别,我们可以考虑分库分表了。


分库分表可能导致的问题:


多表联查问题


之前在库中只存在一张表,所以非常轻松的就能进行联表查询获取数据,但是此时做了水平分表后,同一张业务的表存在多张小表,这时再去连表查询时具体该连接哪张呢?


①如果分表数量是固定的,直接对所有表进行连接查询,但这样性能开销较大,还不如不分表。

②如果不想用①,或分表数量会随时间不断变多,那就先根据分表规则,去确定要连接哪张表后再查询。


第一条好理解,第二条是啥意思呢?好比现在是按月份来分表,那在连表查询前,就先确定要连接哪几张月份的表,才能得到自己所需的数据,确定了之后再去查询对应表即可


聚合操作


之前因为只有一张表,所以进行sum()、count()....、order by、gorup by....等各类聚合操作时,可以直接基于单表完成,但此刻似乎行不通了呀?对于这类聚合操作的解决方案如下:


  • ①放入第三方中间件中,然后依赖于第三方中间件完成,如ES
  • ②定期跑脚本查询出一些常用的聚合数据,然后放入Redis缓存中,后续从Redis中获取。
  • ③首先从所有表中统计出各自的数据,然后在Java中作聚合操作。


前面两种操作比较好理解,第三种方案是什么意思呢?比如count()函数,就是对所有表进行统计查询,最后在Java中求和,好比分组、排序等工作,先从所有表查询出符合条件的数据,然后在Java中通过Stream流进行处理。


上述这三种方案都是比较合理且常规的方案,最好是选择第一种,这也是一些大企业选用的方案。


跨库join问题


因为将不同业务的表拆分到了不同的库中,而往往有些情况下可能会需要其他业务的表数据,在单库时直接join连表查询相应字段数据即可,但此时已经将不同的业务表放到不同库了,这时咋办?


  • ①在不同的库需要数据的表中冗余字段,把常用的字段放到需要要数据的表中,避免跨库连表。
  • Java系统中组装数据,通过调用对方服务接口的形式获取数据,然后在程序中组装后返回。


往往垂直分库的场景中,第二种方案是最常用的,因为分库分表的项目中,Java业务系统那边也绝对采用了分布式架构,因此通过调用对端API接口来获取数据,是分布式系统最为常见的一种现象。


分布式事务问题


分布式事务应该是分布式系统中最核心的一个问题,这个问题绝对不能出现,一般都要求零容忍,也就是所有分布式系统都必须要解决分布式事务问题,否则就有可能造成数据不一致性。


在之前单机的MySQL中,数据库自身提供了完善的事务管理机制,通过begin、commit/rollback的命令可以灵活的控制事务的提交和回滚,在Spring要对一组SQL操作使用事务时,也只需在对应的业务方法上加一个@Transactional注解即可,但这种情况在分布式系统中就不行了。


为什么说MySQL的事务机制会在分布式系统下失效呢?因为InnoDB的事务机制是建立在Undo-log日志的基础上完成的,以前只有一个Undo-log日志,所以一个事务的所有变更前的数据,都可以记录在同一个Undo-log日志中,当需要回滚时就直接用Undo-log中的旧数据覆盖变更过的新数据即可。


但垂直分库之后,会存在多个MySQL节点,这自然也就会存在多个Undo-log日志,不同库的变更操作会记录在各自的Undo-log日志中,当某个操作执行失败需要回滚时,仅能够回滚自身库变更过的数据,对于其他库的事务回滚权,当前节点是不具备该能力的,所以此时就必须要出现一个事务管理者来介入,从而解决分布式事务问题。


其中常用的可能有 Seata 和 最终一致性事务模式 的方案。


Seata


是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。


整体的事务逻辑是两阶段提交的模型,主要由三个重要的组件组成:


  1. TC:Transaction Coordinator 事务协调器,管理全局的分支事务的状态,用于全局性事务的提交和回滚。
  2. TM:Transaction Manager 事务管理器,用于开启、提交或者回滚【全局事务】。
  3. RM:Resource Manager 资源管理器,用于分支(即每一个微服务,它是嵌在服务中的)事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚分支事务


  • 传统XA协议实现2PC方案的 RM 是在数据库层,RM本质上就是数据库自身;
  • Seata的RM是以jar包的形式嵌入在应用程序里面


TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端


Seata事务处理


AT模式


该模式适合的场景:


  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

生命周期描述:


  • A服务的TM TC 申请一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
  • A服务的RMTC注册分支事务
  • A服务执行分支事务,对数据库做操作
  • A服务开始远程调用B服务,并把XID 在微服务调用链路的上下文中传播。
  • A服务会调用B服务,形成调用链接,这也是分布式事务形成的由来。
  • B服务的RMTC注册分支事务,并将其纳入XID对应的全局事务的管辖。
  • B服务执行分支事务,向数据库做操作
  • B服务又开始调用C服务,又形成一个调用链,这样ABC三个服务形成调用链。
  • C服务的RM也向TC注册分支事务,并将其XID纳入全局事务管理中,这样TC会把A、B、C服务串联起来,保证在一个事务里管理。
  • 全局事务调用链处理完毕,TM 根据有无异常向 TC 发起针对 XID 的全局提交(Commit)或回滚(Rollback)决议。
  • TC 调度 XID 下管辖的全部分支事务完成提交(Commit)或回滚(Rollback)请求。


但是分布式事务还是存在一个问题就是,微服务场景下,配置了统一全局异常处理,导致seata在AT模式下无法正常回滚问题。


原因:服务A调用服务B, 服务B发生异常,由于全局异常处理的存在@ControllerAdvice, seata 无法拦截到B服务的异常,从而导致分布式事务未生效。


解决思路:配置了全局异常处理,所以rpc一定会有返回值, 所以在每个全局事务方法最后, 需要判断rpc是否发生异常发生异常则抛出 RuntimeException


场景实例


简单的用户下单场景,4个子工程分别是**Bussiness** (事务发起者)、 **Order** (创建订单) 、**Product** (扣减库存) 和 **Account** (扣减账户余额)。


下图中黄色区域理解为各自独立的微服务被TC纳入了全局事务管理中,整个流程变成了一个原子操作。

用户进行下单,需要进行三个业务调用,Bussiness表示的是业务的发起方,也是我们的一个服务,发起了一个全局事务TM,接着调用商品服务开始扣减库存,Bussiness在调用订单服务,订单服务又会调用账户服务。此时假设商品服务扣减库存成功,订单服务创建订单成功,但是账户服务扣减账户余额失败,出现了异常,此时账户服务的RM即分支事务资源管理器向TC事务报告状态产生异常,此时TC会通知其余的RM回滚事务。当所有的RM均正常则提交事务。


最终一致性事务模式 + 可靠消息


描述: 可靠消息模式采用一个可靠的消息中间件作为中介,事务的发起方在完成本地事务后向可靠的消息中间件发起消息,事务消费方在收到消息后处理消息,该方式强调的是双方最终的数据一致性。


流程: 订单服务将消息发送给订单的消息队列,库存服务去监听订阅订单服务的消息队列,并从中消费消息。这种方式需要考虑消息的生产者发送到消息队列,再由消费者去消费消息,中间都有可能因为网络原因导致数据的不一致性。

  • 本地事务提交后可以使用主动触发方式对本地消息表进行保存与推送。
  • 库存服务在接收到消息并且处理完业务逻辑后,通过消息确认机制,回复ACK保证消息的消费成功。如果库存服务没有回复ACK,则消息中间件在没收到ACK是将进行重复投递。
  • 当消息被成功消费,库存服务可以回调一个订单服务的确认API,订单服务从本地消息表中删除或者更新其状态
  • 在订单服务中,如果重复性把本地消息发到库存服务,则需要消息的消费者(库存服务)提供消息的幂等性支持。

分布式事务的选择


关于一致性:


一致性就是数据保持一致性,在分布式系统中,可以理解为多个节点中的数据的值是一致的,而一致性分为强一致性和弱一致性/最终一致性(本身也是弱一致性的特殊表现形式)。


  • 强一致性:是程度最高的一种要求,也是最难实现的。系统中的某个数据被更新后,后续任何对该数据的操作都是及时更新后的值。
  • 弱一致性:系统中某个数据被更新后,后续对该数据的操作可能得到更新后得知,也可能是更新前的值,但经过“不一致的时间窗口”后,后续对该数据的操作都是更新后的值。
  • 最终一致性:在一段时间后,数据会最终达到一致性状态,这个状态时弱一致性的特殊形式。


场景对比:


模拟一个简单个新用户注册送福利,即营销拉新活动。用户服务与营销服务作为两个独立的服务,假设选择以seata作为分布式事务的解决方案,此时发生的场景:在用户注册环节或者营销服务发放福利环节任意一个环节出异常,那么都会导致用户的注册失败,这就是很不友好了,我们期望的是尽管营销服务出现问题那么应该不会影响用户的注册,福利可以通过后期补发,所以这种场景,消息事务方案更具有优势。


分页问题


MySQL数据库为例,如果是在之前的单库环境中,可以直接通过limit index,n的方式来做分页,而水平分库后由于存在多个数据源,因此分页又成为了一个难题,比如10条数据为1页,那如果想要拿到某张表的第一页数据,就必须通过如下手段获取:

这种方式可以是可以,但略微有些繁杂,同时也会让拓展性受限,比如原本有两个水平分库的节点,因此只需要从两个节点中拿到第一页数据,然后再做一次过滤即可,但如果水平库从两节点扩容到四节点,这时又要从四个库中各自拿10条数据,然后做过滤操作,读取前十条数据显示,这显然会导致每次扩容需要改动业务代码,对代码的侵入性有些强,所以合理的解决方案如下:


①常用的分页数据提前聚合到ES或中间表,运行期间跑按时更新其中的分页数据。

②利用大数据技术搭建数据中台,将所有子库数据汇聚到其中,后续的分页数据直接从中获取。

③上述聊到的那种方案,从所有字库中先拿到数据,然后在Service层再做过滤处理。


上述第一种方案是较为常用的方案,但这种方案对数据实时性会有一定的影响,使用该方案必须要能接受一定延时。第二种方案是最佳的方案,但需要搭建完善的大数据系统作为基础,成本最高。第三种方案成本最低,但对拓展性和代码侵入性的破坏比较严重。


ID主键唯一性问题


在之前的单库环境时,对于一张表的主键通常会选用整数型字段,然后通过数据库的自增机制来保证唯一性,但在水平分库多节点的情况时,假设还是以数据库自增机制来维护主键唯一性,这就绝对会出现一定的问题,可能会导致多个库中出现ID相同、数据不同的情况,如下:

上述两个库需要存储不同的数据,当插入数据的请求被分发到对应节点时,如果再依据自增机制来确保ID唯一性,因为这里有两个数据库节点,两个数据库各自都维护着一个自增序列,因此两者ID值都是从1开始往上递增的,这就会导致前面说到的ID相同、数据不同的情况出现,那此时又该如何解决呢?如下:

这时可以根据水平库节点的数量来设置自增步长,假设此时有两个库,那自增步长为2,两个库的ID起始值为:{DB1:1}、{DB2:2},最终达到上图中的效果,无论在插入数据的操作落入哪个节点,都能够确保ID的唯一性。当然,保障分布式系统下ID唯一性的解决方案很多,如下:


  • ①通过设置数据库自增机制的起始值和步长,来控制不同节点的ID交叉增长,保证唯一性。
  • ②在业务系统中,利用特殊算法生成有序的分布式ID,比如雪花算法、Snowflake算法等。
  • ③利用第三方中间件生产ID,如使用Redisincr命令、或创建独立的库专门做自增ID工作。


上述这几种方案都是较为主流的分布式ID生成的方案,同时也能够保证ID的有序性,能够最大程度上维护索引的性能,相对来说第一种方案成本最低,但是会限制节点的拓展性,也就是当后续扩容时,数据要做迁移,同时要重新修改起始值和自增步长。


一般企业中都会使用第二种方案,也就是通过分布式ID生成的算法,在业务系统中生成有序的分布式ID


读写分离


增加MySQL数据库的从节点来实现负载均衡,减轻主节点的查询压力,让主节点专注于处理写请求,保证读写操作的高性能。


物理机


  • 如果数据库服务器内存、硬件资源,或者网络资源配置不是很好,就会慢一些哈。这时候可以升级配置。这就好比你的计算机有时候很卡,你可以加个内存条什么的一个道理。
  • 如果数据库压力本身很大,比如高并发场景下,大量请求到数据库来,数据库服务器CPU占用很高或者IO利用率很高,这种情况下所有语句的执行都有可能变慢的哈。


相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
目录
相关文章
|
6天前
|
SQL 缓存 监控
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
本文详细解析了数据库、缓存、异步处理和Web性能优化四大策略,系统性能优化必知必备,大厂面试高频。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
|
6天前
|
存储 SQL API
探索后端开发:构建高效API与数据库交互
【10月更文挑战第36天】在数字化时代,后端开发是连接用户界面和数据存储的桥梁。本文深入探讨如何设计高效的API以及如何实现API与数据库之间的无缝交互,确保数据的一致性和高性能。我们将从基础概念出发,逐步深入到实战技巧,为读者提供一个清晰的后端开发路线图。
|
6天前
|
SQL Java 数据库连接
数据库常用接口
ODBC(Open Database Connectivity):开放数据库互连技术为访问不同的SQL数据库提供了一个共同的接口。ODBC使用SQL作为访问数据的标准。这一接口提供了最大限度的互操作性,一个应用程序可以通过共同的一组代码访问不同的SQL数据库管理系统(DBMS)。 一个基于ODBC的应用程序对数据库的操作不依赖任何DBMS,不直接与DBMS打交道,所有的数据库操作由对应的DBMS的ODBC驱动程序完成。也就是说,不论是Access,MySQL还是Oracle数据库,均可用ODBC API进行访问。由此可见,ODBC的最大优点是能以统一的方式处理所有的数据库。
|
7天前
|
存储 SQL 数据库
深入浅出后端开发之数据库优化实战
【10月更文挑战第35天】在软件开发的世界里,数据库性能直接关系到应用的响应速度和用户体验。本文将带你了解如何通过合理的索引设计、查询优化以及恰当的数据存储策略来提升数据库性能。我们将一起探索这些技巧背后的原理,并通过实际案例感受优化带来的显著效果。
25 4
|
8天前
|
SQL 缓存 监控
数据库性能优化指南
数据库性能优化指南
|
11天前
|
缓存 监控 NoSQL
数据库如何进行性能优化?
【10月更文挑战第31天】数据库如何进行性能优化?
23 3
|
14天前
|
运维 NoSQL Java
后端架构演进:微服务架构的优缺点与实战案例分析
【10月更文挑战第28天】本文探讨了微服务架构与单体架构的优缺点,并通过实战案例分析了微服务架构在实际应用中的表现。微服务架构具有高内聚、低耦合、独立部署等优势,但也面临分布式系统的复杂性和较高的运维成本。通过某电商平台的实际案例,展示了微服务架构在提升系统性能和团队协作效率方面的显著效果,同时也指出了其带来的挑战。
54 4
|
16天前
|
Java 数据库连接 数据库
Java连接池在数据库性能优化中的重要作用。连接池通过预先创建和管理数据库连接,避免了频繁创建和关闭连接的开销
本文深入探讨了Java连接池在数据库性能优化中的重要作用。连接池通过预先创建和管理数据库连接,避免了频繁创建和关闭连接的开销,显著提升了系统的响应速度和吞吐量。文章介绍了连接池的工作原理,并以HikariCP为例,展示了如何在Java应用中使用连接池。通过合理配置和优化,连接池技术能够有效提升应用性能。
32 1
|
21天前
|
程序员
后端|一个分布式锁「失效」的案例分析
小猿最近很苦恼:明明加了分布式锁,为什么并发还是会出问题呢?
30 2
|
25天前
|
SQL JavaScript 关系型数据库
node博客小项目:接口开发、连接mysql数据库
【10月更文挑战第14天】node博客小项目:接口开发、连接mysql数据库

热门文章

最新文章