RPC项目

为什么要做RPC项目

目前的应用大部分都是分布式或者微服务架构,通常各个模块之间都是通过rpc来进行调用的,所以我认为自己写一个rpc项目可以更加深入的理解rpc的原理

然后在写rpc项目的过程中,学会了对Zookeeper和netty的使用,也学会了通信协议设计、序列化算法、服务注册与发现、负载均衡策略的设计和使用。

项目有什么难点,如何解决的

  1. 关于通信协议的实现和切换模块,我希望客户端和服务端支持http,socket和netty传输协议,并且根据配置自动切换。我的解决方案是首先定义了统一的接口RpcClientRpcServer,然后实现其协议子类比如NettyRpcServerHttpRpcServer,利用Spring Boot的 @ConditionalOnProperty@ConditionalOnMissingBean 机制,支持配置项自动装配对应的协议实现
  2. 关于负载均衡模块的实现,就是服务调用时如何在多个服务节点直接选择一个合适的目标节点。我的解决方案是定义一个统一的接口LoadBalance,在此基础上扩展成随机、轮询和一致性哈希三种方法,使用SPI的方式决定具体使用哪种策略,最后把LoadBalance注入到服务发现模块中,实现调用时选择
  3. 关于服务注册和发现模块的实现,我编写了一个注解RpcService,服务启动时会遍历所有使用了这个注解的Bean,然后将服务的接口全限定名和服务提供地址注册到注册中心,然后在Zookeeper层面会创建一个临时节点,表明这个服务被创建成功。然后在客户端通过代理类发起远程调用时,如果存在多个地址,就是用负载均衡模块选择一个,最后建立连接发起远程调用
  4. 还有就是Kryo序列化与多线程环境的兼容性。因为Kryo是线程不安全的,直接复用同一个实例会导致数据错乱。后来我通过ThreadLocal为每个线程分配独立的Kryo实例,避免了线程间冲突,同时保持了性能

为什么用Netty做网络通信

因为相比于Java原生的Socket和Http更适合高性能和高并发的场景

  • Netty的IO是异步非阻塞的,支持大量并发连接,吞吐量高
  • Netty使用线程复用、事件驱动机制,资源占用低,GC压力小
  • Netty内部提供粘包半包处理器,实现rpc协议更方便

http、socket、netty三种网络通信方式的区别

  • socket是传输层的协议,是操作系统提供的网络编程接口,只提供最基础的通信方式,可以是阻塞或者非阻塞的,需要自己实现编解码器和传输的逻辑
  • http是应用层的协议,基于请求-响应的模式,默认是短连接的,并且不支持双向通信,性能比较低,但是http被很多框架支持,使用比较方便
  • netty是一个基于NIO的网络通信框架,底层用到了socket,是异步非阻塞,基于事件驱动的,性能比较高,并且其中提供了粘包半包的处理器和对自定义协议的支持,扩展性比较好

JDK、JSON、Protostuff、Hession、Kryo五种序列化方式的区别

  • JDK是Java原生的序列化方式,不需要引入三方库,但是序列化后体积比较大,性能低,反序列化速度慢
  • Json的可读性比较强,支持跨语言调用,比较适合web接口,但是它占用空间比较大,而且不支持复杂的对象图
  • Protostuff是基于Protobuf的序列化方式,支持基础的Java Bean,可以跨语言,并且序列化后体积小,性能高,但是它序列化后不可读
  • Hession是一个二进制协议,支持序列化复杂对象,也可以跨语言支持,兼容性比较好
  • Kryo适合Java内部序列化,不支持跨语言,序列化性能非常高,默认要求类有无参构造器,而且kryo是线程不安全的

讲一下自定义协议

我在rpc项目中的自定义协议头有魔数(4byte,识别协议是否合法),版本号(1byte,兼容后续可能的更新),序列化算法(1byte,具体使用的序列化协议),消息类型(1byte,有请求、响应、心跳),消息状态(1byte,成功,失败,超时),消息序列号(4byte,每个请求的唯一id,处理多路复用的时候可以用到),消息长度(4byte)

