我没有高并发项目经验,但是面试的时候经常被问到高并发、性能调优方面的问题,有什么办法可以解决吗?

5016 关注132 万浏览收录于 · 技术急诊室:从线上故障到架构实战

1467 人赞同了该回答

拿一个最普通的 Spring Boot 项目。后台管理系统,几张表的增删改查,日活可能三位数。默认配置启动,Tomcat 200 个线程, HikariCP 10 个数据库连接,啥也没调过。能跑,没人投诉。

现在打开 JMeter,200 个线程同时请求你的订单列表接口。

jmeter -n -t order_list_test.jmx -l result.jtl -e -o report/

报告出来了:平均响应时间 4200ms,吞吐量 38 req/s,错误率 12%。四秒多才返回一个列表,一秒才处理 38 个请求,还有八分之一直接报错了。

这就是你的”高并发经验”的起点。

先找谁在拖后腿

大部分人看到这个结果,第一反应是调大 Tomcat 线程数。这是错的。线程数不是瓶颈的时候,调到 2000 也没用——线程全卡在别的地方排队呢。

用 Arthas 看一下这个接口内部每一步的耗时:

java -jar arthas-boot.jar

trace com.example.OrderController listOrders

输出会告诉你 listOrders 里面调了什么方法、每个方法花了多久。假设你看到的是这样:

+---[3856ms] com.example.OrderService.listOrders()
    +---[3ms] com.example.OrderMapper.selectPage()
    +---[3851ms] com.example.OrderService.fillOrderDetails()

3856 毫秒里有 3851 毫秒花在了 fillOrderDetails 。翻开这个方法一看——订单列表查出来 20 条数据,然后 for 循环每一条去查用户信息、商品信息、物流信息。三个查询乘以 20 条,60 次数据库调用,每次几十毫秒。

这就是经典的 N+1 问题 。一次列表查询变成了 61 次数据库查询。

改法很直接:把 for 循环里的单条查询改成批量查询。先收集所有 userId,一次 WHERE user_id IN (...) 查出来,再在内存里匹配。

List<Long> userIds = orders.stream()
    .map(Order::getUserId)
    .distinct()
    .collect(Collectors.toList());
Map<Long, User> userMap = userService.listByIds(userIds)
    .stream()
    .collect(Collectors.toMap(User::getId, Function.identity()));

for (Order order : orders) {
    order.setUserName(userMap.get(order.getUserId()).getName());
}

商品信息、物流信息同理。61 次查询变成 4 次。再跑一次 JMeter:平均响应时间从 4200ms 降到 320ms,吞吐量从 38 涨到 280 req/s。

这一步优化完,面试的时候你能聊什么?N+1 问题的识别和解决、批量查询的写法、 IN 子句数量太大时需要分批(MySQL 默认没有硬限制,但超过几千个值查询计划可能不走索引)、MyBatis 的 <foreach> 标签怎么写。一个优化动作背后能展开一大片。

数据库连接池是第二个瓶颈

280 req/s 看起来不错了,但 JMeter 报告里有个数字不太对:TP99 响应时间 1800ms。平均 320ms 是因为大部分请求很快,但最慢的那 1% 要将近两秒。

再看 HikariCP 的日志,会发现大量这种输出:

HikariPool-1 - Connection is not available, request timed out after 30000ms

默认 10 个连接,200 个并发线程在抢。抢到连接的很快处理完了,没抢到的在等。等 30 秒还没等到就直接超时报错——之前那 12% 的错误率就是这么来的。

spring:
  datasource:
    hikari:
      maximum-pool-size: 30
      minimum-idle: 10
      connection-timeout: 3000

连接数调到 30,超时时间从 30 秒改成 3 秒(等不到就快速失败,别让用户傻等)。再跑一次:错误率降到 0,TP99 从 1800ms 降到 450ms。

但连接池不是越大越好。HikariCP 官方有一篇 “About Pool Sizing”,结论让很多人意外:连接数 ≈ CPU 核心数 * 2 + 磁盘数。4 核机器大概 10~15 个连接就到头了。再多数据库那头反而更慢——每个连接要占内存、维护锁状态,连接太多数据库自己先撑不住。

这里面试能聊的东西:连接池的工作原理、为什么连接不是越多越好、 connection-timeout 设太长会导致线程一直占着不释放、连接泄漏怎么排查(HikariCP 的 leak-detection-threshold 参数)。

