09-微服务原理篇(XXLJOB-幂等-MySQL)

学习目标

  1. 能够说出xxl-job任务调度的优势
  2. 能说出xxl-job的组成结构
  3. 能够编写热点商品更新缓存任务
  4. 能够说出什么是幂等,常见幂等解决方案
  5. 能够说出数据库常见存储引擎的核心区别
  6. 能够说出索引失效的场景
  7. 能够说出回表、覆盖索引
  8. 能够说出SQL调优的方案
  9. 能够说出常见分库分表的方案

1 任务调度方案

1.1 什么是任务调度

1.1.1 概念

我们可以先思考一下下面业务场景的解决方案:

  • 某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
  • 某银行系统需要在信用卡到期还款日的前三天进行短信提醒。
  • 某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总。
  • 12306会根据车次的不同,而设置某几个时间点进行分批放票。
  • 某网站为了实现天气实时展示,每隔5分钟就去天气服务器获取最新的实时天气信息。

以上场景就是任务调度所需要解决的问题。

任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人 力由系统自动去执行任务。

在解决缓存击穿方案中,通过缓存定时预热避免缓存击穿。

1.1.2 技术方案

如何实现任务调度?

1、使用jdk提供的Timer定时器

示例代码如下:

每个Timer对应一个线程,可以同时启动多个Timer定时执行多个任务。

复制代码
public static void main(String[] args){  
    Timer timer = new Timer();  
    timer.schedule(new TimerTask(){
        @Override  
        public void run() {  
            //TODO:something
        }  
    }, 1000, 2000);  //1秒后开始调度,每2秒执行一次
}

Time使用简单,可以实现每隔一定的时间去执行任务,但无法实现每天凌晨去执行任务,即在某个时间点去执行任务。

2、使用第三方Quartz方式实现

Quartz 是一个功能强大的任务调度框架(项目地址:链接 ),它可以满足更多更复杂的调度需求,Quartz 设计的核心类包括 Scheduler, Job 以及 Trigger。其中,Job 负责定义需要执行的任务,Trigger 负责设置调度策略,Scheduler 将二者组装在一起,并触发任务开始执行。Quartz支持简单的按时间间隔调度、还支持按日历调度方式,通过设置CronTrigger表达式(包括:秒、分、时、日、月、周、年)进行任务调度。

虽然Quartz可以实现按日历调度的方式,但无法支持分布式环境下任务调度。分布式环境下通常一个服务部署多个实例即多个jvm进程,假设一个项目的微服务部署两个实例每个实例定时执行更新缓存的任务,两个实例就会重复执行。如下图:

3、使用分布式调度平台XXL-JOB

XXL-JOB是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

官网:https://www.xuxueli.com/xxl-job/

文档:https://www.xuxueli.com/xxl-job/#《分布式任务调度平台XXL-JOB》

XXL-JOB主要有调度中心、执行器、任务:

调度中心:

负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码;

主要职责为执行器管理、任务管理、监控运维、日志管理等

任务执行器:

负责接收调度请求并执行任务逻辑;

主要职责是执行任务、执行结果上报、日志服务等

使用XXL-JOB可以解决多个jvm进程重复执行任务的问题,如下图:

XXL-JOB调度中心可以配置路由策略,比如:第一个、轮询策略、分片等,它们分别表示的意义如下:

  • 第一个:即每次执行任务都由第一个执行器去执行。
  • 轮询:即执行器轮番执行。
  • 分片:每次执行任务广播给每个执行器让他们同时执行任务。

如果根据需求每次执行任务仅由一个执行器去执行任务可以设置路由策略:第一个、轮询。

如果根据需求每次执行任务由多个执行器同时执行可以设置路由策略为:分片。

xxl-job分布式任务调度系统具体有以下优势:

1、并行任务调度

并行任务调度实现靠多线程,如果有大量任务需要调度,此时光靠多线程就会有瓶颈了,因为一台计算机CPU的处理能力是有限的。

如果将任务调度程序分布式部署,每个结点还可以部署为集群,这样就可以让多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。

2、高可用

若某一个实例宕机,不影响其他实例来执行任务。

3、弹性扩容

当集群中增加实例就可以提高并执行任务的处理效率。

4、任务管理与监测

对系统中存在的所有定时任务进行统一的管理及监测。让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。

5、避免任务重复执行

当任务调度以集群方式部署,同一个任务调度可能会执行多次,比如在上面提到的电商系统中到点发优惠券的例子,就会发放多次优惠券,对公司造成很多损失,所以我们需要控制相同的任务在多个运行实例上只执行一次。

1.1.3 小结

xxl-job任务调度与第三方Quartz或timer定时器实现任务调度有什么优势?

1.2 搭建XXL-JOB

1.2.1 组成结构

XXL-JOB由两部分组成:

  • 调度模块(调度中心): 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
  • 执行模块(执行器): 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效; 接收"调度中心"的执行请求、终止请求和日志请求等。

1.2.2 部署调度中心

1.查阅xxl-job的源码

首先下载XXL-JOB

GitHub:https://github.com/xuxueli/xxl-job

码云:https://gitee.com/xuxueli0323/xxl-job

项目使用2.3.1版本: https://github.com/xuxueli/xxl-job/releases/tag/2.3.1

也可从课程资料目录获取,解压xxl-job-2.3.1.zip

使用IDEA打开解压后的目录

xxl-job-admin:调度中心

xxl-job-core:公共依赖

xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用)

:xxl-job-executor-sample-springboot:Springboot版本,通过Springboot管理执行器,推荐这种方式;

:xxl-job-executor-sample-frameless:无框架版本;

doc :文档资料,包含数据库脚本

在下发的虚拟机的MySQL中已经创建了xxl_job_2.3.1数据库

如下图:

安装xxl-job

没有使用下发虚拟机的同学请自行安装xxl-job。

拉取镜像:

docker pull xuxueli/xxl-job-admin:2.3.1

创建数据库:xxl_job_2.3.1

导入xxl_job_2.3.1.sql,如下:

创建目录:

/data/soft/xxl-job

/data/soft/xxl-job/applogs

创建配置文件:/data/soft/xxl-job/application.properties,内容如下:

