学习目标
- 能够说出xxl-job任务调度的优势
- 能说出xxl-job的组成结构
- 能够编写热点商品更新缓存任务
- 能够说出什么是幂等,常见幂等解决方案
- 能够说出数据库常见存储引擎的核心区别
- 能够说出索引失效的场景
- 能够说出回表、覆盖索引
- 能够说出SQL调优的方案
- 能够说出常见分库分表的方案
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/
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 幂等场景及解决方案
以下场景代码均已准备,如下
注意:下述的解决方案,不一定只能解决当前场景下的,比如:数据库唯一索引一样可以解决消息重复消费,这里借助不同场景,便于大家学习多种解决方案。
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 回滚日志),数据一致性有保障 |
不支持崩溃恢复,崩溃后可能出现数据碎片或损坏,需通过 工具修复 |
数据文件 |
单表独立文件(.ibd)+ 系统表空间文件(ibdata1)+ redo log 文件(ib_logfile0/1) |
表结构文件(.frm)+ 数据文件(.MYD)+ 索引文件(.MYI),文件结构简单 |
存储特性 |
支持数据压缩(需开启特定配置)、分区表、MVCC(多版本并发控制,读不加锁) |
支持压缩表(COMPRESSED 格式)、空间索引(GIS 数据),无 MVCC 机制 |
全文索引 |
MySQL 5.6+ 支持全文索引,性能与功能逐步完善 |
早期版本唯一支持全文索引的引擎,全文索引功能较基础 |
读写性能 |
写性能优秀(行锁 + 事务优化),读性能略逊于 MyISAM,但整体均衡 |
读性能极优(索引缓存高效),写性能极差(表锁导致阻塞) |
内存占用 |
主要依赖 (缓存数据 + 索引),内存占用较高 |
主要依赖 (仅缓存索引),内存占用较低 |
适用场景 |
核心业务表(订单、支付、用户数据)、高并发读写、需事务 / 外键的场景 |
只读 / 读写比例极高的静态数据(博客、新闻)、日志表、统计报表(无并发写入) |
版本支持 |
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,影响性能