讲一下编解码器

编码部分是出站处理,把RpcMessage编码成Bytebuf对象,先把自定义协议头写到bytebuf中,再把协议体经过序列化后填充到bytebuf中

解码部分分为粘包半包解码器和具体解码两部分,关于粘包半包处理我使用的是Netty的定长解码器,在解码器中我设定了偏移量和消息体长度值,因为我的协议有16字节,后4字节代表消息体长度,所以偏移量是12字节,长度是4字节,解码器就通过长度字段,得出body的长度,然后从TCP流中拼接出这条完整消息,或者如果多条消息被粘在一起,Netty会按每条消息长度拆分出多条消息。然后再是具体解码,先校验是否符合我自定义协议的格式,再解出具体消息体

讲一下Netty心跳机制和Channel的连接复用

TCP是长连接,但不会自动告诉你对方是否断线,如果一端异常断开,另一端会长时间挂起资源,会造成一定的资源浪费,所以我使用了定期的心跳检测,确认连接是否存活。我使用了netty的IdleStateHandler来实现心跳检测,当检测到写空闲时自动发送一个心跳检测数据包

然后在rpc框架中,如果每发一次请求就新建TCP连接,就会导致大量Socket打开关闭,并且并发高时会造成端口耗尽的问题。我在项目中使用ConcurrentHashMap存放每个IP+端口号使用过的Channel,如果当前IP的端口再次发起rpc请求后,检测到之前的Channel还没关闭,就可以再次使用,避免重复连接服务端

然后是多路复用的实现,因为同一个服务可能会发出多个不同的rpc请求,我使用一个ConcurrentHashMap存放每个请求的等待结果区,key是协议头中的sequenceId,value是Netty的Promise(代表一个异步响应),当响应回来时,用sequenceId找到对应的Promise完成结果,就可以做到同一个连接处理多个不同的请求。

注册中心为什么使用Zookeeper不用Nacos

我这个RPC框架作为一个独立项目,需要自己实现服务注册/发现、监听机制、负载均衡等功能。

  • 项目对一致性的要求更高,服务注册、注销都是临时节点,Zookeeper会在连接断开时自动清理
  • Zookeeper支持监听机制,服务节点变化能即时通知客户端,便于实现本地缓存刷新,Nacos的监听更偏配置文件,服务变化通知粒度更粗一些
  • 项目使用的是Netty和SpringBoot,整体较轻量,不需要配置中心功能
  • Nacos提供了配置中心和服务注册功能,对于微服务项目来说非常适合。但我的RPC框架更偏向于底层架构搭建和分布式通信机制学习,所以我更偏向选用Zookeeper,它更容易控制细节、简洁可定制

服务如何注册、客户端如何发现、服务如何下线

服务注册的逻辑在服务提供端,主要流程是服务启动时,连接到Zookeeper,创建持久结点,表示这个服务,然后在持久结点下面创建临时子节点,表示提供这个服务的IP地址和端口号,临时节点中会存数据,是Json格式的服务对象,有服务名,ip地址,端口号和版本号。注册成功后,Zookeeper会维持与该服务实例的心跳连接

服务发现的逻辑在服务调用端,主要流程是服务启动时连接到Zookeeper,发送rpc请求时,先根据服务名查找服务。如果客户端第一次查找该服务,会从Zookeeper拉取服务列表,构建本地缓存,并设置监听器,后续若服务变化,缓存会自动更新。不是第一次的话则直接调用缓存,然后使用负载均衡算法从服务列表中选一个可用实例,返回该实例信息用于客户端建立连接并发起请求

服务下线分为两种情况,第一种情况是服务主动下线,服务关闭时使用注销方法将服务从Zookeeper中删除,然后客户端因为配置了监听器,会自动更新缓存列表。第二种情况是服务异常下线,因为Zookeeper有心跳机制,当检测不到这个服务时,会认为这个服务已经停止了,删除这个服务

介绍一下客户端调用、服务端响应的一个完整流程