复制代码
### web
server.port=8080
server.servlet.context-path=/xxl-job-admin

### actuator
management.server.servlet.context-path=/actuator
management.health.mail.enabled=false

### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/

### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########

### mybatis
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
#mybatis.type-aliases-package=com.xxl.job.admin.core.model

### xxl-job, datasource
spring.datasource.url=jdbc:mysql://192.168.101.68:3306/xxl_job_2.3.1?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=mysql
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

### datasource-pool
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=1000

### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.from=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

### xxl-job, access token
xxl.job.accessToken=default_token

### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")
xxl.job.i18n=zh_CN

## xxl-job, triggerpool max size
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100

### xxl-job, log retention days
xxl.job.logretentiondays=30

创建容器:

复制代码
docker run -d -e \
  --restart=always \
  -v /data/soft/xxl-job/applogs:/data/applogs \
  -v /data/soft/xxl-job/application.properties:/application.properties \
  -p 8088:8080 \
  --name xxl-job-admin \
  xuxueli/xxl-job-admin:2.3.1

启动成功进入管理界面:

http://192.168.101.68:8088/xxl-job-admin

账号/密码:admin/123456

2.启动xxl-job

执行docker start xxl-job-admin 启动xxl-job

访问:http://192.168.101.68:8088/xxl-job-admin/

账号和密码:admin/123456

1.2.3 执行器

1.添加执行器依赖

下边配置执行器,执行器负责与调度中心通信接收调度中心发起的任务调度请求,执行器负责执行微服务中定义的任务,执行器程序由xxl-job提供,在微服务中引入下边的依赖即加入了执行器的程序。

我们在商品服务中引入xxl-job执行器依赖。

复制代码
<dependency>
  <groupId>com.xuxueli</groupId>
  <artifactId>xxl-job-core</artifactId>
  <version>2.3.0</version>
</dependency>

参考源代码中的XxlJobConfig去编写xxl-job的配置类,此配置类已提供,将课程资料中xxl-job下的配置类和模型类拷贝到商品服务的config包下:

2.配置xxl-job

在application.yaml下配置xxl-job

复制代码
xxl-job:
  enable: true
  port: 11603
  access-token: default_token
  admin:
    address: http://192.168.101.68:8088/xxl-job-admin
  executor:
    appName: ${spring.application.name}
    #ip: 172.17.0.170
    port: ${xxl-job.port}
    # 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能
    log-retention-days: 30

说明:

address:调度中心的地址

appName:执行器名称,spring.application.name表示微服务的名称(在bootstrap.yml中配置)

port:执行器端口号,通过xxl-job.port配置,执行器通过此端口与调度中心通信。

3. 下边进入调度中心添加执行器

启动商品服务即启动了xxl-job执行器。

进入调度中心,进入执行器管理界面,如下图:

点击新增,填写执行器信息

AppName:执行名称, appName: ${spring.application.name}表示指定执行器名称就是微服务的应用名。

名称:取一个中文名称。

注册方式:自动注册,只要执行器和调度中心连通执行器会自动注册到调度中心

机器地址:自动注册时不用填写。

添加成功:

启动item-service,查看item-service的控制台:

>>>>>>>>>>> xxl-job remoting server start success, nettype = class com.xxl.job.core.server.EmbedServer, port = 11603 说明执行器启动成功。

稍等片刻进入 xxl-job调度中心,进入执行器管理界面,执行器注册成功:

点击"查看(1)",查看执行器的地址,如下图:

1.2.4 小结

项目为什么要用xxl-job?

能说出xxl-job的组成部分。

1.3 XXL-JOB任务入门

1.3.1 编写测试任务

定时执行任务就需要编写任务方法,此任务方法由执行器去调用。

可以参考xxl-job源码去编写任务方法,从源码目录中找到执行器示例代码:

xxl-job-2.3.1\xxl-job-executor-samples\xxl-job-executor-sample-springboot\src\main\java\com\xxl\job\executor\service\jobhandler\SampleXxlJob.java

部分示例代码如下:下边代码中demoJobHandler()就是一个任务方法,需要使用@XxlJob注解标识,所在类需要由spring去管理,所以加了@Component注解。

将源代码中的SampleXxlJob类拷贝到商品服务的job包下,修改代码如下:

复制代码
package com.hmall.item.job;
...
@Component
@Slf4j
public class SampleXxlJob {
    private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);


    /**
     * 1、简单任务示例(Bean模式)
     */
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
        log.info("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
            log.info("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        // default success
    }


    /**
     * 2、分片广播任务
     */
    @XxlJob("shardingJobHandler")
    public void shardingJobHandler() throws Exception {

        // 分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();

        log.info("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);

        // 业务逻辑
        for (int i = 0; i < shardTotal; i++) {
            if (i == shardIndex) {
                log.info("第 {} 片, 命中分片开始处理", i);
            } else {
                log.info("第 {} 片, 忽略", i);
            }
        }

    }

}

1.3.2 配置任务

下边在调度中心配置任务。

进入任务管理,新增任务:

填写任务信息:

说明:

调度类型

固定速度指按固定的间隔定时调度。

Cron,通过Cron表达式实现更丰富的定时调度策略。

Cron表达式是一个字符串,通过它可以定义调度策略,格式如下:

{秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}

Cron 的各个域的定义如下表格所示:

xxl-job提供图形界面去配置:

一些例子如下:

0 0 0 * * ? 每天0点触发

30 10 1 * * ? 每天1点10分30秒触发

0/30 * * * * ? 每30秒触发一次

* 0/10 * * * ? 每10分钟触发一次

为了方便测试这里第5秒执行一次,设置为:0/5 * * * * ?

cron 表达式的难点在于通配符,下边的内容请自行阅读

  • , 这里指的是在两个以上的时间点中都执行,如果我们在 "分" 这个域中定义为 8,12,35 ,则表示分别在第8分,第12分 第35分执行该定时任务。
  • - 这个比较好理解就是指定在某个域的连续范围,如果我们在 "时" 这个域中定义 1-6,则表示在1到6点之间每小时都触发一次,用 , 表示 1,2,3,4,5,6
  • * 表示所有值,可解读为 "每"。 如果在"日"这个域中设置 *,表示每一天都会触发。
  • ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的8号触发一个操作,但不关心是周几,我们可以这么设置 0 0 0 8 * ?
  • / 在某个域上周期性触发,该符号将其所在域中的表达式分为两个部分,其中第一部分是起始值,除了秒以外都会降低一个单位,比如 在 "秒" 上定义 5/10 表示从 第 5 秒开始 每 10 秒执行一次,而在 "分" 上则表示从 第 5 秒开始 每 10 分钟执行一次。
  • L 表示英文中的LAST 的意思,只能在 "日"和"周"中使用。在"日"中设置,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年), 在"周"上表示周六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在"周"上设置"7L"这样的格式,则表示"本月最后一个周六"
  • W 表示离指定日期的最近那个工作日(周一至周五)触发,只能在 "日" 中使用且只能用在具体的数字之后。若在"日"上置"15W",表示离每月15号最近的那个工作日触发。假如15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果是 "1W" 就只能往本月的下一个最近的工作日推不能跨月往上一个月推。
  • # 表示每月的第几个周几,只能作用于 "周" 上。例如 "2#3" 表示在每月的第三个周二。

