场景题
阅读提示
建议先看题目目录,再按“概念 -> 原理 -> 场景 -> 优化”顺序复习。
每题先讲结论,再补关键机制和项目实践,回答会更稳。
本文的具体机制以 MySQL 8.4 / InnoDB、Redis 7.x、RabbitMQ 4.4、Maven 3.9.x 和 Caffeine 3.x(Java 11+)为适用范围;其他组件需结合所用版本确认 API 与投递语义。代码为机制示意,未在数据库或中间件环境运行。
1、如何设计一个订单号生成服务
- 看数据量的大小:在设计订单号生成服务时,需要充分考虑数据量的大小,以确定生成的ID所需的位数。这可以避免位数不足导致存储问题,同时确保足够的位数以防止影响索引检索效率。
- 不重复、唯一性:订单号应有数据库唯一约束兜底。UUID 是概率唯一;Snowflake 依赖节点编号不冲突,并处理时钟回拨和序列耗尽;数据库序列或 Redis 自增还需评估故障恢复、持久化和主从切换时的重复风险,不能仅凭算法名称承诺全局唯一。
- 考虑高可用:通过多节点部署、负载均衡、健康检查等技术手段,确保服务在面对故障时能够保持可用状态,避免影响其他服务。
- 易于理解:为了方便排查问题,需要考虑到订单号的格式和组成,可以使用时间戳、随机数、用户ID等信息来拼接订单号。
- 安全性:不能让别人通过订单号看出公司的经营状况,比如你的订单就是流水号的话,那么别人就可以从订单号推测出你公司的整体运营状况。
2、订单超时自动取消功能如何设计
- 定时任务:通过定时任务隔一段时间扫表,对于过期超时的订单数据更新状态为已取消。这种方式容易实现;缺点就是定时任务是固定时间间隔的,所以一些订单并不能及时到点的去立马取消,可能过期了一两分钟才被定时任务扫描到;而且这种方式也增加了数据库的压力,当表里数据越来越多之后,扫描的时间也会越来越长。所以这种方式适合对时间要求不高的,数据量也不是很多的业务场景。
- DelayQueue延迟队列:DelayQueue是JDK提供的一个无界队列,订单生成以后,设置过期时间放入定义好的DelayQueue,然后创建一个线程,在线程中通过
while(true)不断的从DelayQueue中获取过期的数据。这种方式不依赖数据库,也不依赖其他组件,实现也方便;但是DelayQueue是一个无界队列,如果放入的订单数据过多,就会造成OOM。所以这种方式适合数据量比较小的,万一丢失了影响也不大的业务场景。 - Redisson:
RDelayedQueue可将任务延迟转入目标队列,具体 API 以项目使用的 Redisson 版本为准。Lua 能保证单次脚本执行期间不被其他命令穿插,但不能保证业务处理不重复;消费者仍需处理重试、宕机与幂等。 - MQ延迟消息:订单创建后发送延迟任务,到期再查数据库中的订单状态。RocketMQ 的延迟能力和精度取决于版本;RabbitMQ 可用 TTL + 死信交换机构造延迟链路,不一定需要插件。容量取决于部署、消息大小和保留时间,不能笼统承诺“万亿级”。需处理订单落库与发消息之间的失败窗口,例如事务内写 outbox,再可靠投递、重试和对账。
- MQ的死信队列:RabbitMQ 4.4 中,消息 TTL 到期、长度超限或消费者拒绝且不重新入队等情形可触发死信;需配置死信交换机及路由。延迟队列中放的是超时检查任务,支付行为不直接消费这条任务。不同 TTL 混排时,过期消息可能要到队首才被清理和死信化,不能当作精确定时器。参见 RabbitMQ TTL。
- Redis过期监听:
notify-keyspace-events Ex开启过期事件通知,但 Redis 7.x 的通知基于 Pub/Sub,断线期间消息会丢失;事件在实际删除键时产生,不保证 TTL 到点立即触发,且只携带键名。适合作为辅助信号,不能作为取消订单的唯一可靠来源。参见 Redis 键空间通知。
超时任务可能重复或晚到,支付回调也可能同时执行。以 MySQL 8.4 / InnoDB 为例,把状态与截止时间放进同一条条件更新:
UPDATE orders
SET status = 'CANCELLED'
WHERE id = ? AND status = 'PENDING' AND expire_at <= CURRENT_TIMESTAMP;只有影响行数为 1 才是本次成功取消;支付回调也须从 PENDING 条件迁移,避免先读后写覆盖终态。若取消还要释放库存,可在同一数据库事务内完成,跨服务则记录可靠事件并幂等执行。支付平台已扣款而本地订单已取消时,还需查单和退款补偿。定期按状态、到期时间索引扫描作为兜底。InnoDB 的更新会加锁并判断更新条件,参见 锁定读与并发更新。
3、如何设计一个购物车功能
- 购物车系统的主要功能是在用户选择商品后,在下单之前保存用户的意向商品、数量等信息,以便用户进行统一支付。购物车的操作主要包括加入购物车、查看购物车以及通过购物车下单等。
- 一般情况下,在存储购物车信息时,并不需要保存商品的所有详细信息,只需记录一个SKU ID,并加上数量、时间等字段即可。商品的库存、价格、介绍等信息可以在渲染购物车时实时查询和计算。
- 对于未登录用户,购物车信息可以临时缓存在客户端,使用Cookie或LocalStorage等技术进行存储。以下是一个JSON格式的未登录用户购物车存储示例:
{
"cart": [
{
"SKUID": 10086,
"timestamp": 1666513990,
"count": 2
},
{
"SKUID": 10010,
"timestamp": 1666513990,
"count": 10
}
]
}- 已登录用户的购物车需要使用持久化存储,可以选择数据库或Redis缓存。如果使用数据库,可以创建表格存储
user_id、sku_id、count、time_stamp等业务字段。如果使用Redis,可以在未登录用户购物车的基础上添加user_id作为key,如下所示:
{
"KEY": 12343123,
"VALUE": [
{
"SKUID": 10086,
"timestamp": 1666513990,
"count": 2
},
{
"SKUID": 10010,
"timestamp": 1666513990,
"count": 10
}
]
}- 使用Redis和数据库各有优势:Redis性能更高,响应时间更短,能支撑更多并发请求;而MySQL具有较高的数据可靠性和支持丰富的查询方式和事务机制。
4、如果让你设计一个秒杀系统,怎么设计?
一个秒杀系统面临着多个独特的挑战,设计的时候应该考虑以下问题:
- 高并发瞬时流量:
在处理秒杀场景的高并发瞬时流量时,系统的稳定性和可用性成为关键挑战。通常,我们会在整体架构设计上采取逐层的流量过滤策略,确保系统能够有效处理巨大的请求压力。
客户端可通过按钮防重复点击、退避重试等方式减少无效请求,但攻击者能绕过客户端,真正的准入控制和限流必须由网关及服务端执行。
接下来,通过CDN(内容分发网络)和Nginx进行统一接入,不仅可以实现负载均衡和流量分发,还可以配置黑白名单、IP限流等策略,进行更细粒度的流量过滤。
在服务端,通过Nginx配置的流量过滤和前端的丢弃策略,可以减轻服务端的压力。服务器层面也可以配置各种限流策略,例如基于Sentinel的动态限流,自定义的限流算法等。
此外,针对一些查询操作和写操作,可以使用缓存来提高性能。在缓存方面,本地缓存的响应速度更快,近端缓存效果优于远端缓存,可以有效抵挡部分请求。
由于秒杀业务本身允许一定的失败率,通过层层过滤,我们可以在前端、Nginx、服务器等层次上过滤更多的流量,提升系统的稳定性,确保在高并发情况下仍能提供可用的服务。
- 热点数据:
秒杀系统常面临热点数据问题,特别是在大家抢购同一商品时,该商品成为热点数据。这带来了高频的查询请求和非常频繁的写请求,尤其是在库存扣减等操作上。
针对热点数据,主要的解决方案包括拆分和缓存。
首先是拆分,即将热点数据分割成多个相对冷门的数据,并通过负载均衡将不同请求分散到不同的数据上,以降低单一数据的请求压力。
其次是利用缓存。通常,对于秒杀业务,可以提前预知哪些数据会成为热点,因此可以提前进行缓存的预热。这不仅包括在Redis等缓存中的预热,还应在本地缓存上执行预热,以避免Redis的热key问题。在处理热点数据时,充分利用缓存可以有效减轻数据库的负担,提高系统的响应速度。
通过拆分和缓存的组合应用,能够有效缓解热点数据带来的高并发压力,提升系统的稳定性和性能。
- 数据量大:
秒杀系统经常面临高频的下单操作,导致最终产生的订单量巨大,进而带来了查询效率低的问题。
针对这个挑战,有几种可行的解决方案:
- 加缓存: 在查询订单时可以引入缓存,将常被查询的订单数据缓存在内存中,以提高查询效率。可以使用分布式缓存如Redis等。
- 使用Elasticsearch(ES): 当订单数据量庞大时,可以考虑使用Elasticsearch进行存储和检索。ES对于大规模文档的全文搜索和分析非常高效,适用于订单数据的查询。
- 分库分表: 当单一数据库无法承载巨大的订单数据时,可以考虑采用分库分表的方式,将订单数据分散存储在多个数据库或表中,提高查询性能。
- 数据归档: 对于历史订单数据,可以实施数据归档策略,将过早的历史数据归档到冷数据存储中,减轻热数据存储的压力。这样可以保证查询性能,同时降低存储成本。
选择适当的方案取决于具体业务需求和系统架构。可能需要综合考虑缓存的实时性、ES的搜索能力、分库分表的扩展性以及数据归档的存储成本等因素。
- 库存的正确扣减:
在秒杀业务中,由于高频的并发库存扣减操作,提升库存扣减的性能并确保准确性是一项极为重要的任务。稍有不慎可能导致超卖、少卖等严重问题。
为了解决这个问题,可以采取以下策略:
- 优化数据库事务: 通过合理设置数据库事务隔离级别,减小事务的范围,降低锁竞争,提高并发处理能力。合理使用事务的提交点,确保扣减库存和生成订单等关键操作的原子性。
- 使用乐观锁: 在数据库层面引入乐观锁机制,通过版本号或时间戳等方式进行标记,减少锁的争用,提高并发性能。热点库存冲突率高时,乐观锁重试也会放大压力,应限制重试次数并按负载选择方案。
- 分布式锁: 对于分布式系统,可以引入分布式锁,确保同一时刻只有一个请求能够扣减库存。分布式锁可以有效避免并发写入冲突。
- 异步扣减: 将库存扣减操作异步化,通过消息队列等方式将扣减请求发送到异步处理队列中。这样可以降低同步扣减的压力,提高系统整体吞吐量。
- 缓存预减:在 Redis 内通过 Lua 原子执行“检查可用库存不少于购买量,再扣减”;库存不足直接拒绝,不能等库存为负才到数据库扣减。预留成功后还需可靠落单,失败补偿和超时释放必须幂等。脚本原子性不等于 Redis 与数据库之间的事务原子性,参见 Redis Lua 执行机制。
- 定时任务回补:只对已成功预留但最终取消或失败的订单释放库存,以预留记录和状态迁移作为依据,重复任务不能重复回补;未扣减成功的请求不应增加库存。
以上策略的选择应根据具体业务需求和系统架构来决定。综合考虑事务隔离级别、数据一致性、系统复杂度等因素,可以设计出更高效和稳定的库存扣减方案。
- 黄牛抢购:
在秒杀业务中,由于商品存在差价,吸引了大量黄牛参与抢购,因此如何识别并阻挡这些黄牛的下单行为显得至关重要。
首先,防刷的核心在于检测可能是黄牛的用户,这涉及到风控领域,通常需要借助算法模型。通过分析用户的IP、设备信息、网络数据、行为模式等,可以推算出可能是黄牛的用户。
一旦识别出潜在的黄牛用户,需要对他们的流量进行防控。这可以通过以下手段实现:
- 用户ID黑名单: 将识别出的黄牛用户ID加入黑名单中。Nginx和业务系统都可以在流量过滤时检查黑名单,如果用户在黑名单中,直接拒绝请求。
- IP地址、设备限流: 对于黄牛用户的IP地址和设备进行限流。例如,限制某个IP地址在一段时间内只能下单几次。这可以利用令牌桶、漏桶等限流算法来实现。
- 借助限流工具: 利用诸如Nginx、Sentinel、Guava等限流工具,直接在系统层面进行流量控制。这些工具提供了强大的限流功能,能够有效防止过量的请求。
通过综合利用黑名单、限流算法和专门的防刷工具,可以提高系统对黄牛的防控能力,确保秒杀活动的公平性和稳定性。
- 重复下单:
在秒杀业务中,用户频繁尝试下单支付可能导致重复下单问题,占用额外库存,最终导致少卖的情况。因此,需要采取一些手段来避免重复下单。
可给同一次业务请求分配幂等键,服务端通过唯一约束或原子写入绑定请求与订单,重试时返回原订单。页面 token 相同本身不能保证幂等,还需服务端原子处理并校验请求参数一致。
此外,在秒杀业务中通常存在限购策略,因此可以结合业务场景,判断用户是否已有在途订单,通过限购方式来防止用户重复下单。
然而,上述过程中可能存在并发情况,导致token检测和订单重复性判断时的并发问题。幂等性不必依赖显式分布式锁,也可由数据库唯一约束与事务保障。
通过引入锁机制,可以确保在并发场景下,同一用户不会重复下单。可以采用分布式锁,数据库乐观锁,或者业务层面的锁机制,并结合订单唯一约束、事务和失败恢复,防止重复扣减库存和生成重复订单。这有助于维护系统的稳定性和数据的准确性。
- 对普通交易的影响:
秒杀通常作为电商网站的一个功能,往往与其他业务一同部署,包括服务器、缓存和数据库等资源。为避免秒杀业务对普通交易造成过大的影响,需要考虑隔离策略。
隔离可以采用逻辑隔离和物理隔离两种方式,具体实施视业务需求而定。
- 物理隔离: 物理隔离是指将秒杀业务的前后端服务以及数据存储完全分离。这样可以确保秒杀活动对其他业务的影响最小化。前后端服务可以单独部署,甚至使用不同的服务器集群,以确保资源独立。
- 逻辑隔离: 在应用层面进行逻辑隔离,即通过对数据进行标记或打标签的方式来区分秒杀订单和普通订单。例如,在订单和商品数据上添加标记,明确标识出哪些是秒杀订单。这样方便查询和分析,但标签本身不隔离资源竞争;还需独立线程池、连接池配额、队列和限流,防止秒杀耗尽普通交易资源。
综合考虑业务场景和系统复杂度,可以选择适当的隔离策略。物理隔离更彻底,但成本和复杂度也更高。逻辑隔离则相对灵活,能够在业务层面实现隔离效果,同时降低了部署和维护的难度。
5、应用程序中存在包冲突的情况下,怎么发现和解决
包冲突是指在应用程序中引入的不同版本的库或依赖之间存在冲突,可能导致编译错误、运行时异常或其他不稳定行为。解决包冲突通常需要以下步骤:
- 诊断冲突: 确定哪些包发生了冲突,以及冲突的具体原因。可以通过查看编译日志、运行时异常信息或使用工具来分析依赖关系。
- 查看依赖关系: 使用构建工具或依赖管理工具查看项目的依赖关系。对于Java项目,可以查看Maven的依赖配置文件。了解每个依赖项的版本以及它们之间的关系。
- 升级或降级依赖版本: 如果发现包冲突是由于不同依赖项使用了相同库的不同版本而引起的,可以考虑升级或降级相关依赖项的版本,以便它们使用相同的库版本。
- 排除依赖项: 对于某些依赖项,可以使用排除(exclusion)机制来排除特定的传递性依赖项。这样可以防止冲突的库被引入。
- 在Maven中,可以使用
<exclusions>元素:
<dependency>
<groupId>example</groupId>
<artifactId>example-artifact</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>conflicting-group</groupId>
<artifactId>conflicting-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>- 使用类加载器隔离: 对于较为复杂的场景,可以考虑使用类加载器隔离机制,将不同版本的库加载到不同的类加载器中。这样可以在同一应用中使用不同版本的库而不会发生冲突。但需要注意,类加载器隔离可能引入新的问题,需要谨慎使用。
- 统一兼容版本:优先遵循框架 BOM 和兼容矩阵,通过
dependencyManagement统一版本;不要盲目把所有依赖升级到最新版。 - 使用依赖检查工具:Maven Enforcer 的
dependencyConvergence规则可检查依赖收敛,但不是依赖锁文件。Maven 3.9.x 通常按“路径最近优先,同深度先声明优先”仲裁传递依赖;dependencyManagement管理版本并不自动引入依赖。可用mvn dependency:tree -Dverbose查看实际解析路径,定位运行时NoSuchMethodError等问题后再排除或统一版本。参见 Maven 依赖机制。
6、跨节点join查询如何实现
在分布式系统中,跨节点的Join查询是一个具有挑战性的问题,因为传统的关系数据库系统通常设计为在单一节点上执行Join操作。在分布式环境下,有一些常见的解决方案:
- 数据复制: 将涉及到Join操作的表数据在分布式环境中的每个节点上都复制一份。这样,每个节点可以在本地执行Join操作而无需跨节点通信。这种方法虽然简单,但会导致数据冗余和一致性的问题。
- 分布式Join算法: 开发专门的分布式Join算法,使得在分布式环境下也能高效执行Join操作。例如,MapReduce框架就提供了一些分布式Join的实现。这需要对数据进行划分和分布式计算。
- 使用分布式数据库: 选择支持分布式Join的分布式数据库系统。一些分布式数据库提供了内建的分布式Join支持,可以在分布式环境中高效地执行Join查询。
- 分片和合并: 将Join操作分解为可在每个节点上执行的部分,然后在一个中心节点上合并结果。这种方式需要设计合适的数据分片策略和结果合并机制。
- 数据缓存: 在每个节点上缓存可能用于Join的数据片段,以减少跨节点通信的需求。这需要根据查询模式预先缓存可能用到的数据。
- 消息传递: 使用消息传递机制,将涉及Join的查询分解为子查询,分发到各个节点执行,然后将结果进行合并。这需要有效的通信机制和任务协调。
- 全局索引: 在所有节点上建立全局索引,以加速Join查询。这需要维护一致的全局索引,可能带来额外的维护成本。
7、Excel中百万级数据怎么导入到数据库
在处理百万级数据从Excel读取并插入到数据库的场景中,涉及到多方面的问题,需要综合考虑性能、内存占用、错误处理等因素。以下是一个可能的解决方案:
- 内存溢出问题
使用流式读取方式,分批处理Excel数据。可使用 Apache POI 5.x 的 XSSF SAX/Event API 读取 .xlsx;SXSSF 主要用于流式写出,不是流式读取接口。现有 EasyExcel 项目也应采用监听器分批处理,避免在回调中累积全部行;流式读取并不能消除共享字符串、业务缓冲等内存开销。参见 Apache POI 读取与写出说明。
- 性能问题
考虑多线程处理,使用线程池同时读取不同的sheet,以提升并发性能。对于数据插入操作,借助多线程和数据库的批量插入功能,进一步提高性能。
- 错误处理
- 数据检查: 在读取和插入之前进行数据检查,处理数据格式错误、不一致等问题。
- 异常处理: 在插入过程中,对异常进行处理。采用自动重试机制,一定次数内尝试重新插入。记录日志以便后续分析。
- 数据重复处理: 设置Excel中某几个字段为数据库唯一性约束,处理数据重复问题。可以选择覆盖、跳过或报错,根据业务需求选择合适的方式。
整体方案
- 用事件式读取逐行解析 Excel;默认单线程读取,确需并发时使用独立读取资源,避免多个线程共享非线程安全的工作簿对象。
- 数据分批插入,设置一个批次大小,当达到批次大小时执行一次批量插入操作。
- 使用有界队列(如指定容量的
ArrayBlockingQueue)连接读取与写入,写入变慢时阻塞生产者形成背压;无界的ConcurrentLinkedQueue即使线程安全也可能耗尽内存。失败重试需有导入批次/行标识,避免已提交批次被重复写入。
通过这种方案,能够在相对较短的时间内处理百万级数据的读取和插入,同时保证了内存的有效利用和错误处理的可靠性。具体的实现可以基于以上思路,结合实际项目需求和技术栈进行调整。
8、缓存预热怎么实现
在系统中进行缓存预热是为了提高系统性能和响应速度的一种策略。常见的缓存预热方案包括:
1. 启动过程中预热
一种常见的实现方式是在系统启动时进行缓存预热,特别适用于本地缓存。在Spring应用程序启动时,可以通过监听应用启动事件或在初始化阶段,将需要缓存的数据加载到缓存中。Spring提供了多个事件和扩展点,如ApplicationReadyEvent、CommandLineRunner、InitializingBean、@PostConstruct等,可用于执行预热逻辑。
2. 定时任务
定时任务是在系统运行过程中通过设定的时间间隔更新缓存,以确保缓存中的数据保持最新。在Spring中,可以使用@Scheduled注解轻松实现定时任务,适用于需要动态更新的情况。
3. 用时加载
请求到达后才加载属于懒加载,会让首次请求承担回源延迟;严格意义的预热是在流量到来前主动加载热点。示例代码如下:
public Data fetchData(String key) {
// 先检查缓存中是否存在数据
Data cachedData = cache.get(key);
if (cachedData == null) {
cachedData = loadFromSource(key); // 业务数据源
if (cachedData != null) {
cache.put(key, cachedData);
}
}
return cachedData;
}4. 缓存加载器
某些缓存框架提供了缓存加载器的机制,当缓存中不存在数据时,自动调用加载器加载数据到缓存中。例如,在Caffeine中可以使用LoadingCache实现,简化了缓存预热的逻辑。示例代码如下:
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class MyCacheService {
private final LoadingCache<String, String> cache;
public MyCacheService() {
this.cache = Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES) // 写入满1分钟后,再次访问时触发异步刷新
.build(key -> loadDataFromSource(key)); // 使用加载器加载数据
}
public String getValue(String key) {
return cache.get(key);
}
private String loadDataFromSource(String key) {
return "value-for-" + key; // 示例占位,实际应查询数据源
}
}Caffeine 3.x 的 refreshAfterWrite 只使条目具备刷新资格,访问才触发异步刷新,期间通常返回旧值;不是每分钟自动扫描刷新。LoadingCache 构造时也不会自行加载全部数据,主动预热需调用 getAll(hotKeys) 等。参见 Caffeine Refresh。
9、表数据量大的时候只能分库分表?
当MySQL中的单表数据量庞大时,考虑分库分表通常是为了提升数据库吞吐量和查询效率。然而,分库分表方案的实现较为复杂,且可能引发一系列问题,如跨库事务、分页查询等。在采取分库分表之前,应优先考虑以下方案:
- 数据库优化: 通过优化数据库结构、使用合适的索引、调整SQL语句等手段,可以有效提升查询性能。在2000万数据量级别,合理使用索引和进行表结构设计优化能够显著改善数据库性能。
- 缓存: 减轻数据库压力可以通过缓存实现。在近端引入本地缓存或分布式缓存,能够迅速返回结果并减轻数据库负担,提高系统性能。
- 分区:MySQL 8.4 分区仍是同一逻辑表,不减少总数据量或自动扩容到多台服务器。收益依赖查询条件能否触发分区裁剪,以及按分区归档/删除等维护需求;应结合
EXPLAIN验证实际访问分区。参见 MySQL 分区裁剪。 - 数据归档: 定期清理不再需要或不再活跃的数据,将其归档到辅助存储中(如历史表、离线数仓),可以减少数据量,进而提升数据库效率。
- 分布式数据库: 考虑采用分布式数据库系统,充分利用多个节点分散数据,提高系统性能和容量。尽管分布式数据库成本较高,但相较于分库分表的改造而言,是否有收益需要根据事务语义、访问模式、迁移成本和实测结果判断。
这些方案在实施时需要根据具体业务场景和需求进行综合考虑,选择最适合的优化方案,以达到性能提升的效果。
10、线上发现一个接口响应很慢,怎么排查问题
- 监控工具:可以使用性能监控工具来实时监控接口的响应时间、吞吐量和错误率等指标。常见的监控工具:Arthas、Prometheus、Grafana、AppDynamics等。
- 日志分析:查看接口的日志文件,关注接口的执行时间、请求参数和响应结果等消息。同构分析可以找到执行时间较长的接口,以及可能导致慢接口的原因。
- 接口性能测试:通过模拟多用户并发请求,观察接口的响应时间和吞吐量等指标,找出慢响应的接口。常见的性能测试工具:JMeter、LoadRunner等。
- 数据库查询分析:如果接口涉及数据库查询,通过数据库查询分析工具(如EXPLAIN语句)来检查查询执行计划,确保数据库查询效率。
- 代码剖析:对接口代码进行剖析,了解代码中的性能瓶颈和耗时操作,通过剖析结果找到影响性能的代码段,进行优化,常见的剖析工具:Java Profiler、VisualVM等
11、MQ里面数据到达1000万的时候,怎么尽快消费掉
- 增加消费者数量: 消息堆积了,消费不过来了,那就把消费者的数量增加一下,让更多的实例来消费这些消息。
- 提升消费者消费速度:排查数据库、外部调用和锁等待等瓶颈,采用受控并发和批处理。业务提交成功后再确认;仅放进内存线程池就返回成功,会在进程崩溃时丢任务。若先落本地持久化任务表,需保证可恢复扫描和幂等执行。RabbitMQ 4.4 的确认语义参见 消费者确认。
- 降低生产者的生产速度:如果生产者可控的话,可以让生产者生成消息的速度慢一点。
- 清理过期消息:有一些过期消息、或者一直无法成功的消息,在业务做评估之后,如果无影响或者影响不大其实是可以清理的。
- 调整 RocketMQ 的配置参数: RocketMQ 提供了很多可配置的参数,例如消息消费模式、消息拉取间隔时间等,可以根据实际情况来调整这些参数,从而优化消息消费的效率。
- 增加Topic队列数: 如果一个 Topic 的队列数比较少,那么就容易出现消息堆积的情况。可以通过增加队列数来提高消息的处理并发度,从而减少消息堆积。
12、涉及到订单超卖问题怎么解决
订单超卖:就是当有两个并发线程同时查询库存商品,这时数据库中商品库存就剩1个,所以两个线程得到的库存数量都是1,然后经过库存校验之后分别开始进行库存扣减,最终导致库存被扣减成负数。
- 悲观锁:在同一显式事务内执行
SELECT ... FOR UPDATE、校验和扣减,事务提交或回滚后释放锁;仅在自动提交模式执行一次查询,不能保护后续独立更新。 - 乐观锁:更新条件同时校验版本和
stock >= quantity,成功后递增版本;版本匹配本身不代表库存充足,冲突时应有界重试或返回失败。 - 限流:对订单的创建进行限流,防止短时间内大量订单的涌入,从而减小并发冲突的可能性。可以使用令牌桶算法或漏桶算法来实现限流;
限制同一IP在一定时间内只能发送一个请求。可以通过在网关中自定义全局过滤器来实现IP限流;
限制接口每秒只接受一定数量的请求。可以使用Sentinel的限流功能;
- 库存预留:在用户下单时,先检查库存是否足够,并将一部分库存标记为已预留状态。在订单支付完成后再确认扣减库存。
- 缓存:单条
DECRBY或负数INCRBY虽然原子,却允许扣成负数;检查与扣减应放进同一 Lua 脚本,并处理缓存与数据库之间的可靠同步和补偿。 - 消息队列:异步处理可以削峰,但多消费者不天然串行,全局有序也不是默认保证。仍需原子扣减、幂等处理与失败补偿;限流只是减压,不能代替库存正确性约束。
单库库存扣减可先采用下面的条件更新(MySQL 8.4 / InnoDB,sku_id 为主键,购买量必须为正整数):
UPDATE inventory
SET stock = stock - ?
WHERE sku_id = ? AND stock >= ?;
-- 第一个和第三个参数都是同一购买量;影响行数为 1 才允许继续创建订单。检查与扣减在同一条更新内完成,InnoDB 通过记录锁协调并发,避免两个请求都根据旧库存通过检查。扣减和订单写入应在同一事务内提交,写单失败则回滚;幂等键唯一约束防止同一请求重复扣减。跨库订单不能直接套用本地事务结论,需要库存预留、可靠消息与补偿机制。