客户端通过代理类发起服务调用,动态代理类会封装请求,客户端进行服务发现,然后使用负载均衡算法选择一个服务提供者发起网络通信请求,请求成功后,使用自定义协议将信息封装为消息头和消息体,并通过编码器和序列化器转成字节数组,将数据发送到目标Channel,服务端收到数据后,进行解码和反序列化,还原消息体,然后通过反射调用目标方法,获取返回结果,再封装消息,进行编码和序列化后将字节数组通过Channel发回到客户端中,然后客户端再进行解码和反序列化获得结果。

如果zk中的服务节点退出了,服务还能访问吗,怎么做

如果Zookeeper中某个服务节点退出,它注册的临时节点会自动被删除。客户端如果还用旧地址调用,就会访问失败。为避免这个问题,我在客户端实现了本地服务地址缓存和动态监听机制,保证地址列表是实时更新的。客户端在读取服务地址的时候会注册监听器,一旦某个节点下线了,Zookeeper会提交一个事件给监听器,进而更新本地缓存。但是如果是一个客户端已经请求到这个服务节点了,但是此时这个节点宕机了,那么会出现请求失败的情况,关于这种情况,我配置了失败重试策略,请求发送失败时,使用负载均衡策略切换下一个地址重试

如果一个正在被客户端请求的服务提供方准备下线,有什么方法能保证客户端这次能正常通信

如果一个服务正在被请求,此时直接下线可能会导致请求失败或连接中断。我采用的是优雅下线策略,先从注册中心摘除服务,但不会马上关闭进程,等待处理中的请求完成后再关闭服务进程,保证客户端本次调用能够完成(netty的EventLoopGroup.shutdownGracefully())

你做的rpc项目与目前主流的rpc框架对比有哪些优势或区别

我做的这个RPC框架虽然不如Dubbo、gRPC功能全面,但胜在轻量级,更适合作为学习、定制型服务通信的基础框架。同时我也实现了一些Dubbo和gRPC中的重要特性,如服务注册、序列化、连接复用、自动注入等功能。

当然与成熟框架相比,我的RPC框架还存在一些后续可以升级的点:

  • 服务治理能力(如限流、熔断、降级)
  • 跨语言支持
  • 服务可观测性(如日志链路追踪)

实现一个RPC框架最主要看重哪些点

  1. 通信协议的设计,协议需要清晰的定义请求和响应结构,序列化格式和状态码等数据
  2. 编解码和序列化,编解码和序列化的效率应该要高,然后序列化后的数据体积应该小,看具体业务判断是否需要跨语言
  3. 网络通信性能,阻塞式或者异步式,是否支持连接池、长连接、心跳机制
  4. 服务注册与发现,负载均衡,是否具备故障感知能力
  5. 容错与高可用机制,是否支持失败重试,熔断和降级策略
  6. 还有易用性和可扩展性,是否支持注解,自动装配和可插拔机制

本地方法调用和远程过程调用有什么区别

本地方法调用是程序内的方法直接调用,属于同一个进程内的方法调用,延迟很低,调用比较可靠,不需要中间件支持,但是代码的耦合度比较高

远程过程调用是调用远程服务器上提供的方法,属于不同进程或者不同机器上的方法调用,需要进行网络通信,所以有网络延迟,效率相对较低,也会有网络中断,超时等异常情况,需要中间件的支持,但是做到了服务之间的解耦合

rpc如何支持服务的熔断和降级的

熔断是如果某个服务连续调用失败,系统就会临时阻止再发起请求,防止拖垮系统,可以在rpc中引入熔断器组件Sentinel,每个服务接口调用前先经过熔断器判断是否“断路”,统计失败率、超时数等动态判断是否进入熔断状态,还可以支持闭合->半开->打开的状态转换

降级是如果调用失败次数太多,系统就自动返回默认数据或走备用逻辑,可以编写Fallback方法,方法调用失败/超时后,调用备用逻辑,返回默认值

介绍一下一致性哈希算法

