面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

原文链接:https://mp.weixin.qq.com/s/VBOTIr1GhKdv712POBNImA

此文都是干货,有点长,建议先收藏。

消息队列,即常说的 MQ 是经常用到的一个东西,本文并不是要个告诉你如何使用消息中间件,而是站更高的一个层次,思考当我们使用任何消息队列解决业务问题时,都需要面对的一些通用的问题,这些问题理解透彻了,MQ 才能被你用的出神入化。

目录

  • 1、消息队列使用场景
    • 1.1、场景 1:异步处理
    • 1.2、场景 2:应用解耦
    • 1.3、场景 3:流量削锋
    • 1.4、场景 4:日志处理
    • 1.5、场景 5:分布式事务
    • 1.6、场景 6:消息通讯
  • 2、事务消息如何实现?
    • 2.1、电商中有这样的一个场景
    • 2.2、方式一:业务事务中投递消息
    • 2.3、方式二:先业务事务、后投递消息
    • 2.4、方式三:通过事务消息记录实现
    • 2.5、方式四:2 阶段投递消息
    • 2.6、方式五:2 阶段投递消息优化
    • 2.7、投递消息选择哪种方式呢?
  • 3、消息消费的 2 种方式:pull 方式、push 方式,如何选择?
    • 3.1、消息消费通常有 2 种方式
    • 3.2、push 方式
    • 3.3、pull 方式
    • 3.4、pull 方式和 push 方式选择建议
  • 4、如何确保消息消息至少被成功消费一次?
    • 4.1、消息消费的过程
    • 4.2、消费失败出现死循环
    • 4.3、采用衰减式消费+人工干预解决消息消费失败的问题
    • 4.4、消息消费需确保幂等性
  • 5、如何确保消息消费的幂等性?
    • 5.1、什么是幂等性?
    • 5.2、幂等性设计
    • 5.3、方式 1(普通方式)
    • 5.4、方式 2(jvm 加锁方式)
    • 5.5、方式 3(悲观锁方式)
    • 5.6、方式 4(乐观锁方式)
    • 5.7、方式 4(唯一约束方式)
    • 5.8、幂等性总结
  • 6、顺序消息如何实现?
  • 7、消息中间件视频教程
  • 8、往期资源需要请自取

1、消息队列使用场景

异步处理,应用解耦,流量削锋、日志处理、分布式事务、消息通讯六个场景。

1.1、场景 1:异步处理

场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式