运行模式有BEAN和GLUE,bean模式较常用就是在项目工程中编写执行器的任务代码,GLUE是将任务代码编写在调度中心。

JobHandler即任务方法名,填写任务方法上边@XxlJob注解中的名称。

路由策略

第一个:即每次执行任务都由第一个执行器去执行。

轮询:即执行器轮番执行。

分片:每次执行任务广播给每个执行器让他们同时执行任务。

详细说明xxl-job源码中的doc目录下的文档:

1.3.3 启动任务并测试

任务配置完成,下边启动任务

启动成功:

我们在任务方法上打断点跟踪,任务方法被执行,如下图:

1.4 分片广播任务

1.4.1 什么是分片广播任务

先看一张动图

掌握了xxl-job的基本使用,下边思考如何进行分布式任务处理呢?如下图,我们会启动多个执行器组成一个集群,去执行任务。

查看xxl-job官方文档,阅读高级配置相关的内容:

复制代码
高级配置:
    - 路由策略:当执行器集群部署时,提供丰富的路由策略,包括;
        FIRST(第一个):固定选择第一个机器;
        LAST(最后一个):固定选择最后一个机器;
        ROUND(轮询):;
        RANDOM(随机):随机选择在线的机器;
        CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
        LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
        LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
        FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
        BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
        SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;

下边要重点说的是分片广播策略,分片是指是调度中心以执行器为维度进行分片,将集群中的执行器标上序号:0,1,2,3...,广播是指每次调度会向集群中的所有执行器发送任务调度,请求中携带分片参数。

分片广播任务就是调度中心按照调度策略广播通信所有执行器(分片)去执行任务。

如下图:

每个执行器收到调度请求同时接收分片参数。

xxl-job支持动态扩容执行器集群从而动态增加分片数量,当有任务量增加可以部署更多的执行器到集群中,调度中心会动态修改分片的数量。

作业分片适用哪些场景呢?

  • 分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍;

所以,广播分片方式不仅可以充分发挥每个执行器的能力,并且根据分片参数可以控制任务是否执行,最终灵活控制了执行器集群分布式处理任务。

使用说明:

"分片广播" 和普通任务开发流程一致,不同之处在于可以获取分片参数进行分片业务处理。

Java语言任务获取分片参数方式:

BEAN、GLUE模式(Java),可参考Sample示例执行器中的示例任务"ShardingJobHandler":

复制代码
/**
 * 2、分片广播任务
 */
@XxlJob("shardingJobHandler")
public void shardingJobHandler() throws Exception {

    // 分片参数
    int shardIndex = XxlJobHelper.getShardIndex();
    int shardTotal = XxlJobHelper.getShardTotal();
    
    XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);
    
    // 业务逻辑
    for (int i = 0; i < shardTotal; i++) {
        if (i == shardIndex) {
            XxlJobHelper.log("第 {} 片, 命中分片开始处理", i);
        } else {
            XxlJobHelper.log("第 {} 片, 忽略", i);
        }
    }

}

1.4.2 测试分片广播任务

下边测试作业分片广播任务:

1、定义作业分片的任务方法

复制代码
/**
 * 2、分片广播任务
 */
@XxlJob("shardingJobHandler")
public void shardingJobHandler() throws Exception {

    // 分片参数
    int shardIndex = XxlJobHelper.getShardIndex();
    int shardTotal = XxlJobHelper.getShardTotal();
    
    XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);
    
    // 业务逻辑
    for (int i = 0; i < shardTotal; i++) {
        if (i == shardIndex) {
            XxlJobHelper.log("第 {} 片, 命中分片开始处理", i);
        } else {
            XxlJobHelper.log("第 {} 片, 忽略", i);
        }
    }

}

2、在调度中心添加任务

添加成功:

下边启动两个商品服务实例

两个实例的在启动时注意端口不能冲突:

  • 实例1 在VM options处添加:-Dserver.port=8081 -Dxxl-job.port=11603
  • 实例2 在VM options处添加:-Dserver.port=7081 -Dxxl-job.port=11604

启动成功观察执行器

启动任务,观察日志

实例1:

实例2:

下边启动两个执行器实例,观察每个实例的执行情况。

1.4.3. 分片执行任务

当一次分片广播到来,各执行器如何根据分片参数去分布式执行任务,保证执行器之间执行的任务不重复呢?

举例:

批量处理商品表中的数据,保证每个执行器处理的商品信息不重复。

可以将分片总数和分片序列带入sql,如下:

select * from item where id % #{shardingTotalCount} = #{shardingItem}

假设当前有两个分片,分片0执行如下sql:

select ***from item where id % 2=0

分片1执行如下sql:

select ***from item where id % 2=1

两个分片获取的数据是不一样的。

测试如下:

定义mapper如下:

复制代码
public interface ItemMapper extends BaseMapper<Item> {
    ....

    //根据分片总数和分片序号查询商品表 select * from item where id % ? = ?
    @Select("select * from item where id % #{shardingTotalCount} = #{shardingIndex}")
    List<Item> selectBySharding(int shardingTotalCount, int shardingIndex);
}