如果使用简单的哈希方式,当服务节点发生变化的时候,所有请求的分发都会大范围变化,这会导致缓存失效,影响系统效率。一致性哈希算法可以做到:节点数量变化时,尽量少地影响原有请求的分配规律,就是请求一致性和低扰动性。

一致性哈希算法把哈希空间想象成一个环,所有服务实例都通过哈希函数映射到这个环上某个位置,对服务的ip地址,端口号以及服务名称进行hash后对一个比较大的数进行取模,对数据key也进行hash,然后顺时针找到某一个node,就是这个key要存储的服务器。这样如果增加或者删除一台服务器的话,就只会影响部分数据。但是这样做,当节点比较少的时候会造成数据倾斜的问题,大部分数据会集中在某一个服务上。我的项目中使用了虚拟节点的解决方式,每个真实节点会映射为160个虚拟节点,通过MD5算法再生成多个哈希值,映射到哈希环上,对于请求来说,使用服务方法名加请求参数拼接后做MD5,再映射成哈希值,定位到哈希环上离它最近的服务节点,当服务列表变更时,重新构建哈希环,保证一致性,然后这个重建是不会造成大批量请求映射出错的,比如某个请求落到虚拟节点 A,服务列表变了,A 还在,请求还是到 A,如果A不在了,请求会顺时针走一点落到B。

项目中哪些地方使用到了异步

  • Netty的核心通信是异步的,就是所有的IO操作都是异步非阻塞的
  • 发送rpc请求是异步的,可以发送消息后立刻返回,监听发送是否成功
  • 接收响应也是异步的,我在发生时生成了一个id,将future对象存到HashMap里,等收到响应后再异步完成这个future

小程序项目

你用自定义注解实现了权限控制,讲讲具体是怎么做的

在这个小程序中,会涉及到三种角色:管理员root,活动发起者admin和活动参与者visitor,不同角色访问系统中接口的权限是不同的。

在项目中我使用JWT实现用户认证,用户登录后会签发Token,并将用户信息存入上下文,作为认证依据。

授权部分,我设计了一个自定义注解@CheckPermission来标记每个接口需要的具体权限,并结合AOP实现了权限切面。切面会在方法执行前解析注解,获取当前用户权限列表,进行匹配校验。如果用户具备权限,就放行接口,否则抛出无权限异常,交由全局异常处理器处理。

用了三级缓存,讲一下细节

我的项目中使用了Caffeine、Redis和MySQL结构来优化数据访问性能,本地缓存用于存储访问最频繁的数据,活动列表和当前登录用户信息。Redis作为二级缓存,用于存储访问过的活动详情、活动列表和用户信息,最后由数据库层面,存储全部数据

整体流程是用户访问数据时,先查本地缓存,未命中则查Redis,如果Redis也未命中再查数据库,并将结果同步回前两级缓存。

关于过期策略,活动信息在Caffeine中缓存5分钟左右,在Redis中缓存30分钟左右。

本地缓存层面,我使用了Caffeine的CacheLoaderrefreshAfterWrite来预先加载数据避免本地的缓存穿透

数据库表是怎么设计的

用户表,存放用户信息和用户的角色

活动表,存放已经发布的活动,活动的相关信息和发起人

活动报名表,表示活动id和用户id之间的关系

如何部署项目,上线流程讲一下

预先在yaml文件中配置好生产环境参数,然后使用maven对项目进行打包,生成jar文件,传到服务器上,使用systemd后台运行项目,然后配置Nginx,将小程序API请求代理到SpringBoot项目端口,然后配置Nginx的ssl,让小程序支持HTTPS。

redis和mysql都部署在同一台服务器上,SpringBoot通过配置文件连接到redis和mysql

活动报名如果高并发,怎么防止名额超限或重复提交

我在每个活动创建的时候,会把名额写入redis中,使用DECR命令,用户报名前先DECR名额,如果减成功则允许报名,否则失败,因为DECR操作是Redis原子命令,天然支持并发控制,报名成功之后我使用redis创建一个活动名加上用户id的分布式锁,表示这个用户已经报名了这个活动,然后这个设置了一个过期时间,在过期时间内我会把这个报名数据写到数据库中,后续用户再重复报名时,因为redis层面的对应数据已经过期了,就会去查询数据库是否有他的信息,如果有再返回已报名的提示,用这个方式防止名额超限和重复报名