有些查询根本不用走数据库

商品信息一天更新一次,但每次请求订单列表都去查商品表。200 个并发请求,200 次查同样的商品数据。

@Cacheable(value = "product", key = "#productId")
public Product getProduct(Long productId) {
    return productMapper.selectById(productId);
}

加上 Redis 缓存之后,商品查询从数据库的几十毫秒变成 Redis 的 1~2 毫秒。吞吐量又上了一个台阶:380 req/s。

但缓存一加就有新问题了。运营改了商品价格,缓存里还是旧价格。用户看到的订单列表里显示的价格跟商品详情页不一样——这种 bug 比接口慢更要命。

最简单的处理:商品更新的时候删掉缓存。下次查的时候发现缓存没了,查数据库,回填缓存。

@CacheEvict(value = "product", key = "#product.id")
public void updateProduct(Product product) {
    productMapper.updateById(product);
}

但如果更新操作和查询操作并发了呢?线程 A 删了缓存,线程 B 紧接着查,发现缓存没了,去查数据库(拿到的还是旧值因为线程 A 的数据库更新还没提交),回填了旧值。线程 A 的数据库更新提交了,但缓存里已经被线程 B 塞了旧值。

这就是缓存一致性问题。延迟双删、订阅 binlog、设置较短的过期时间兜底——每种方案都有适用场景和局限性。面试聊到这里,已经不是”背八股文”了,是在讨论工程决策。

接口里的非核心逻辑

订单列表接口优化到这一步,已经够快了。但你翻了翻其他接口,发现下单接口有个问题:扣库存、创建订单、发短信通知、发邮件确认、加积分,全在一个同步方法里。

扣库存和创建订单必须同步——这是核心逻辑,失败了要回滚。但发短信要 500ms,发邮件要 300ms,加积分又要查一次数据库。用户点”下单”之后要等一秒多才看到结果,其中大半的时间花在了他根本不关心的事情上。

@Async("orderExecutor")
public void handlePostOrder(Long orderId) {
    smsService.send(...);
    emailService.send(...);
    pointsService.add(...);
}

非核心逻辑丢到异步线程池里。下单接口只做扣库存+创建订单,200ms 内返回。用户体验好了,Tomcat 线程占用时间也短了,能接更多请求。

如果非核心任务量大或者不能丢,再往前一步——用消息队列替代 @Async 。RabbitMQ 或 Kafka 把任务丢进队列,消费者慢慢处理。队列有持久化,服务重启了消息也不会丢。

面试聊到这里又能展开: @Async 的线程池溢出了怎么办( CallerRunsPolicy 还是直接拒绝)、消息队列的消息丢失怎么保证(publisher confirm + 持久化 + 手动 ack)、消息重复消费怎么处理(幂等性设计)。

回头看一眼数字

最初:38 req/s,4200ms 响应,12% 错误率。

改完 N+1、调了连接池、加了缓存、异步化之后:500+ req/s,平均响应 80ms,错误率 0。

整个过程用到的技术:批量查询、连接池调优、Redis 缓存、异步线程池。没有分布式锁,没有消息队列集群,没有分库分表,没有 K8s 自动扩容。全是最基础的优化手段,在一台开发机上就能跑通。

但你把这个过程在面试里从头讲一遍——每一步为什么这么做、发现了什么问题、用什么工具诊断的、改完效果怎么样——面试官听到的不是”背了多少知识点”,而是”这个人碰到性能问题知道怎么一步步排查和解决”。

这就是面试官想要的”高并发经验”。不是你的系统扛过多少 QPS,而是你有没有从”慢”到”快”完整走过一遍,知道每一步的原因和代价。

百万 QPS 的系统,全国做过的人可能一个会议室就坐满了。面试官自己大概率也没扛过。他想招的不是那种人,他想招的是给一个慢系统能把它搞快的人。你手头这个日活三位数的 CRUD 项目,压测一轮、优化一轮,就是一份完整的高并发实战经历。

还没有人送礼物,鼓励一下作者吧

所属专栏 · 2026-09-17 10:27 更新

技术急诊室:从线上故障到架构实战