在任务中调用ItemMapper获取商品信息

复制代码
@XxlJob("shardingJobHandler")
public void shardingJobHandler() throws Exception {

    // 分片参数
    int shardIndex = XxlJobHelper.getShardIndex();
    int shardTotal = XxlJobHelper.getShardTotal();
    
    //查询商品表
    //select * from item where id % ? = ?
    List<Item> items = itemMapper.selectBySharding(shardTotal, shardIndex);
    
    log.info("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);
}

1.5 热点商品定时预热任务

1.5.1 编写任务方法

根据需求,为了防止缓存击穿我们使用xxl-job定时对热点商品进行预热。

首先在ItemServiceImpl中编写获取热点商品id方法

复制代码
@Override
public List<Long> queryHotItems() {
    //模拟热点商品id
    return List.of(317578L, 317580L);
}

编写商品预热任务方法

复制代码
@Component
@Slf4j
public class SampleXxlJob {
    private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);

    //注入itemService
    @Resource
    private IItemService itemService;

    /**
     * 对热点商品进行定时更新缓存
     */
    @XxlJob("hotItemCacheJobHandler")
    public void hotItemCacheJobHandler() throws Exception {
        log.info("定时对热点商品进行更新缓存开始...");
        //获取热点商品id
        List<Long> ids = itemService.queryHotItems();
        itemService.queryItemByIdsCache(ids);
        log.info("定时对热点商品进行更新缓存完成...");
    }
    ...

1.5.2 配置任务

下边在调度中心配置任务。

进入任务管理,新增任务:

1.5.3 启动任务并测试

任务配置完成,下边启动任务

下边重启商品服务,观察热点商品是否在redis更新缓存。

1.5.4 小结

项目中哪里用了xxl-job?怎么用的?

2 幂等解决方案

2.1 什么是幂等

在计算机科学和网络通信中,幂等指一个操作无论执行多少次,其结果都是一致的。换句话说,对于相同的输入,无论进行多少次重复操作,都应该保持其结果一致。

  • 读:不对底层数据产生变化,天然支持幂等,如HTTP:GET
  • 写:会修改底层数据,无法保证幂等,如HTTP:POST/DELETE/PUT

2.2 产生幂等场景

在Web应用程序中,导致幂等的原因一般分为:

  • 网络延迟导致的请求重试
  • 前端用户的重复操作请求
  • 第三方中间件的重试机制(如MQ)

当重复请求(相同输入的重复操作)进入系统中时,如果接口不具备幂等性,可能会导致:

  • 状态不一致数据
  • 重复提交

2.3 幂等场景及解决方案

以下场景代码均已准备,如下

idempotent.zip

注意:下述的解决方案,不一定只能解决当前场景下的,比如:数据库唯一索引一样可以解决消息重复消费,这里借助不同场景,便于大家学习多种解决方案。

1.前端重复提交

按照上述执行流程,如果用户在订单、预约等场景重复点击,是否会创建多个订单呢?

我们可以借助上述代码进行复现

解决方案如下:

2.消息重复消费

3.任务重复执行

3 关系型数据库MySQL

3.1 数据库存储引擎

|-----------|-------------------------------------------------------------------------|-----------------------------------------------------|
| 对比维度 | InnoDB | MyISAM |
| 事务支持 | 完全支持 ACID 特性(原子性 / 一致性 / 隔离性 / 持久性),支持事务隔离级别(默认 REPEATABLE READ)、回滚、保存点 | 不支持事务,无回滚机制,数据更新时崩溃可能导致数据损坏 |
| 并发控制 | 支持行级锁(默认)+ 表级锁,写入操作仅锁定目标行,并发读写冲突概率低,适合高并发场景 | 仅支持表级锁,写入操作(INSERT/UPDATE/DELETE)锁定整个表,并发写入性能极差 |
| 数据完整性 | 支持外键(FOREIGN KEY)、主键约束、唯一约束,强制数据参照完整性 | 不支持外键,仅支持主键 / 唯一约束(逻辑层面),无参照完整性保障 |
| 索引结构 | 聚簇索引(主键与数据物理存储绑定),非主键索引存储主键值,主键查询效率极高;若未指定主键,自动生成隐藏 6 字节 row_id | 非聚簇索引(数据文件 .MYD 与索引文件 .MYI 分离),索引仅存储数据行指针 |
| 崩溃恢复 | 支持崩溃恢复(通过 redo log 重做日志 + undo log 回滚日志),数据一致性有保障 | 不支持崩溃恢复,崩溃后可能出现数据碎片或损坏,需通过 myisamchk 工具修复 |
| 数据文件 | 单表独立文件(.ibd)+ 系统表空间文件(ibdata1)+ redo log 文件(ib_logfile0/1) | 表结构文件(.frm)+ 数据文件(.MYD)+ 索引文件(.MYI),文件结构简单 |
| 存储特性 | 支持数据压缩(需开启特定配置)、分区表、MVCC(多版本并发控制,读不加锁) | 支持压缩表(COMPRESSED 格式)、空间索引(GIS 数据),无 MVCC 机制 |
| 全文索引 | MySQL 5.6+ 支持全文索引,性能与功能逐步完善 | 早期版本唯一支持全文索引的引擎,全文索引功能较基础 |
| 读写性能 | 写性能优秀(行锁 + 事务优化),读性能略逊于 MyISAM,但整体均衡 | 读性能极优(索引缓存高效),写性能极差(表锁导致阻塞) |
| 内存占用 | 主要依赖 innodb_buffer_pool_size (缓存数据 + 索引),内存占用较高 | 主要依赖 key_buffer_size (仅缓存索引),内存占用较低 |
| 适用场景 | 核心业务表(订单、支付、用户数据)、高并发读写、需事务 / 外键的场景 | 只读 / 读写比例极高的静态数据(博客、新闻)、日志表、统计报表(无并发写入) |
| 版本支持 | MySQL 5.5+ 默认引擎,持续迭代优化(如 MySQL 8.0 增强全文索引、并行查询) | 逐步被淘汰,MySQL 8.0 中已移除部分特性支持,不推荐新系统使用 |

3.2 常见索引类型

|----------|------------------------------|------------------------------------------|--------------------------------|-----------------------------------------------|--------------------------------------------------------------------------------------|
| 分类维度 | 索引类型 | 核心定义 | 支持引擎 | 适用场景 | 关键特点 / 注意事项 |
| 存储结构 | 聚簇索引(Clustered) | 数据与索引物理存储绑定,索引即数据(索引叶子节点存储完整数据行) | InnoDB(默认) | 主键查询、高频根据主键 / 聚簇键查询 | 1. 每张表仅 1 个聚簇索引;2. 优先以主键为聚簇键,无主键则选唯一非空列,否则生成隐藏 row_id;3. 主键查询效率极高 |
| 存储结构 | 非聚簇索引(Non-Clustered) | 数据与索引物理分离,索引叶子节点存储数据行指针 / 主键值(间接指向数据) | MyISAM、InnoDB(二级索引)、Memory | 非主键字段查询(如用户名、手机号查询) | 1. 每张表可多个非聚簇索引;2. 查询需 "回表"(InnoDB)或 "通过指针找数据"(MyISAM);3. 存储开销比聚簇索引小 |
| 约束特性 | 普通索引(Normal) | 无约束限制,仅用于加速查询的基础索引 | 所有支持索引的引擎 | 普通查询场景(如商品分类查询、文章列表查询) | 1. 允许重复值和 NULL;2. 无额外约束逻辑,创建 / 维护成本低 |
| 约束特性 | 唯一索引(Unique) | 索引列值必须唯一(允许 NULL,且 NULL 可多个),兼具约束和加速查询功能 | InnoDB、MyISAM、Memory | 需保证字段唯一性的场景(如手机号、邮箱、身份证号) | 1. 避免重复数据,插入重复值会报错;2. 查询效率略高于普通索引(索引树更紧凑);3. 主键索引是特殊的唯一索引(不允许 NULL) |
| 约束特性 | 主键索引(Primary Key) | 特殊的唯一索引,不允许 NULL,默认作为 InnoDB 的聚簇键 | 所有支持索引的引擎 | 表的唯一标识(如 user_id、order_id) | 1. 每张表仅 1 个主键索引;2. InnoDB 中自动作为聚簇索引,MyISAM 中为普通唯一非聚簇索引;3. 优先用自增 INT/BIGINT 作为主键 |
| 字段数量 | 单列索引(Single-Column) | 仅基于单个字段创建的索引 | 所有支持索引的引擎 | 单字段查询(如按手机号查用户、按状态查订单) | 1. 创建简单、维护成本低;2. 仅能加速该字段的查询条件 |
| 字段数量 | 组合索引(Composite/Multi-Column) | 基于多个字段组合创建的索引(字段顺序影响索引效率) | InnoDB、MyISAM、Memory | 多字段联合查询(如按 "用户 ID + 订单状态" 查订单、"用户名 + 手机号" 验证) | 1. 遵循 "最左前缀原则"(查询需匹配索引字段顺序);2. 可覆盖部分多字段查询,减少回表;3. 字段顺序建议:高区分度字段在前 |
| 查询功能 | 全文索引(Fulltext) | 针对文本内容的语义检索索引,支持关键词匹配(而非前缀 / 后缀模糊匹配) | InnoDB(5.6+)、MyISAM | 文章内容搜索、商品描述搜索、评论检索 | 1. 支持 CHAR/VARCHAR/TEXT 字段;2. 忽略短词(默认 < 4 字符);3. 高并发 / 复杂检索建议用 Elasticsearch 替代 |
| 查询功能 | 空间索引(Spatial) | 针对地理空间数据的索引,支持位置关系查询(如距离、包含) | InnoDB(5.7+)、MyISAM | 附近位置查询(如 "1 公里内餐厅")、区域筛选 | 1. 支持 POINT/LINESTRING/POLYGON 等空间类型;2. 字段不能为 NULL;3. 需用空间函数(如 ST_Distance_Sphere)查询 |
| 索引结构 | B + 树索引(B+Tree) | 主流索引结构,所有数据存储在叶子节点,叶子节点按顺序链表连接 | InnoDB、MyISAM、Memory(可选) | 绝大多数查询场景(等值、范围、排序、分页) | 1. 平衡树结构,查询效率稳定(O (log n));2. 支持范围查询和排序;3. InnoDB/MyISAM 默认索引结构 |
| 索引结构 | 哈希索引(Hash) | 基于哈希表实现,通过字段哈希值快速定位数据,不支持范围查询 | Memory(默认)、InnoDB(自适应哈希索引,AHI) | 高频等值查询(如缓存表、字典表) | 1. 等值查询极快,范围 / 排序查询失效;2. 不支持模糊匹配;3. InnoDB 的 AHI 为自动开启,不可手动创建 |

3.3 索引到底长什么样

学习mysql存储数据之前,得先了解几个概念

数据页

数据页 :存放有多条完整记录行的存储空间;

  • 如果没有数据页;需要一条一条记录从磁盘读取到内存(pageout),频繁的IO,影响性能
  • 如果有数据页 ,将这16KB的记录组称为一个 ;运用 程序局部性原理;加载一页进行一次IO;与没有利用页的方式比较;大大地降低了磁盘IO;

目录页

借助於能不能一下子就定位到在哪一頁:不能

nan

有了页之后,只是减轻了部分IO(磁盘)【从原来一次一条数据,变成了一次一页数据(16KB)】;加载到内存后还是要一条条比较,除非查询到了数据不然会一直加载所有页;这样依然不够快。如果能知道要查找的数据在哪一页的话就好; 直接加载那页数据。

于此;只将每个页的 页号和比较的数据 作为一条页号记录存入一页中;这样

把针对 生成的目录对应的页称为目录页 ,而之前存储完整记录 的页称为数据页

索引

借助於能不能一下子就定位到在哪一頁:不能

nan

南:220页

男:220页

難:221页

♂:222页

楠:222页

目录页 多了之后也需要提高对于目录的查询;如果目录页之间也可以根据一定的形式进行组织;保存用户数据与目录项。目录项中保存页的记录中主键的最小值与页号,从而保证下一个数据页中记录的主键值大于上一个页中记录的主键值。可形成如下:

上图呈现的也就是索引 是一种数据结构,用于加速查询

MySQL 中是由存储引擎实现索引,InnoDB存储引擎索引基于B+树 实现;也就是MySQL中每个索引都是一棵B+树。每个表的数据也是存在在B+树中。

B+树

  • 最顶层的目录页称为根节点 ,最下层的存储完整记录的数据页称为叶子节点
  • 用户数据保存在叶子节点,目录项保存在非叶子节点,每个节点中可能保存多个页;
  • 最上面的节点称为根节点,根节点的地址保存在内存的数据字典中;
  • B+ 树的深度一般控制在 3 层以内,因此定位到单条记录不超过 3 次 IO
  • 页面和记录是排好序的,就可以通过二分法来快速定位查找
    • 冒泡、快排、选择;二分;单例模式:懒汉&恶汉&双检锁(sync)

MySQL B+树深度超过4层,性能有非常大影响;所以一般控制在3层

3.4 回表与覆盖索引

当普通二级索引中不包含要查询的所有字段时,就需要先通过二级索引查出主键索引,再通过主键索引查询二级索引中没有的其他列的数据,这个过程叫做回表;

举例:构建了一个组合索引abc,表结构:id,a,b,c,d,e

select id from user where id=1

select id from user where a=1

select id from user where a=1 and b=2

select id,a,b,c from user where a=1 and b=2


select id,a,b,e from user where a=1 and b=2

  • 步骤1:根据where条件得到id
    • select id,a,b from user where a=1 and b=2
  • 步骤2:根据id得到e:这个过程就叫回表
    • select e from user where id=?

3.6 索引失效场景

一、查询条件对索引字段做 "函数 / 运算操作"

场景示例

sql

复制代码
-- 索引:idx_create_time (create_time)
SELECT * FROM order WHERE DATE(create_time) = '2024-01-01'; -- 函数操作,失效
SELECT * FROM user WHERE age + 1 = 30; -- 算术运算,失效
SELECT * FROM goods WHERE SUBSTR(name, 1, 2) = '手机'; -- 字符串函数,失效
失效原因

索引是按「字段原始值」排序存储的,对字段做函数 / 运算后,值的有序性被破坏,优化器无法通过索引树快速定位,只能全表扫描计算后匹配。

优化方案

将函数 / 运算移到等号右侧(用常量运算),保留索引字段原始值:

sql

复制代码
SELECT * FROM order WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00';
SELECT * FROM user WHERE age = 30 - 1; -- 常量运算,不影响索引
SELECT * FROM goods WHERE name LIKE '手机%'; -- 前缀匹配,走索引(替代SUBSTR)

二、隐式类型转换

场景示例

sql

复制代码
-- 索引:idx_phone (phone),phone 是 VARCHAR 类型
SELECT * FROM user WHERE phone = 13800138000; -- 数值 vs 字符串,失效
-- 索引:idx_user_id (user_id),user_id 是 INT 类型
SELECT * FROM order WHERE user_id = '1001'; -- 字符串 vs 数值,可能失效(取决于MySQL版本)
失效原因

MySQL 会对字段做 "隐式类型转换"(如 VARCHAR→INT),本质等同于对索引字段做了函数操作(CAST(phone AS UNSIGNED) = 13800138000),破坏索引有序性。

优化方案

查询条件的值类型与索引字段类型完全一致

sql

复制代码
SELECT * FROM user WHERE phone = '13800138000'; -- 字符串匹配字符串
SELECT * FROM order WHERE user_id = 1001; -- 数值匹配数值

三、模糊查询以 "%" 开头(左模糊 / 全模糊)

场景示例

sql

复制代码
-- 索引:idx_name (name)
SELECT * FROM user WHERE name LIKE '%张三'; -- 左模糊,失效
SELECT * FROM user WHERE name LIKE '%张三%'; -- 全模糊,失效
失效原因

B + 树索引是按字段 "前缀顺序" 排序的,左模糊 / 全模糊无法确定查询的起始位置,只能遍历整个索引树(等同于全表扫描)。

优化方案
  • 前缀模糊匹配(% 放右侧):name LIKE '张三%'(走索引);
  • 全模糊需求:用全文索引(FULLTEXT INDEX)或 Elasticsearch;
  • 特殊场景:若字段是手机号 / 身份证号,可存储 "反转字段"(如 reverse_phone),查询时 reverse_phone LIKE reverse('%138')(即 phone LIKE '138%' 的反转,走索引)。

四、组合索引不满足 "最左前缀原则"

场景示例

sql

复制代码
-- 组合索引:idx_userid_status_time (user_id, status, create_time)
SELECT * FROM order WHERE status = 1; -- 跳过左列 user_id,失效
SELECT * FROM order WHERE create_time >= '2024-01-01'; -- 跳过前两列,失效
SELECT * FROM order WHERE status = 1 AND user_id = 1001; -- 字段顺序颠倒,失效(本质是没匹配左前缀)
失效原因

组合索引的索引树是按「第一字段→第二字段→第三字段」的顺序排序的,必须从左到右连续匹配,跳过左列或颠倒顺序,都会导致索引无法利用。

优化方案
  • 严格按组合索引的 "左前缀顺序" 查询(可跳过后缀,不能跳过前缀):sql

    SELECT * FROM order WHERE user_id = 1001; -- 匹配第1列,走索引
    SELECT * FROM order WHERE user_id = 1001 AND status = 1; -- 匹配前2列,走索引

  • 调整组合索引字段顺序:将查询频率高、区分度高的字段放在左侧(如 user_id 是高频查询条件,放最左)。

五、使用 OR 连接非索引字段

场景示例

sql

复制代码
-- 索引:idx_phone (phone),name 无索引
SELECT * FROM user WHERE phone = '13800138000' OR name = '张三'; -- OR 连接非索引字段,失效
失效原因

OR 两边的条件需同时满足 "可利用索引",若其中一个字段无索引,优化器无法通过索引快速筛选所有符合条件的记录,只能全表扫描。

优化方案
  • OR 两边的字段都建索引(如给 nameidx_name),此时优化器会走 "索引合并";

  • UNION 替代 OR(适用于两边都有索引的场景):sql

    SELECT * FROM user WHERE phone = '13800138000'
    UNION ALL
    SELECT * FROM user WHERE name = '张三';

六、NOT IN/NOT EXISTS/<>(不等于)

场景示例

sql

复制代码
-- 索引:idx_status (status)
SELECT * FROM order WHERE status NOT IN (1, 2); -- NOT IN,失效
SELECT * FROM order WHERE status <> 1; -- 不等于,失效
SELECT * FROM user WHERE NOT EXISTS (SELECT 1 FROM order WHERE order.user_id = user.user_id); -- 部分场景失效
失效原因

NOT IN/<> 等条件表示 "排除某些值",优化器认为这类条件筛选后的结果集可能较大,全表扫描比索引扫描更高效(若结果集极小,可能仍走索引,但不稳定)。

优化方案
  • 用 "范围查询" 替代(若业务允许):status > 2(走索引);

  • NOT EXISTS 可优化为 LEFT JOIN + IS NULL(通常更高效,且可能走索引):sql

    SELECT u.* FROM user u LEFT JOIN order o ON u.user_id = o.user_id WHERE o.user_id IS NULL;

七、JOIN 关联时字段类型不匹配(或关联字段无索引)

场景示例

sql

复制代码
-- 表1:user (user_id INT PRIMARY KEY),表2:order (user_id VARCHAR(20),无索引)
SELECT * FROM user u JOIN order o ON u.user_id = o.user_id; -- 字段类型不匹配 + 关联字段无索引,失效
失效原因
  • 关联字段类型不匹配:触发隐式类型转换,索引失效;
  • 关联字段无索引:JOIN 时需对右表做全表扫描(Nested Loop 连接时)。
优化方案
  • 关联字段类型完全一致(如 order.user_id 改为 INT);
  • 给右表的关联字段建索引(idx_order_userid (user_id))。

八、查询条件包含 NULL 判断(IS NULL/IS NOT NULL

场景示例

sql

复制代码
-- 索引:idx_email (email),email 允许 NULL
SELECT * FROM user WHERE email IS NULL; -- 可能失效(取决于数据分布)
SELECT * FROM user WHERE email IS NOT NULL; -- 大概率失效
失效原因

InnoDB 的 B + 树索引会存储 NULL 值,但 IS NOT NULL 表示 "查询非空值",若非空值占比极高,优化器会选择全表扫描;IS NULL 若空值极少,可能走索引,否则失效。

优化方案
  • 字段设计时尽量避免 NULL(用默认值替代,如 email 默认为空字符串 '');
  • 若必须允许 NULL,且需频繁查询 IS NULL,可单独建索引(或组合索引包含该字段),并确保空值占比低。

九、使用 SELECT * 导致无法触发覆盖索引(间接导致回表,但不算 "索引失效",算 "索引未充分利用")

场景示例

sql

复制代码
-- 组合索引:idx_phone_name (phone, username)
SELECT * FROM user WHERE phone = '13800138000'; -- SELECT * 需回表,未利用覆盖索引
问题原因

SELECT * 会查询所有字段,而组合索引仅包含 phoneusername,需通过回表获取其他字段(如 ageaddress),虽索引未失效,但多了一次索引扫描,效率下降。

优化方案
  • 只查询需要的字段(覆盖索引包含的字段):sql

    SELECT phone, username FROM user WHERE phone = '13800138000'; -- 无需回表,效率极高

3.7 验证索引是否失效

EXPLAIN 分析查询语句,重点看 typekey 字段:

  • typeALL:全表扫描(索引失效);
  • keyNULL:未使用任何索引(索引失效);
  • typeref/range/eq_ref:索引有效。

示例:

sql

复制代码
EXPLAIN SELECT * FROM user WHERE DATE(create_time) = '2024-01-01';
-- type: ALL,key: NULL → 索引失效

3.8 慢SQL优化/SQL调优

  • 偶尔慢:一般是第三方引起的
    • MQ消息挤压
    • 缓存三兄弟
    • 大文件导入导出
  • 一直慢:一般是mysql自己的问题
    • 借助于explain关键字分析,参见3.7的回答

3.9 分库分表

📎20230121直播PPT.pptx

1)当前遇到的问题

随着订单数据 的增加,当MySQL单表存储数据达到一定量时其存储及查询性能会下降,在阿里的《Java 开发手册》中提到MySQL单表行数超过 500 万行或者单表容量超过 2GB时建议进行分库分表,分库分表可以简单理解为原来一个表存储数据现在改为通过多个数据库及多个表去存储,这就相当于原来一台 服务器提供服务现在改成多台服务器组成集群共同提供服务,从而增加了服务能力。

阿里巴巴规范手册(嵩山版).pdf

这里说的500 万行或单表容量超过 2GB并不是定律,只是根据生产经验而言,为什么MySQL单表达到一定数量时性能会下降呢?我们知道为了提高表的查询性能会增加索引,MySQL在使用索引时会将索引加入内存,如果数据量非常大内存肯定装不下,此时就会从磁盘去查询索引就会产生很多磁盘IO,从而影响性能,这些和表的设计及服务器的硬件配置都有关,所以如果当表的数据量达到一定程度并且还在不断的增加就需要考虑进行分库分表了。

2)什么是分库分表