三级缓存的缓存一致性怎么保证

读请求是如果访问的是用户信息和首页活动列表,那么会先查询本地缓存,在查询redis,最后查询数据库,然后把数据库返回的数据存到redis和本地缓存中

如果访问的不是这两个,就直接查询redis,没查到再查询数据库,再写会redis。

写请求的话,我使用的是先更新数据库,在删除缓存的策略。

电商项目

说一下分布式事务

由多个服务通过网络完成一个事务叫分布式事务。

首先根据CAP原理决定我们的需求,是要实现CP、还是要实现AP。

实现CP就是要实现强一致性,可以使用 Seata 框架基于AT、TCC模式去实现。

我们项目中大部分实现的是AP,使用本地消息表加任务调度完成分布式事务最终数据一致性。

项目中哪里用到了分布式事务,如何解决的

  1. 发布商品,发布商品需要在商品服务的数据库表中记录,同时将商品信息同步到redis和ES
  • 发布商品使用本地事务向商品表中写入商品数据,同时写入一条待处理的消息到本地消息表
  • 提交本地事务之后,xxl的调度中心使用分片广播模式向执行器下发任务,开始扫描消息表,查询待处理的消息
  • 根据消息内容,通过调用Redis和ES的接口完成数据同步
  • 任务完成后删除消息表记录
  1. 用户下单,需要调用商品服务扣减库存,订单服务创建订单,用户服务添加购买记录
  • 订单服务收到下单请求后,开启本地事务,生成订单记录,然后写入本地消息表,记录要通知商品服务和用户服务的消息,提交事务后,通过消息队列发送通知消息
  • 商品服务和用户服务通过监听消息队列来处理库存的扣减和添加购买记录的逻辑
  • 如果消息队列发送失败或者没有被消费,就会使用xxl作为定时任务调度器,每隔一段时间扫描本地消息表中的异常记录进行消息补发

如何进行分布式事务的回滚

我项目中采用的是一种基于本地消息表和补偿机制的最终一致性方案。没有使用传统的强一致性分布式事务框架,通过状态标记和异步处理来实现逻辑上的回滚

在整个业务流程中,每个关键操作都配合本地消息表记录执行状态。当某个服务执行失败时,首先在服务内部有失败重试机制,尽最大努力成功,如果重试后仍失败,会将失败消息记录转移到数据库中的事务失败表,并在消息表中更新状态字段,最后使用xxl定时扫描失败记录表,执行补偿逻辑,并且对于一致性要求比较强的场景,我还使用了消息队列进行即时的通知,xxl作为兜底

比如用户下单操作中,订单服务已经创建好订单了,但是后续商品扣减库存出了问题,现在想要回滚,那么首先商品服务会发送一条失败消息到消息队列中,并且将失败消息记录到数据库中,订单服务获取到之后,会修改订单表中的状态字段为失败,通过状态标记避免后续的处理

除了本地信息表加任务调度保证分布式事务,还有什么思路去做,为什么选择这种思路

  • XA,两阶段提交,先准备资源,再统一提交或者回滚
  • TCC,把每个操作拆分成三部分,先预检查,再尝试获取资源,最后提交实际操作
  • SAGA,长事务补偿,将事务拆分为一系列子事务,每个子事务完成后立刻提交,如果后续事务失败,则按顺序调用每个子事务的补偿操作来回滚

为什么我选择任务调度这种方式,因为我认为这个项目中主要以性能和并发为优先考虑,不适合xa和tcc,并且业务是允许最终一致性的,不是强一致性的,而且消息队列和xxl的方式比较好实现

Elasticsearch是怎么使用的

1)首先创建索引(相当于mysql的表),将商品信息添加到索引库,对商品信息进行分词,存储到索引库