(1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

(2)并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

假设三个业务节点每个使用 50 毫秒钟,不考虑网络等其他开销,则串行方式的时间是 150 毫秒,并行的时间可能是 100 毫秒。

因为 CPU 在单位时间内处理的请求数是一定的,假设 CPU1 秒内吞吐量是 100 次。则串行方式 1 秒内 CPU 可处理的请求量是 7 次(1000/150)。并行方式处理的请求量是 10 次(1000/100)

小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?

引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下:

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是 50 毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是 50 毫秒。因此架构改变后,系统的吞吐量提高到每秒 20 QPS。比串行提高了 3 倍,比并行提高了两倍

1.2、场景 2:应用解耦

场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

传统模式的缺点:

  • 假如库存系统无法访问,则订单减库存将失败,从而导致订单失败
  • 订单系统与库存系统耦合

如何解决以上问题呢?引入应用消息队列后的方案,如下图:

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

  • 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功
  • 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作
  • 假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦

1.3、场景 3:流量削锋

流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛

应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。

  • 可以控制活动的人数
  • 可以缓解短时间内高流量压垮应用

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

  • 用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面
  • 秒杀业务根据消息队列中的请求信息,再做后续处理

1.4、场景 4:日志处理

日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。架构简化如下

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

  • 日志采集客户端,负责日志数据采集,定时写入 Kafka 队列
  • Kafka 消息队列,负责日志数据的接收,存储和转发
  • 日志处理应用:订阅并消费 kafka 队列中的日志数据

以下是新浪 kafka 日志处理应用案例

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

(1)、Kafka:接收用户日志的消息队列

(2)、Logstash:做日志解析,统一成 JSON 输出给 Elasticsearch

(3)、Elasticsearch:实时日志分析服务的核心技术,一个 schemaless,实时的数据存储服务,通过 index 组织数据,兼具强大的搜索和统计功能

(4)、Kibana:基于 Elasticsearch 的数据可视化组件,超强的数据可视化能力是众多公司选择 ELK stack 的重要原因

1.5、场景 5:分布式事务

使用消息队列可以实现分布式事务中最终一致性的场景。

1.6、场景 6:消息通讯

消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等

点对点通讯:

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

客户端 A 和客户端 B 使用同一队列,进行消息通讯。

聊天室通讯:

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

客户端 A,客户端 B,客户端 N 订阅同一主题,进行消息发布和接收。实现类似聊天室效果。

以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。

2、事务消息如何实现?

如何确保本地事务执行成功的情况下,消息一定会投递成功;或者本地事务执行失败的情况下,消息取消投递,这也就是常说的事务消息。

2.1、电商中有这样的一个场景

  1. 下单成功之后送积分的操作,我们使用 mq 来实现
  2. 下单成功之后,投递一条消息到 mq,积分系统消费消息,给用户增加积分

我们主要讨论一下,下单及投递消息到 mq 的操作,如何实现?每种方式优缺点?

2.2、方式一:业务事务中投递消息

过程

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:投递消息到 mq
  • step4:提交本地事务

这种方式是将发送消息放在了事务提交之前。

可能存在的问题

  • step3 发生异常:导致 step4 失败,商品下单失败,直接影响到商品下单业务
  • step4 发生异常,其他 step 成功:商品下单失败,消息投递成功,给用户增加了积分

2.3、方式二:先业务事务、后投递消息

下面我们换种方式,我们将发送消息放到事务之后进行。

过程

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:提交本地事务
  • step4:投递消息到 mq

可能会出现的问题

step4 发生异常,其他 step 成功:导致商品下单成功,投递消息失败,用户未增加积分

上面两种是比较常见的做法,也是最容易出错的。

2.4、方式三:通过事务消息记录实现

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:本地库中插入一条需要发送消息的记录 t_msg_record
  • step3:提交本地事务
  • step5:新增一个定时器,轮询 t_msg_record,将待发送的记录投递到 mq 中

这种方式借助了数据库的事务,业务和消息记录作为了一个原子操作,业务成功之后,消息日志必定是存在的。解决了前两种方式遇到的问题。如果我们的业务系统比较单一,可以采用这种方式。

对于微服务化的情况,上面这种方式不是太好,每个服务都需要上面的操作;也不利于扩展。

2.5、方式四:2 阶段投递消息

增加一个消息服务消息库,负责消息的落库、将消息发送投递到 mq。

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:当前事务库插入一条日志:生成一个唯一的业务 id(bus_id),将 bus_id 和订单关联起来保存到当前事务所在的库中
  • step4:调用消息服务:携带 bus_id,将消息先落地入库,此时消息的状态为待发送状态,返回消息 id(msg_id)
  • step5:提交本地事务
  • step6:如果上面都成功,调用消息服务,将消息投递到 mq 中;如果上面有失败的情况,则调用消息服务取消消息的发送

能想到上面这种方式,已经算是有很大进步了,我们继续分析一下可能存在的问题:

  1. 系统中增加了一个消息服务,商品下单操作依赖于该服务,业务对该服务依赖性比较高,当消息服务不可用时,整个业务将不可用。
  2. 若 step6 失败,消息将处于待发送状态,此时业务方需要提供一个回查接口(通过 bus_id 查询),验证业务是否执行成功;消息服务需新增一个定时任务,对于状态为待发送状态的消息做补偿处理,检查一下业务是否处理成功;从而确定消息是投递还是取消发送
  3. step4 依赖于消息服务,如果消息服务性能不佳,会导致当前业务的事务提交时间延长,容易产生死锁,并导致并发性能降低。我们通常是比较忌讳在事务中做远程调用处理的,远程调用的性能和时间往往不可控,会导致当前事务变为一个大事务,从而引发其他故障。

2.6、方式五:2 阶段投递消息优化

在以上方式中,我们继续改进,进而出现了更好的一种方式:

  • step1:生成一个全局唯一业务消息 id(bus_msg_id),调用消息服务,携带 bus_msg_id,将消息先落地入库,此时消息的状态为待发送状态,返回消息 id(msg_id)
  • step2:开启本地事务
  • step3:生成购物订单
  • step4:当前事务库插入一条日志(将 step3 中的业务和 bus_msg_id 关联起来)
  • step5:提交本地事务
  • step6:分 2 种情况:如果上面都成功,调用消息服务,将消息投递到 mq 中;如果上面有失败的情况,则调用消息服务取消消息的发送

若 step6 失败,消息将处于待发送状态,此时业务方需要提供一个回查接口(通过 bus_msg_id 查询),验证业务是否执行成功;

消息服务需新增一个定时任务,对于状态为待发送状态的消息做补偿处理,检查一下业务是否处理成功;从而确定消息是投递还是取消发送。

方式五和方式四对比,比较好的一个地方:将调用消息服务,消息落地操作,放在了事务之外进行,这点小的改进其实算是一个非常好的优化,减少了本地事务的执行时间,从而可以提升并发量,阿里有个消息中间件RocketMQ就支持方式 5 这种,大家可以去用用。

面试阿里被diss不懂MQ,且看大佬如何将MQ玩的出神入化

2.7、投递消息选择哪种方式呢?

  1. 若我们的系统系统比较小比较单一简单,建议采用方式三
  2. 若我们的系统采用微服务的方式,建议使用方式五

3、消息消费的 2 种方式:pull 方式、push 方式,如何选择?

3.1、消息消费通常有 2 种方式

  1. push 方式
  2. pull 方式

3.2、push 方式

push 方式的过程

  1. mq 接收到消息
  2. mq 主动将消息推送给消费者(消费者需提供一个消费接口)

mq 属于主动方,消费者属于一种被动消费,一旦有消息到达 mq,会触发 mq 推送机制,将消息推送给消费者,不管消费者处于何种状态。

push 方式优点

  1. 消费者代码较少:对于消费者来说,只需提供一个消费接口给 mq 即可;mq 将接收到的消息,随即推送到指定的消费接口
  2. 消息实时性比较高:对于消费者来说,消息一旦到达 mq,mq 会立即推送给消费者

push 方式缺点

  1. 消费者属于被动方,消息量比较大时,对消费者性能要求比较高;若消费者机器资源有限,可能会导致压力过载,引发宕机的情况。
  2. 对消费者可用性要求比较高:当消费者不可用时,会导致很 push 失败,在 mq 方需要考虑至少推送成功一次,这块的设计下章节会做说明。

3.3、pull 方式

push 方式过程

1.消费端采用轮询的方式,从 mq 服务中拉取消息进行消费

2.消费完成通知 mq 删除已消费成功的消息

3.继续拉取消息消费

对于消费者来说,是主动方,可以采用线程池的方式,根据机器的性能来增加或缩小线程池的大小,控制拉取消息的速度,可以很好的控制自身的压力。

push 方式优点

1.消费者可以根据自己的性能主动控制消息拉取的速度,控制自己的压力,不至于把自己弄跨

2.实时性相对于 push 方式会低一些

3.消费者属于主动方,控制权更大一些

push 方式缺点

1.消费方需要实现消息拉取的代码

2.消费速度较慢时,可能导致 mq 中消息积压,消息消费延迟等

3.4、pull 方式和 push 方式选择建议

  1. 消费者性能较好,对实时性要求比较高的,可以采用 push 的方式
  2. 消费者性能有限,建议采用 pull 的方式
  3. 整体上来说,主要在于消费者的性能,机器的性能如果没有问题,push 和 pull 都是可以的
  4. 通常我们在 Spring 中使用的 RabbitMQ、RocketMQ 都是采用 pull 的方式,消费端会启动多个消费者,不断的从 MQ 中拉取消息进行消费

4、如何确保消息消息至少被成功消费一次?

4.1、消息消费的过程

  • step1、从 mq 中拉取消息
  • step2、执行本地业务
  • step3、将消息从队列中删除
  • step4、继续重复 step1

4.2、消费失败出现死循环

若 step2 执行失败,队列会被 step2 阻塞,step2 消费会产生死循环。

4.3、采用衰减式消费+人工干预解决消息消费失败的问题

当消息消费失败之后,可以将消息丢到延迟队列,比如第一次失败之后,延迟 2 秒再次重试,第二次失败了,延迟 4 秒再次重试。

第1次失败:延迟2秒再次消费

第2次失败:延迟4秒

第3次失败:延迟8秒

第4次失败:延迟16秒

.......

第n次失败:延迟2的n次方秒

n 可以设置一个阈值,比如 100 次,尝试 100 次,且都是失败的情况,此时就需要有监控系统触发报警,有人工介入解决了。

4.4、消息消费需确保幂等性

消息消费成功了,但是未将其从队列中剔除,会导致消息再次消费,此时需要通过幂等性来确保消息只被成功消费一次。

5、如何确保消息消费的幂等性?

5.1、什么是幂等性?

对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。

5.2、幂等性设计

我们以对接支付宝充值为例,来分析支付回调接口如何设计?

如果我们系统中对接过支付宝充值功能的,我们需要给支付宝提供一个回调接口,支付宝回调信息中会携带(out_trade_no【商户订单号】,trade_no【支付宝交易号】),trade_no 在支付宝中是唯一的,out_trade_no 在商户系统中是唯一的。

回调接口实现有以下实现方式。

5.3、方式 1(普通方式)

过程如下:

1.接收到支付宝支付成功请求
2.根据trade_no查询当前订单是否处理过
3.如果订单已处理直接返回,若未处理,继续向下执行
4.开启本地事务
5.本地系统给用户加钱
6.将订单状态置为成功
7.提交本地事务

这个过程存在一个问题:

对于同一笔订单,如果支付宝同时通知多次,会出现什么问题?当多次通知同时到达第 2 步时候,查询订单都是未处理的,会继续向下执行,最终本地会给用户加两次钱。

此方式适用于单机,通知按顺序执行的情况,只能用于自己写着玩玩。

5.4、方式 2(jvm 加锁方式)

方式 1 中由于并发出现了问题,此时我们使用 java 中的 Lock 加锁,来防止并发操作。

过程如下:

1.接收到支付宝支付成功请求
2.调用java中的Lock加锁
3.根据trade_no查询当前订单是否处理过
4.如果订单已处理直接返回,若未处理,继续向下执行
5.开启本地事务
6.本地系统给用户加钱
7.将订单状态置为成功
8.提交本地事务
9.释放Lock锁

这个过程存在一个问题:

Lock 只能在一个 jvm 中起效,如果多个请求都被同一套系统处理,上面这种使用 Lock 的方式是没有问题的,不过互联网系统中,多数是采用集群方式部署系统,同一套代码后面会部署多套,如果支付宝同时发来多个通知经过负载均衡转发到不同的机器,上面的锁就不起效了。此时对于多个请求相当于无锁处理了,又会出现方式 1 中的结果。此时我们需要分布式锁来做处理。

5.5、方式 3(悲观锁方式)

使用数据库中悲观锁实现。悲观锁类似于方式二中的 Lock,只不过是依靠数据库来实现的,数据中悲观锁使用 for update 来实现。

过程如下:

1.接收到支付宝支付成功请求
2.打开本地事物
3.查询订单信息并加悲观锁:select * from t_order where order_id = trade_no for update;
4.判断订单是已处理
5.如果订单已处理直接返回,若未处理,继续向下执行
6.给本地系统给用户加钱
7.将订单状态置为成功
8.提交本地事物

重点在于for update,对for update,做一下说明:

  1. 当线程 A 执行 for update,数据会对当前记录加锁,其他线程执行到此行代码的时候,会等待线程 A 释放锁之后,才可以获取锁,继续后续操作。
  2. 事物提交时,for update 获取的锁会自动释放。

方式 3 可以正常实现我们需要的效果,能保证接口的幂等性,不过存在一些缺点:

  1. 如果业务处理比较耗时,并发情况下,后面线程会长期处于等待状态,占用了很多线程,让这些线程处于无效等待状态,我们的 web 服务中的线程数量一般都是有限的,如果大量线程由于获取 for update 锁处于等待状态,不利于系统并发操作。

5.6、方式 4(乐观锁方式)

依靠数据库中的乐观锁来实现。

过程如下:

  1. 接收到支付宝支付成功请求
  2. 查询订单信息select * from t_order where order_id = trade_no;
  3. 判断订单是已处理
  4. 如果订单已处理直接返回,若未处理,继续向下执行
  5. 打开本地事物
  6. 给本地系统给用户加钱
  7. 将订单状态置为成功,注意这块是重点,伪代码:update t_order setstatus = 1 where order_id = trade_no where status = 0;
    //上面的update操作会返回影响的行数num
    if(num==1){
    //表示更新成功提交事务;}else{//表示更新失败回滚事务;}

关键代码解释:

update t_order set status = 1 where order_id = trade_no where status = 0;

这个 sql 是依靠乐观锁来实现的,status=0 作为条件去更新,类似于 java 中的 cas 操作。

执行这条 sql 的时候,如果有多个线程同时到达这条代码,数据内部会保证 update 同一条记录会排队执行,最终最有一条 update 会执行成功,此时成功的 num 为 1;其他未成功的,num 为 0,然后根据 num 是否为 1 来判断是否成功 。

5.7、方式 4(唯一约束方式)

依赖数据库中唯一约束来实现。

我们可以创建一个表:

CREATE TABLE `t_uq_dipose` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '关联对象类型',
  `ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '关联对象id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保证业务唯一性'
) ENGINE=InnoDB;

对于任何一个业务,有一个业务类型(ref_type),业务有一个全局唯一的订单号,业务来的时候,先查询 t_uq_dipose 表中是否存在相关记录,若不存在,继续放行。

过程如下:

  1. 接收到支付宝支付成功请求
  2. 查询 t_uq_dipose(条件 ref_id,ref_type),可以判断订单是否已处理select * from t_uq_dipose where ref_type = '充值订单' and ref_id = trade_no;
  3. 判断订单是已处理
  4. 如果订单已处理直接返回,若未处理,继续向下执行
  5. 打开本地事物
  6. 给本地系统给用户加钱
  7. 将订单状态置为成功
  8. 向 t_uq_dipose 插入数据,插入成功,提交本地事务,插入失败,回滚本地事务,伪代码:try{
    insert into t_uq_dipose (ref_type,ref_id) values ('充值订单',trade_no);
    提交本地事务:}catch(Exception e){
    回滚本地事务;}

关键代码解释:

对于同一个业务,ref_type 是一样的,当并发时,插入数据只会有一条成功,其他的会违反唯一约束,进入 catch 逻辑,当前事务会被回滚,最终最有一个操作会成功,从而保证了幂等性操作。

关于这种方式可以写成通用的方式,不过业务量大的情况下,t_uq_dipose 插入数据会成为系统的瓶颈,需要考虑分表操作,解决性能问题。

上面的过程中向 t_uq_dipose 插入记录,最好放在最后执行,原因:插入操作会锁表,放在最后能让锁表的时间降到最低,提升系统的并发性。

关于消息服务中,消费者如何保证消息处理的幂等性?

每条消息都有一个唯一的消息 id,类似于上面业务中的 trade_no,使用上面的方式即可实现消息消费的幂等性。

5.8、幂等性总结

  1. 实现幂等性常见的方式有:悲观锁(for update)、乐观锁、唯一约束
  2. 几种方式,按照最优排序:乐观锁 > 唯一约束 > 悲观锁

6、顺序消息如何实现?

一个队列配合一个消费者即可实现,就像火车站买票一样,只开一个窗口,然后让大家排队,即可按顺序购票,先确保顺序消费的消息被投递到同一个队列,消费端需要确保只能有一个消费者,拉取一个消费一个,消费完毕,再拉取另外一条消息。

但是,咱们的系统可能采用集群的方式部署,如果是集群的方式,代码相同,此时就相当于一个队列有多个消费者了,集群中通常有 2 种方式解决这个问题:

  • 分布式锁的方式:上锁成功的机器负责消费,分布式锁建议使用 zookeeper 或者 redis 来实现
  • 选主的方式:多个机器自己通过选主的方式,只有一台机器会成为 master,成为 master 的机器负责消费消息,选主可以通过 zookeeper 来实现
  • 5
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
后端工程师常常被前端diss的原因有以下几点: 1. 缺乏可视化效果:前端工程师负责开发用户可见的界面和交互效果,而后端工程师主要处理数据处理和业务逻辑。因为后端技术的本质复杂性,在展示上可能不如前端直观,因此很容易被人忽视或者认为是无趣的。 2. 不显眼的贡献:前端工程师的工作通常会在网页上直接展示出来,更容易让用户感受到他们的贡献。而后端工程师的工作大部分是在后台运行,对用户而言不太可见。这可能导致一些人会有对后端工程师工作价值的误解。 3. 视觉冲突:前端开发强调视觉效果和用户体验,而后端开发更注重数据处理和系统安全。这两个工作领域的关注点不同,导致了不同的技术和思路,容易引发一些技术上的冲突和非议。 4. 技术难度:后端开发需要掌握一系列的编程语言和技术栈,而且需要处理大量的数据库操作、算法设计和系统优化等等。相对而言,前端开发门槛相对较低。这可能导致一些前端工程师对后端技术的复杂性和专业性产生了一定的误解。 以上只是一些可能的原因,当然,并不是所有的前端工程师都会对后端工程师产生负面评价。无论前端还是后端,每个领域都有其独特的挑战和价值。在现代的软件开发中,前后端的紧密配合是非常重要的,只有互相理解和尊重,才能共同构建出优秀的产品。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值