下边通过一个例子来说明什么是分库分表。

下边是一个电商系统的数据库,涉及了店铺、商品的相关业务。

随着公司业务快速发展,数据库中的数据量猛增,访问性能也变慢了,如何优化呢?

我们可以把数据分散在不同的数据库中,使得单一数据库的数据量变小来缓解单一数据库的性能问题,从而达到提升数据库性能的目的,如下图:将电商数据库拆分为若干独立的数据库,并且对于大表也拆分为若干小表,通过这种数据库拆分的方法来解决数据库的性能问题

分库分表就是为解决数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。

3)分库分表的四种方式

分库分表包括分库和分表两个部分,在生产中通常包括:垂直分库、水平分库、垂直分表、水平分表四种方式。

  1. 垂直分表

下图是商品查询列表:

用户在浏览商品列表时,只有对某商品感兴趣时才会查看该商品的详细描述。因此,商品信息中商品描述字段访问频次较低,且该字段存储占用空间较大,访问单个数据IO时间较长;商品信息中商品名称、商品图片、商品价格等 其他字段数据访问频次较高。

由于这两种数据的特性不一样,因此考虑将商品信息表拆分如下:

将访问频次低 的商品描述信息存放在一张表中,访问频次高的商品基本信息放在另一张表中(冷热分离)