2)在商品服务中编写商品搜索接口,调用es的rest接口根据关键字、商品分类信息进行搜索

如何保证索引同步

我项目是使用本地任务表加xxl-job任务调度进行索引同步

1)添加或修改或删除商品的同时向任务表插入一条记录,这条记录就记录了是修改了哪个商品

2)任务调度定时扫描任务表,根据任务表的内容对商品信息进行同步,如果添加了商品将商品添加到索引库,如果修改了商品就修改索引库的商品,如果是删除了商品将商品信息从索引库删除

xxl-job的工作原理是什么

xxl由调度中心和执行器组成,在应用启动时,执行器会将自己注册到调度中心Admin,然后在Admin中配置定时任务,指定要调哪个Bean和方法,到达任务触发时间后,Admin通过HTTP请求调用对应的执行器,执行结果会通过回调上传给调度中心,如果失败还可以配置自动重试机制。并且如果有很多执行器,xxl还支持负载均衡策略和任务分片

在我的项目中,我主要使用xxl实现对本地消息表和失败记录表的定时扫描,还有对于优惠券和商品的限时抢购的时间预订功能

如何保证任务不重复执行

主要有两种方式,一个是每个任务都会生成一个唯一的Id,业务层判断该任务是否已经执行过,还有每条待处理的消息记录在消息表中含有状态字段,调度器扫描任务时,通过状态判断是否执行过,避免重复触发

未支付订单如何处理

在我的项目中使用RabbitMQ的死信队列机制处理未支付订单,用户提交订单之后,系统会向消息队列中发送一条消息,交换机配置为死信交换机,过期后,消息被路由到死信队列,然后执行取消订单,恢复库存的逻辑

如何保证RabbitMQ的消息可靠性

可靠性保证主要分为三部分,一个是生产者到消息队列的可靠性,这部分由消息确认机制保证,就是生产者发送消息后,会异步等待消息队列反馈ACK,一个是消息队列内部的可靠性,消息队列内部有持久化机制,可以将未消费消息保存到硬盘中,一个是消息队列到消费者的可靠性,这个与生产者类似,消费者接收到消息并且成功处理之后,会反馈给消息队列,然后消息队列才会把这条消息删除,如果处理失败或者一直没有处理会触发重试机制

如何避免消息重复消费

每个业务操作都有唯一ID,这个ID会随着消息一起发送到消息队列中,如果这个消息已经被消费了,就一定会在数据库中有记录,就算消费者后续反馈到消息队列的ack丢失,触发消息队列的重试机制了,消费者再次收到相同消息时也不会重复消费

为什么选择RabbitMQ

首先RabbitMQ支持可靠性传输机制,提供了持久化,ack确认机制,消息重试和死信机制,然后它提供了很多的消息路由类型(点对点路由、模糊匹配路由、广播路由),可以应对不同的业务场景

项目使用Redis缓存了哪些数据

缓存内容有商品详情、商品列表、用户信息等访问频率可能比较高的数据

使用的数据结构:

  • String:商品详情
  • Hash:用户信息、购物车数据
  • List:排行榜、热门商品

如何保证Redis缓存一致性

对于正常读写场景,我采用的是先更新数据库,再删除缓存的方式,对于高并发写场景,我采用的是先更新缓存,再异步更新数据库的方式,高并发为了减轻数据库压力、提高写入性能,直接写缓存,再通过消息队列异步写数据库,确保最终一致

优惠券秒杀部分怎么实现的

秒杀的核心目标首先是保证优惠券不能超卖,同一用户不能重复领券。首先我会把券的库存预加载进Redis中,然后把校验用户身份和扣减库存这两步写到lua脚本里原子执行,然后如果redis返回成功,就会向消息队列中发一条更新数据库的消息,把优惠券写到用户信息中,这样既能流量削峰,也能保证幂等性。