[

花宝宝

互联网行业 从业人员

287 篇内容 · 56478 赞同

](https://zhuanlan.zhihu.com/c_1982067831853036046)

[

最热内容 ·

有哪些令人拍案叫绝的代码/优化?

](https://zhuanlan.zhihu.com/c_1982067831853036046)

发布于2026-03-06 13:33 ・北京

赞同 1467

更多回答

我是一名大厂技术总监,面试过几百人,题主的这个问题太常见了。 下面我结合我面试候选人的经验,以及我对高并发的理解,给你一个非常完整的建议 。如果能认真看完,我相信一定有所收获,至少应付面试没啥问题。 首先说一下,当我作为面试官问候选人 「对于高并发的理解」 时,我觉得 「答得不好」 的情况分成以下几类: 1、对数据化的指标没有概念 :不清楚选择什么样的指标来衡量高并发系统?分不清并发量和QPS,不知道自己系统的总用户量、活跃用户量,平峰和高峰时的QPS和TPS等关键数据。 2、设计过一些高并发方案,但是细节掌握不透彻 :讲不出该方案要关注的技术点和可能带来的副作用。比如读性能有瓶颈会引入缓存,但是忽视了缓存命中率、热点key、数据一致性等问题。 3、理解片面,把高并发设计等同于性能优化 :大谈并发编程、多级缓存、异步化、水平扩容,却忽视高可用设计、服务治理和运维保障。 4、掌握大方案,却忽视最基本的东西 :能讲清楚垂直分层、水平分区、缓存等大思路,却没意识去分析数据结构是否合理,算法是否高效,没想过从最根本的IO和计算两个维度去做细节优化。 下面,我再结合我的高并发项目经验,系统性地总结下高并发需要掌握的知识和实践思路,内容分成以下3个部分: 如何理解高并发? 高并发系统设计的目标是什么? 高并发的实践方案有哪些? 01 如何理解高并发? 高并发意味着大流量,需要运用技术手段抵抗流量的冲击,这些手段好比操作流量,能让流量更平稳地被系统所处理,带给用户更好的体验。 我们常见的高并发场景有:淘宝的双11、春运时的抢票、微博大V的热点新闻等。除了这些典型事情,每秒几十万请求的秒杀系统、每天千万级的订单系统、每天亿级日活的信息流系统等,都可以归为高并发。 很显然,上面谈到的高并发场景,并发量各不相同, 那到底多大并发才算高并发呢? 1、不能只看数字,要看具体的业务场景。不能说10W QPS的秒杀是高并发,而1W QPS的信息流就不是高并发。信息流场景涉及复杂的推荐模型和各种人工策略,它的业务逻辑可能比秒杀场景复杂10倍不止。因此,不在同一个维度,没有任何比较意义。 2、业务都是从0到1做起来的,并发量和QPS只是参考指标,最重要的是:在业务量逐渐变成原来的10倍、100倍的过程中,你是否用到了高并发的处理方法去演进你的系统,从架构设计、编码实现、甚至产品方案等维度去预防和解决高并发引起的问题?而不是一味的升级硬件、加机器做水平扩展。 此外,各个高并发场景的业务特点完全不同:有读多写少的信息流场景、有读多写多的交易场景, 那是否有通用的技术方案解决不同场景的高并发问题呢? 我觉得大的思路可以借鉴,别人的方案也可以参考,但是真正落地过程中,细节上还会有无数的坑。另外,由于软硬件环境、技术栈、以及产品逻辑都没法做到完全一致,这些都会导致同样的业务场景,就算用相同的技术方案也会面临不同的问题,这些坑还得一个个趟。 因此,这篇文章我会将重点放在基础知识、通用思路、和我曾经实践过的有效经验上,希望让你对高并发有更深的理解。 02 高并发系统设计的目标是什么? 先搞清楚高并发系统设计的目标,在此基础上再讨论设计方案和实践经验才有意义和针对性。 2.1 宏观目标 高并发绝不意味着只追求高性能,这是很多人片面的理解。从宏观角度看,高并发系统设计的目标有三个:高性能、高可用,以及高可扩展。 1、高性能:性能体现了系统的并行处理能力,在有限的硬件投入下,提高性能意味着节省成本。同时,性能也反映了用户体验,响应时间分别是100毫秒和1秒,给用户的感受是完全不同的。 2、高可用:表示系统可以正常服务的时间。一个全年不停机、无故障;另一个隔三差五出线上事故、宕机,用户肯定选择前者。另外,如果系统只能做到90%可用,也会大大拖累业务。 3、高扩展:表示系统的扩展能力,流量高峰时能否在短时间内完成扩容,更平稳地承接峰值流量,比如双11活动、明星离婚等热点事件。 这3个目标是需要通盘考虑的,因为它们互相关联、甚至也会相互影响。 比如说:考虑系统的扩展能力,你会将服务设计成无状态的,这种集群设计保证了高扩展性,其实也间接提升了系统的性能和可用性。 再比如说:为了保证可用性,通常会对服务接口进行超时设置,以防大量线程阻塞在慢请求上造成系统雪崩,那超时时间设置成多少合理呢?一般,我们会参考依赖服务的性能表现进行设置。 2.2 微观目标 再从微观角度来看,高性能、高可用和高扩展又有哪些具体的指标来衡量?为什么会选择这些指标呢? 2.2.1 性能指标 通过性能指标可以度量目前存在的性能问题,同时作为性能优化的评估依据。一般来说,会采用一段时间内的接口响应时间作为指标。 1、平均响应时间:最常用,但是缺陷很明显,对于慢请求不敏感。比如1万次请求,其中9900次是1ms,100次是100ms,则平均响应时间为1.99ms,虽然平均耗时仅增加了0.99ms,但是1%请求的响应时间已经增加了100倍。 2、TP90、TP99等分位值:将响应时间按照从小到大排序,TP90表示排在第90分位的响应时间, 分位值越大,对慢请求越敏感。 3、吞吐量:和响应时间呈反比,比如响应时间是1ms,则吞吐量为每秒1000次。 通常,设定性能目标时会兼顾吞吐量和响应时间,比如这样表述:在每秒1万次请求下,AVG控制在50ms以下,TP99控制在100ms以下。对于高并发系统,AVG和TP分位值必须同时要考虑。 另外,从用户体验角度来看,200毫秒被认为是第一个分界点,用户感觉不到延迟,1秒是第二个分界点,用户能感受到延迟,但是可以接受。 因此,对于一个健康的高并发系统,TP99应该控制在200毫秒以内,TP999或者TP9999应该控制在1秒以内。 2.2.2 可用性指标 高可用性是指系统具有较高的无故障运行能力,可用性 = 正常运行时间 / 系统总运行时间,一般使用几个9来描述系统的可用性。 对于高并发系统来说,最基本的要求是:保证3个9或者4个9。原因很简单,如果你只能做到2个9,意味着有1%的故障时间,像一些大公司每年动辄千亿以上的GMV或者收入,1%就是10亿级别的业务影响。 2.2.3 可扩展性指标 面对突发流量,不可能临时改造架构,最快的方式就是增加机器来线性提高系统的处理能力。 对于业务集群或者基础组件来说,扩展性 = 性能提升比例 / 机器增加比例,理想的扩展能力是:资源增加几倍,性能提升几倍。通常来说,扩展能力要维持在70%以上。 但是从高并发系统的整体架构角度来看,扩展的目标不仅仅是把服务设计成无状态就行了,因为当流量增加10倍,业务服务可以快速扩容10倍,但是数据库可能就成为了新的瓶颈。 像MySQL这种有状态的存储服务通常是扩展的技术难点,如果架构上没提前做好规划(垂直和水平拆分),就会涉及到大量数据的迁移。 因此,高扩展性需要考虑:服务集群、数据库、缓存和消息队列等中间件、负载均衡、带宽、依赖的第三方等,当并发达到某一个量级后,上述每个因素都可能成为扩展的瓶颈点。 03 高并发的实践方案有哪些? 了解了高并发设计的3大目标后,再系统性总结下高并发的设计方案,会从以下两部分展开:先总结下通用的设计方法,然后再围绕高性能、高可用、高扩展分别给出具体的实践方案。 3.1 通用的设计方法 通用的设计方法主要是从「纵向」和「横向」两个维度出发,俗称高并发处理的两板斧:纵向扩展和横向扩展。 3.1.1 纵向扩展(scale-up) 它的目标是提升单机的处理能力,方案又包括: 1、提升单机的硬件性能:通过增加内存、 CPU核数、存储容量、或者将磁盘 升级成SSD 等堆硬 件 的 方 式 来 提升 。 2、提升单机的软件性能:使用缓存减少IO次数,使用并发或者异步的方式增加吞吐量。 3.1.2 横向扩展(scale-out) 因为单机性能总会存在极限,所以最终还需要引入横向扩展,通过集群部署以进一步提高并发处理能力,又包括以下2个方向: 1、做好分层架构:这是横向扩展的前提,因为高并发系统往往业务复杂,通过分层处理可以简化复杂问题,更容易做到横向扩展。 上面这种图是互联网最常见的分层架构,当然真实的高并发系统架构会在此基础上进一步完善。比如会做动静分离并引入CDN,反向代理层可以是LVS+Nginx,Web层可以是统一的API网关,业务服务层可进一步按垂直业务做微服务化,存储层可以是各种异构数据库。 2、各层进行水平扩展:无状态水平扩容,有状态做分片路由。业务集群通常能设计成无状态的,而数据库和缓存往往是有状态的,因此需要设计分区键做好存储分片,当然也可以通过主从同步、读写分离的方案提升读性能。 3.2 具体的实践方案 下面再结合我的个人经验,针对高性能、高可用、高扩展3个方面,总结下可落地的实践方案。 3.2.1 高性能的实践方案 1、集群部署,通过负载均衡减轻单机压力。 2、多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。 3、分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。 4、考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。 5、异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。 6、限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。 7、对流量进行 削峰填谷 ,通过 MQ承接流量。 8、并发处理,通过多线程将串行逻辑并行化。 9、预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。 10、 缓存预热 ,通过异步 任务 提前 预热数据到本地缓存或者分布式缓存中。 11、减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。 12、减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。 13、程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。 14、各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。 15、JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。 16、锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。 上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。 3.2.2 高可用的实践方案 1、对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。 2、非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。 3、接口层面的超时设置、重试策略和幂等设计。 4、降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。 5、限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。 6、MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。 7、灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。 8、监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。 9、灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。 高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。 3.2.3 高扩展的实践方案 1、合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。 2、存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。 3、业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5 )。 写在最后 高并发确实是一个复杂且系统性的问题,如果业务场景不同,高并发的落地方案也会存在差异,但是总体的设计思路和可借鉴的方案基本类似。 高并发设计同样要秉承架构设计的3个原则: 简单、合适和演进 。” 过早的优化是万恶之源 “,不能脱离业务的实际情况,更不要过度设计,合适的方案就是最完美的。 希望上面这些总结能让你对于高并发有更全面的认识。即使你没有高并发的项目经验,如果你能参考上面的内容回答得很体系,我相信也一定能给面试官眼前一亮的感觉。 最后,推荐一本我个人的高质量原创电子书《漫谈线上问题排查》,同样是大厂面试必考题,这本 PDF 凝聚了多篇我的知乎精华文章,兼顾理论+实战,一共 77 页,3 W 字。 我想这应该是 全网第一份有关线上问题排查最系统的资料 ,最起码能在面试复习时助你一臂之力! 戳这里,直接获取这本电子书: 没想到能成为第 1 高赞,追更一些新内容(记录于2021-04-07) 有读者反馈:我上面的回答非常系统地梳理了「高并发的理论知识和实战经验」,但是有点偏「大而全」的答案。面试的时候,仍然把握不好: 在短短几十分钟内,应该按照什么思路去组织内容,最终让面试官眼前一亮? 下面我分 3 种情况,分别说下自己的意见,供大家参考。 情况1:应届生仅仅学过 JUC 和 GC,一点宏观概念也没有,怎么回答比较好? 说实话,大部分公司在面试应届生时,其实很少问高并发,至少我经历过的大小公司都这样。 为什么不问?因为高并发是一个很复杂的系统性问题,很注重业务场景以及实战经验。而应届生最缺的就是项目经验,所以很难区分出大家的水平。 但如果真问到了高并发,而自己仅学过 JUC,懂点 GC,该如何应对呢?给几点建议: 1、概念性的东西要掌握扎实,不少同学容易把「并发、并行、高并发」这几个概念弄混淆,认为高并发就是「高+并发」,建议大家多查查资料,了解清楚高并发一些常识性的知识。 2、打动面试官的一定是你的强项:高并发问题通常涉及到全链路,你能从架构层面去谈,也能程序语言的并发特性上去谈,还能从网络、操作系统层面去谈,角度非常多。 你完全可以引导面试官到你擅长的方向上:比如 JUC、高效算法或者数据结构、数据库索引、IO复用或者零拷贝等等,这些往往是应届生的强项。 3、相反,如果你花很多时间去大谈架构设计、中间件使用这些未实践过的技巧上,反而容易给自己挖坑。 情况2:平时做的项目就几十 QPS,该如何应对呢? 很多同学应该属于这种情况,做过不少项目,唯独缺少高并发的实战经验。我建议从下面几个方向去做针对性准备: 1、高并发有关的理论知识需要先有系统性的了解,然后再回到自己熟悉的业务上去思考:如果并发再提高 10 倍或者 100 倍,哪些环节会遇到问题,然后你会怎么去应对?有实际场景,并且你又能给出方案选型的合理依据,效果远比空谈理论要好很多。 2、对于项目中常用的各种中间件,比如缓存、消息队列、ElasticSearch 等,其实都是非常典型的高并发系统,你可以对某个中间件进行深入研究,然后面试时主动引导面试官到这种基础组件上,谈谈你的认识。 3、如果你对自己的高并发设计能力很有信心,也可以让面试官给你拟定一个业务场景,然后给出你的设计方案。 总之,为了弥补实战经验上的短板,理论知识一定扎实,不能浮于表面,生搬硬套,不然很容易留下负面印象。 情况3:很多高并发技巧都有遇到或者实操过,怎么才能形成比较系统的回答? 这种情况属于有高并发经验,但是平时思考偏少,又没意识去做系统性整理,面试时很容易发挥失常。 建议可以参考我的回答:分别从高性能、高可用、高扩展这 3 个大方向做一次盘点,先找出技术亮点。 每个亮点,一定要扎进细节里去做整理和深度思考,弄清楚 Why 和 How:为什么要用这个技巧?它是怎么发挥作用的?会引发哪些关联问题?最终取得了什么效果?此外,业务背景以及指标性的数据都要做到心中有数。 找到亮点后,可以尝试找一条主脉络:比如按时间顺序、或者系统演进的顺序进行串联,这样比较符合高并发设计的基本原则,会让你的方案更接地气。 要是觉得不错,那就帮我 @Lowry 点个赞,关注一下我呗,硬核码字不易。

赞同 4711收录于 · 程序员的基础课

并发类问题是面试官最喜欢问的题目,实际上这玩意远没你想的那么神秘,一旦把这个问题的底层逻辑搞懂了,我保证你能秒杀任何类似问题。

以前一直觉得并发类问题是区分高手程序员的标志,因为普通程序员有一个很大的问题,大多呆在小公司,做些erp,管理后台,或者用户量不过10万的app项目,他们根本没有经历过高并发项目,所以不懂高并发下写代码要注意什么,到底会遇到有什么问题?

举个例子,他们最常背的题目是,商城怎么减库存?结果面试的时候,面试官换了个题目,怎么扣积分?如果你不懂底层逻辑,马上傻眼了,你想着扣积分就直接扣呗,有啥好说的?

所以这里真正的问题是,为什么商城里面扣库存是一个经典题目,面试官到底想考察什么?

只有知道了这个,才能知道一个系统里面有哪些地方的逻辑需要做类似扣库存的逻辑处理。

为了把上面的各种疑问说清楚,我决定先从最简单的逻辑开始说起。

例如,有个变量值 10,这个数字可以是商品库存,可以是某个用户积分,可以是某个订单的状态。现在多线程同时修改这个值,为什么多线程同时?因为可能2个人同时购买,一个人同时增加/减积分, 某个订单同时收到2个回调状态。

如果上述这些逻辑,你没有特殊处理,会遇到什么问题?例如第一个线程判断10,扣减1,剩余9,第2个线程因为同一时间点进入,判断数量也是10,扣减1,剩余9。导致最终剩余库存9,但是实际卖了2个。后面的积分,订单状态本质都是同理,比方说积分处理,一个加积分的动作,一个扣积分,假设2个动作同时发生,很可能你后面一个值把另外一个数值给覆盖了,导致明明用户扣了积分,结果数据库的值是加积分后的值。例如订单状态是待支付,用户付钱后马上取消订单,可能出现支付宝的回调跟取消的逻辑同时发生,导致用户付钱了,但是订单显示取消。

那怎么解决?

你马上想到排队的思想,按照顺序,一个个来不就好了,对计算机底层来说,想让多个线程自己排队,没那么简单,因为人的大脑,天然知道排队是什么,计算机解决这种问题,底层策略是加锁。

什么叫做加锁,其实就是找个公共地方打标记,其他人看到标记就知道前面已经有人抢先了,例如上厕所,你进去后把门关上,就是给其他人打一个标记,告诉别人,这里有人了。

计算机的锁有很多实现,互斥锁,自旋锁,读写锁,轻量级锁,重量级锁,还有不同的层级应用层,操作系统层,语言层都各有实现。具体实现不在本文讨论范围,你只要知道他们的基本思想就是在内存打标记。

有了锁之后,就能解决上面的问题了,例如在准备扣库存之前调用lock函数,拿到锁之后开始真正扣库存,接着处理后续逻辑,处理完成后,释放锁。

这种方式有个缺点,就是性能相对比较差,因为底层是在排队啊,未拿到的锁的要么直接结束,要么等,相当于同一时间只有一个人能买这个东西。所以如果大量的人都在抢一个东西,就会导致后面的人响应过长。

有没有更好的办法?有的,比方说初始值10,修改的时候判断如果为10,那就修改,否则就说明已经有别人抢先了,那就不改了,这是乐观思想,默认认为没有并发,只在最后改数据的时候判断一次。我们一般把这种逻辑叫做乐观锁。

显然乐观锁的效率要高一些,特别是商城扣库存场景,只要库存>0即可,假设商品库存有100,那么前100次请求都是直接成功的,这里扣件库存的条件为库存>0即可。所以一般商城系统,用乐观锁就比较合适。

以上我说的在单个进程内,多线程修改某个变量的场景,这个问题的本质多个线程看到了同一个变量。再说多进程的场景,什么场景下多进程?类似php等语言在处理外部请求的时候,默认每个请求一个进程,此时多个进程之间内存是隔离的,因此就需要一个公共的地方来保存数据,这就轮到数据库出场了。

其实跟前面本质没有区别,无非就是公共变量保存的地方不一样而已,这里保存在数据库里面,基本思想是一样的,都是锁。数据库扣减库存的逻辑如下:

select num from goods where id=1
结果等于10
扣掉库存1
update goods set num=9 where id=1

显然这个代码存在的问题跟之前的是一样的,2个进程可能同时执行成功,2个请求结束后,库存为9。因此也需要排队/锁的机制来处理,mysql作为知名工具,当然提供了锁的机制,看这个语句。

select num from goods where id=2 for update

这个sql执行的时候会给id等于2的记录加锁,此时如果另外一个进程里面在执行同样的逻辑的时候,他的sql会阻塞住,卡住,一直等到第一个人事务结束后,锁释放了,才会得到执行机会。这就解决了并发读写的问题,这就是mysql的悲观锁。

基于前面说的乐观锁的机制,mysql的乐观锁也很简单,更改库存的时候where条件增加一个条件num等于10或者>0即可。一般来说,小型系统在做库存处理的时候,用这个mysql的乐观锁是最简单方便的。

这里用mysql来实现锁的需求,其实在并发大的系统,并不是很合适,因为mysql作为关键系统,一般支撑并发也不大,前文说,锁的本质是找个公共地方保存数据,显然redis可以干这个事。

redis作为一个缓存系统,为什么可以用来加锁了?因为redis有个命令叫做setnx,意思就是set not exist,意思就是不存在的时候才设置值。如果单凭这个命令照样面临并发问题,因为可能2个请求同时进到redis,不过redis是单线程的,意思就是多个请求是排队处理的,当第一个请求setnx发出之后,哪怕第2个请求相差0.0001秒,也是会返回false,这就是setnx能用来实现锁的原因。

这个redis锁,有一些注意的点,比方说你的锁被其他人释放了,锁时间到期,但是业务没有执行完,具体可搜索相关资料,这里不多解释。

以上侧重于解释在同一时间修改一个共享资源导致的问题,对于90%的公司,理解了这个足够搞定有关锁并发的面试题了。

下面再说真正的高并发,例如十万级并发,如果看懂了之前说的, 就明白,无论你多高的并发,无论多么大得量,对于开发人员来说,真正值得注意问题就是我之前说得问题,核心就是资源修改得加锁。

有关加锁也有个难点,加锁范围,粒度,针对代码块加锁,还是资源加锁,总不能针对所有的修改都加锁吧,这个问题后续我会在写个文章来说。

除了,加锁,还有一个跟并发有关的就是缓存,如果某个热点商品被大流量访问,就必须加缓存,关于缓存的问题,面试也很喜欢考,例如缓存策略,缓存穿透,雪崩,缓存一致性,过期策略等,多级缓存等。这里不做多解释。

剩下的问题,就剩运维的问题了,傻大黑粗的加服务器。根据请求量,每个请求需要多少cpu,内存,来计算需要得资源,配置服务器就够了。

在说点运维层的事情,一个典型的大流量系统架构设计,一般是分3层架构。

第一层:负载均衡层,在这一层负责业务模块划分,协议区分,静态/动态资源划分(如图片文件API 请求)。

第二层:网关层,负责身份认证、访问控制、限流、监控、日志记录等安全和管理功能。

第三层:后端业务服务器,执行具体的业务逻辑,处理数据库查询、缓存操作等任务。

首先这里的负载均衡层算难点,因为要能单机支撑10万级的并发链接能力,而且要实现高可用,一旦这个挂了,所有的业务都停了。到了这种级别就不是一个nginx能搞定的事情了,得上专业的负载均衡,有硬件实现的f5,或者软件lvs。不过有更简单的方案,目前主流的云厂商都提供了负载均衡服务,在管理后台配置好策略,访问你们公司域名的时候,流量根据策略,分发到后续服务器,如果阿里云的负载均衡都挂了,我相信你这个锅能甩的毫无压力。

网关层,这一层通常要求能跟配置中心交互,要能动态感知到服务对应的服务器集群,同时需要有负载均衡能力,能根据策略,转发导后端具体的业务服务器。nginx无法满足跟配置中心交互能力,常见的网关工具有,k8s下的ingress、traefik 或者传统的Kong、Apache APISIX、Spring Cloud Gateway等。

在这样的架构下,流量被均分到业务服务器,业务服务器的性能好说,在微服务场景下,大都是无状态的,可以动态调整,随时增加服务,真正的性能卡点是数据库,线上的数据是不能随意增加的,数据库的主从同步维护成本很高。

这里假设10万个并发的场景,有的访问订单,有的访问商品,有的访问用户信息,后端搭配微服务集群,订单库,商品库,用户库,各自分开,同时每个库各自在做了读写分离,这里假设后端4个库,每个库1主2从,那么整体来说就是12个库分摊10万的并发,理论来说,问题不大,每个库一个高配服务器抗住没压力。

那假设10万个请求,都请求用户库怎么办?例如每个人都来查询各自用户信息。例如10万请求都在访问一个商品怎么办?这种情况,就该缓存出场了,如果你的接口有高并发查询需求那必须得上缓存的,单台redis能抗10万的并发。这种大流量的key要注意缓存不能过期,你需要提前监控缓存key的过期时间,否则几万的并发一旦进入了数据库,你的整个服务就会挂掉。

那假设10万请求都是修改数据库的值怎么办?如果只是短时间的流量,那就上队列,为啥队列比更改数据库快,因为队列是顺序的加一条日志而已,所以并发可以做的很大。数据进了队列之后,后续慢慢倒腾数据处理。

最后我们以秒杀来系统讲解,假设1台苹果 10万个人抢,我们预估10万个人会在同一秒来抢,这里就姑且认为是10万并发吧。

因为预估比较长,还得1-2千字,看到这的先点个关注吧,明天再写。

赞同 385

老人凌晨下地干活被气枪击中身亡 644 万 热

博主曝懂车帝隐瞒丰田车测试问题 607 万 热

长实否认李嘉诚逝世 588 万 新

王星被胁迫借下的网贷该不该还 570 万 热

女子高铁上和男友开免提打1小时电话 456 万 热

教师被迫耻辱合影教体局局长已调离 336 万 热

宜宾一摊主被顾客差评后跳河轻生 330 万 热

二本女生待业一年坚持找双休 324 万 热

1.9元汽水涉事医务人员已报警 322 万 热