垂直分表是将一个表按照字段分成多表,每个表存储其中一部分字段,比如按冷热字段进行拆分。

垂直分表带来的好处是:充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累。

通常我们按以下原则进行垂直拆分:

1、把不常用的字段单独放在一张表;

2、把text,blob等大字段拆分出来放在附表中(如头条的文章&文章配置表&文章内容表、订单&订单详情表);

3、经常组合查询的列放在一张表中;

4、垂直分库

通过垂直分表性能得到了一定程度的提升,但是还没有达到要求,并且磁盘空间也快不够了,因为数据还是始终限制在一台服务器,库内垂直分表只解决了单一表数据量过大的问题,但没有将表分布到不同的服务器上,因此每个表还是竞争同一个物理机的CPU、内存、网络IO、磁盘。

经过思考,他把原有的SELLER_DB(卖家库),分为了PRODUCT_DB(商品库)和STORE_DB(店铺库),并把这两个库分 散到不同服务器,如下图:

由于商品信息商品描述 业务耦合度较高,因此一起被存放在PRODUCT_DB(商品库);而店铺信息相对独立,因此 单独被存放在STORE_DB(店铺库)。

垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用,微服务架构下通常会对数据库进行垂直拆分,不同业务数据放在单独的数据库中,比如:客户信息数据库、订单数据库等。