秒杀的核心瓶颈是什么

  • 高并发,瞬间就会有上万的用户访问同一个服务,可能会导致服务崩溃。可以使用令牌桶拦截多余请求,或者使用消息队列异步处理
  • 库存超卖,多个用户并发抢购,有可能库存为0时仍被扣减。可以使用Redis+Lua脚本原子操作
  • 数据库写入压力,秒杀成功时大量请求写入数据库。可以使用消息队列异步写入,或者分库分表缓解单表写入瓶颈
  • 重复下单,在redis加一个用户id锁,然后在过期之前把优惠券持有情况写到数据库中,后续分布式锁过期了也可以保证幂等性

当有人恶意使用脚本多台机器多个账号去进行抢购,达到一个十几万的并发量,如何应对做限流措施

  • 首先可以在网关层限流,对IP限流,每个IP每秒不超过5次
  • 然后可以在服务层限流,以用户id统计单位时间内的访问次数,超出阈值后进入冷却期,在一定时间内限制该用户的访问
  • 然后在核心接口的访问使用令牌桶,用户抢到令牌才能继续请求

假设当有人支付成功后立马又发起了退款,并且还没有完成分布式事务,如何处理

关于这一点,我采用了订单状态控制和幂等机制来确保流程正确性。我在设计订单支付功能时,订单表中有一个状态字段,支付成功只能从未支付转为已支付,退款只能从已支付状态进入。支付成功后立马发起退款,如果当前数据库的订单状态还没转换成未支付,说明当前的订单服务都还没执行完,就会拒绝退款,如果订单状态已经转换成已支付了,就会进入退款流程,然后把订单状态改为已退款,因为订单服务之后会走用户服务和商品服务,用户服务是一定会走的,因为用户曾经创建过这个订单,然后商品服务的话会执行恢复库存这样的操作

订单失败如何给用户进行反馈的

用户下单请求是通过接口发起的,在Controller层会捕获所有异常情况,如果业务逻辑执行失败,后端会返回统一格式的失败响应结构,前端拿到响应后通过异常响应信息提示用户。在订单表中有一个状态字段,每个订单可能的到达的状态在字段中都有记录,无论哪一步失败,都会更新订单状态字段,保障用户能获取订单失败的原因

当有大量失败订单,如果让你来设计如何去调度处理,你会怎么做

首先将先将失败订单进行归类管理,为后续调度处理提供依据,比如超时未支付,扣库存失败等情况,每一类对应着不同的处理策略

然后为每个服务维护一张失败任务表,记录异常信息与状态字段,用xxl定时扫描任务表,使用分片广播模式将任务分发到多个执行器实例,每次任务执行前先通过订单状态字段判断是否已经处理保证幂等性,高并发场景下,也可结合分布式锁,确保订单只被一个任务执行,对处理失败的任务,支持自动重试机制,配置最大重试次数,失败次数超过阈值后自动转为人工处理,处理完成后更新订单状态,防止重复处理

项目中的线程池使用什么方式创建的

在项目中我使用的是Java原生的ThreadPoolExecutor,核心线程数我选择的是CPU核心数的两倍,最大线程数我用的核心线程数的两倍,非核心线程存活时间我设置的是一分钟,任务队列我用的有界数组队列,长度2000,拒绝策略使用的CallerRunsPolicy,当前线程执行任务。

做微服务的意义是什么?耦合带来了什么问题?为什么要降低耦合?耦合高了有什么好处?

微服务的核心理念是将一个大而全的系统拆分为多个小而独立的服务,每个服务专注于一个业务功能,各服务之间通过API通信。

微服务可以降低模块之间的依赖,每个服务可以独立开发、测试、部署;然后如果某个服务出了故障,不一定影响全局,可以提高系统稳定性;多团队可并行开发,各自维护自己负责的服务;技术栈灵活,每个服务可以选用不同的技术;扩展性也好

耦合高了提高维护成本,改一个模块容易影响其他模块,容易引发连锁 Bug;扩展困难,无法对某个功能进行独立扩展,只能整体扩容,浪费资源;容错性比较差,一个模块崩了可能导致整个系统挂掉

耦合高了也有好处,单体应用性能高,逻辑直观,开发调试方便,高频调用时,方法级耦合比微服务高效