它带来的提升是:

1、解决业务层面的耦合,业务清晰

2、能对不同业务的数据进行分级管理、维护、监控、扩展等

3、高并发场景下,垂直分库一定程度的提升IO、降低单机硬件资源的瓶颈。

垂直分库通过将表按业务分类(专业点就是领域模型DDD),然后分布在不同数据库,并且可以将这些数据库部署在不同服务器上,从而达到多个服务器共同分摊压力的效果,但是依然没有解决单表数据量过大的问题。

  1. 水平分库

经过垂直分库后,数据库性能问题得到一定程度的解决,但是随着业务量的增长,PRODUCT_DB(商品库)单库存储数据已经超出预估。粗略估计,目前有8w店铺,每个店铺平均150个不同规格的商品,再算上增长,那商品数量得往1500w+上预估,并且PRODUCT_DB(商品库)属于访问非常频繁的资源,单台服务器已经无法支撑。此时该如何优化?

再次分库?但是从业务角度分析,目前情况已经无法再次垂直分库。

尝试水平分库,将店铺ID为单数的和店铺ID为双数的商品信息分别放在两个库中。

也就是说,要操作某条数据,先分析这条数据所属的店铺ID。如果店铺ID为双数,将此操作映射至

RRODUCT_DB1(商品库1);如果店铺ID为单数,将操作映射至RRODUCT_DB2(商品库2)。

水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上,比如:单数订单在db_orders_0数据库,偶数订单在db_orders_1数据库。

它带来的提升是:

1、解决了单库大数据,高并发的性能瓶颈。

2、提高了系统的稳定性及可用性。

当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平分库了,经过水平切分的优化,往往能解决单库存储量及性能瓶颈。但由于同一个表被分配在不同的数据库,需要额外进行数据操作的路由工作,因此大大提升了系统复杂度。

  1. 水平分表

按照水平分库的思路把PRODUCT_DB_X(商品库)内的表也可以进行水平拆分,其目的也是为解决单表数据量大 的问题,如下图:

与水平分库的思路类似,不过这次操作的目标是表,商品信息及商品描述被分成了两套表。如果商品ID为双数,将此操作映射至商品信息1表;如果商品ID为单数,将操作映射至商品信息2表。此操作要访问表名称的表达式为商品信息[商品ID%2 + 1]

水平分表是在同一个数据库内,把同一个表的数据按一定规则(规则不对很容易导致数据分配不均匀,如按时间)拆到多个表中,比如:0到500万的订单在orders_0数据、500万到1000万的订单在orders_1数据表。

假设两张表也不够了呢,所以这里我们也可以将id跟表进行hash运算或取余,如一共5张表,就跟5取余得到0-4,0就存表1其余同理。但是这种处理的弊端就是如果后续要增加第六张表,还需要对当下所有表数据重新计算,进行表数据的迁移,成本会非常高,所以一般这种就提前规划好10年的数据量即可。

水平分表优化了单一表数据量过大而产生的性能问题

一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库,垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且持续增长,再考虑水平分库水平分表方案。

4)小结

项目为什么进行分库分表?

分库分表有哪些形式?

相关推荐
周杰伦_Jay3 小时前
【电商微服务日志处理全方案】从MySQL瓶颈到大数据架构的实战转型
大数据·mysql·微服务·架构
果子没有六分钟3 小时前
setprop debug.hwui.profile visual_bars有什么作用
android
合作小小程序员小小店3 小时前
桌面开发,在线%考试管理%系统,基于eclipse,java,swing,mysql数据库。
java·数据库·mysql·eclipse·jdk
vx_bscxy3223 小时前
告别毕设焦虑!Python 爬虫 + Java 系统 + 数据大屏,含详细开发文档 基于微信小程序的民宿预约系统22398 (上万套实战教程,赠送源码)
java·spring boot·mysql·微信小程序·课程设计
春生野草3 小时前
启动Nginx
java·微服务·架构
whltaoin5 小时前
【微服务中间件】RabbitMQ 多平台安装搭建实践指南(Windows_macOS_Ubuntu_Docker 全场景)
微服务·中间件·消息队列·rabbitmq·多平台
chxii5 小时前
第五章:MySQL DQL 进阶 —— 动态计算与分类(IF 与 CASE WHEN)多表查询
数据库·mysql
百***68046 小时前
MySQL四种备份表的方式
mysql·adb·oracle
不会c嘎嘎6 小时前
MySQL -- 库的操作
数据库·mysql