手写RPC框架
快来分享你的内容吧~
- 04-28 09:19
- 04-28 09:09
- 04-27 12:26
- 2025-08-30·后端查看全文求助一下,未初始化的List为什么值是[]而不是null,做RPC项目时遇到一个很奇怪的问题,在做注册中心本地缓存时,服务缓存private List serviceCache;没有做任何初始化,但第一次执行readCache方法时,serviceCache的值不是...leikooo:debug 一下这两个类对应的部分,看看在这里初始化了不350分享
- 2025-02-06·Java后端鱼皮的RPC框架项目 maven项目初始化的时候 这个应该选什么呀编程导航小智:在初始化鱼皮的RPC框架项目时,你可以选择使用Maven进行项目初始化。Maven是一个流行的构建工具,可以自动管理项目的依赖,简化项目的构建和部署过程。首先,确保你已经安装了Maven。然后,你可以执行以下命令来初始化一个Maven项目:mvn archetype:generate -DgroupId=com.example -DartifactId=my-rpc-framework -Darc120分享
- 2025-02-04·Java后端
myrpc 学习笔记-0010 启动机制和注解驱动(完结)
> 项目地址 欢迎访问 > > [https://gitee.com/longlong5/myrpc](https://gitee.com/longlong5/myrpc) 笔记总览 [myrpc 学习笔记-001 实现简易版 rpc](https://www.codefather.cn/post/2045008811399344129) [myrpc 学习笔记-002 配置加载](https://www.codefather.cn/post/2045339466129764353) [myrpc 学习笔记-003 Mock 服务代理](https://www.codefather.cn/post/2045684792053231618) [myrpc 学习笔记-004 序列化实现和 SPI 机制](https://www.codefather.cn/post/2046078441710710785) [myrpc 学习笔记-005 注册中心](https://www.codefather.cn/post/2046443766398697473) [myrpc 学习笔记-006 自定义协议](https://www.codefather.cn/post/2046803165977886721) [myrpc 学习笔记-007 负载均衡](https://www.codefather.cn/post/2047265158895616002) [myrpc 学习笔记-008 重试机制](https://www.codefather.cn/post/2048619664480780290) [myrpc 学习笔记-009 容错机制](https://www.codefather.cn/post/2048932474569916417) [myrpc 学习笔记-0010 启动机制和注解驱动](https://www.codefather.cn/post/2048935098383892481) # 一、Starter 是什么? **SpringBoot Starter = 自动配置包** 作用: - 让使用者**只加一个依赖、一个注解**就能启用 RPC 框架 - 自动初始化、自动注册服务、自动注入代理 - 完全融入 Spring 生命周期 **myrpc-springboot-starter** 实现了: 1. 自动初始化 RPC 2. 自动发布服务(Provider) 3. 自动注入服务(Consumer) 4. 自动启动 TCP 服务 # 二、3个注解 ## 1. `@EnableRpc` 启动开关 ```java @EnableRpc @SpringBootApplication public class ProviderApp {} ``` 作用: - 开启 MyRPC 自动配置 - 通过 `@Import` 导入 3 个启动类 - 可配置是否需要启动服务端 `needServer = false` 导入的类: ```java @Import({ RpcConsumerBootstrap.class, // 消费端注入 RpcProviderBootstrap.class, // 服务端注册 RpcInitBootstrap.class // 框架初始化 }) ``` ## 2. `@RpcService` 服务提供者(发布服务) ```java @RpcService(interfaceClass = UserService.class) public class UserServiceImpl implements UserService {} ``` 功能: - 被 Spring 识别为 RPC 服务 - 自动注册到 **LocalRegistry** - 自动发布到 **注册中心(Etcd/ZK)** 元注解: - `@Component` → 让 Spring 扫描并创建 Bean ## 3. `@RpcReference` 服务消费者(注入服务) ```java @Service public class UserController { @RpcReference private UserService userService; } ``` 功能: - 自动注入 RPC 动态代理 - 支持配置: - 服务版本 - 负载均衡 - 重试策略 - 容错策略 - mock 调用 # 三、三大启动类 ## 1. RpcInitBootstrap 框架初始化 **实现:ImportBeanDefinitionRegistrar** 执行时机:Spring 早期 功能: 1. 解析 `@EnableRpc(needServer)` 2. 执行 `RpcApplication.init()` 3. 初始化配置、注册中心 4. 判断是否启动 `VertxTcpServer` ```java VertxTcpServer server = new VertxTcpServer(); server.doStart(port); ``` ## 2. RpcProviderBootstrap 服务发布 **实现:BeanPostProcessor** 执行时机:Bean 初始化后 功能: 1. 扫描所有 Bean 2. 识别 `@RpcService` 3. 本地注册:`LocalRegistry.register()` 4. 注册中心注册:`registry.register()` 流程: ``` Bean初始化 → 检查@RpcService → 注册本地 → 注册注册中心 ``` ## 3. RpcConsumerBootstrap 服务注入 **实现:BeanPostProcessor** 执行时机:Bean 初始化后 功能: 1. 扫描所有 Bean 的字段 2. 识别 `@RpcReference` 3. 创建代理对象:`ServiceProxyFactory.getProxy()` 4. 反射注入字段 ```java Object proxy = ServiceProxyFactory.getProxy(interfaceClass); field.set(bean, proxy); ``` # 四、完整启动流程 ## 服务提供者启动流程 ``` 1. @EnableRpc ↓ 2. RpcInitBootstrap - RpcApplication.init() - 启动 VertxTcpServer ↓ 3. RpcProviderBootstrap - 扫描 @RpcService - 本地注册 - 注册中心注册 ↓ 服务发布完成 ``` ## 服务消费者启动流程 ``` 1. @EnableRpc(needServer = false) ↓ 2. RpcInitBootstrap - 不启动服务器 ↓ 3. RpcConsumerBootstrap - 扫描 @RpcReference - 创建代理对象 - 注入字段 ↓ 可以直接调用远程接口 ``` # 五、使用示例 ## 服务提供者 ```java @EnableRpc @SpringBootApplication public class ProviderApp { public static void main(String[] args) { SpringApplication.run(ProviderApp.class, args); } } @RpcService public class UserServiceImpl implements UserService { @Override public User getUser(Long id) { return new User(id, "rpc"); } } ``` ## 服务消费者 ```java @EnableRpc(needServer = false) @SpringBootApplication public class ConsumerApp {} @Service public class UserService { @RpcReference( loadBalancer = "random", retryStrategy = "fixedInterval" ) private UserService userService; } ``` # (完结) 从最开始的简易版实现,一步一步的增加新的功能,全局配置、序列化、协议设计、注册中心实现,到 SpringBoot Starter 集成,终于自己亲手从 0 到 1 搭起了一套完整可用的轻量级 RPC 框架。 有过编码报错时的困惑,有过粘包拆包、序列化兼容、注册中心心跳等问题的排查,也有过服务成功调用、启动即运行、注解一键接入时的豁然开朗。 每一个类、每一段逻辑、每一次调试,都是对 RPC 核心思想的深入理解 从底层 TCP 通信到上层注解驱动,从 Etcd 到 ZooKeeper 多注册中心扩展,从服务提供者到消费者全链路打通,整个体系已经完整、健壮、可扩展。 至此,项目圆满完结,撒花 ✨ 感谢鱼总!感谢编程导航!这个项目让我收获满满!
myrpc 学习笔记-009 容错机制
> 项目地址 欢迎访问 > > [https://gitee.com/longlong5/myrpc](https://gitee.com/longlong5/myrpc) 笔记总览 [myrpc 学习笔记-001 实现简易版 rpc](https://www.codefather.cn/post/2045008811399344129) [myrpc 学习笔记-002 配置加载](https://www.codefather.cn/post/2045339466129764353) [myrpc 学习笔记-003 Mock 服务代理](https://www.codefather.cn/post/2045684792053231618) [myrpc 学习笔记-004 序列化实现和 SPI 机制](https://www.codefather.cn/post/2046078441710710785) [myrpc 学习笔记-005 注册中心](https://www.codefather.cn/post/2046443766398697473) [myrpc 学习笔记-006 自定义协议](https://www.codefather.cn/post/2046803165977886721) [myrpc 学习笔记-007 负载均衡](https://www.codefather.cn/post/2047265158895616002) [myrpc 学习笔记-008 重试机制](https://www.codefather.cn/post/2048619664480780290) [myrpc 学习笔记-009 容错机制](https://www.codefather.cn/post/2048932474569916417) [myrpc 学习笔记-0010 启动机制和注解驱动](https://www.codefather.cn/post/2048935098383892481) 架构图v9.0.0 - 实现容错策略 - 支持故障恢复 快速失败 故障转移 静默处理 - 支持自定义容错策略  本模块是 RPC 调用的**最后一层安全保障**,当**重试机制也失败后**,由容错策略处理异常,避免整个系统雪崩或直接报错。 模块基于 **SPI + 工厂模式** 实现,支持 **快速失败、故障转移、静默处理、故障恢复** 四种策略。 # 一、模块结构 ``` fault.tolerant/ ├── TolerantStrategy.java 容错顶层接口 ├── FailFastTolerantStrategy 快速失败 ├── FailSafeTolerantStrategy 静默处理 ├── FailOverTolerantStrategy 故障转移 ├── FailBackTolerantStrategy 故障恢复(降级) ├── TolerantStrategyFactory 工厂 + SPI └── TolerantStrategyKeys 策略常量 ``` --- # 二、代码笔记 ## 1. 容错策略接口 统一规范,所有容错策略必须实现 `doTolerant`。 ```java public interface TolerantStrategy { RpcResponse doTolerant(Map<String, Object> context, Exception e); } ``` --- ## 2. 快速失败(默认)FailFast **直接抛出异常**,让调用方立即感知,是生产最常用策略。 ```java public class FailFastTolerantStrategy implements TolerantStrategy { @Override public RpcResponse doTolerant(Map<String, Object> context, Exception e) { throw new RuntimeException("服务报错", e); } } ``` ## 3. 静默处理 FailSafe **不抛异常,返回空结果**,系统继续运行。 ```java public class FailSafeTolerantStrategy implements TolerantStrategy { @Override public RpcResponse doTolerant(Map<String, Object> context, Exception e) { return new RpcResponse(); } } ``` ## 4. 故障转移 FailOver **自动切换其他可用节点**,提高可用性。 ```java public class FailOverTolerantStrategy implements TolerantStrategy { @Override public RpcResponse doTolerant(Map<String, Object> context, Exception e) { // 扩展:从服务列表获取下一个节点重试 return null; } } ``` ## 5. 故障恢复(降级)FailBack **降级调用其他服务或本地方法**。 ```java public class FailBackTolerantStrategy implements TolerantStrategy { @Override public RpcResponse doTolerant(Map<String, Object> context, Exception e) { // 扩展:调用降级服务 return null; } } ``` ## 6. 工厂 + SPI 加载 ```java public class TolerantStrategyFactory { static { SpiLoader.load(TolerantStrategy.class); } public static TolerantStrategy getTolerantStrategy(String key) { return SpiLoader.getInstance(TolerantStrategy.class, key); } } ``` ## 7. 策略常量 ```java public interface TolerantStrategyKeys { String FAIL_BACK = "failBack"; // 降级 String FAIL_FAST = "failFast"; // 快速失败 String FAIL_OVER = "failOver"; // 故障转移 String FAIL_SAFE = "failSafe"; // 静默处理 } ``` # 三、调用方式(代理层) ```java try { // 重试调用 } catch (Exception e) { // 重试失败,进入容错 TolerantStrategy strategy = TolerantStrategyFactory.getTolerantStrategy("failFast"); RpcResponse response = strategy.doTolerant(context, e); } ``` # 四、四种容错策略对比 | 策略 | 行为 | 优点 | 缺点 | 适用场景 | |------|------|------|------|----------| | **failFast 快速失败** | 立即抛异常 | 简单、快速定位问题 | 可能中断流程 | 核心服务、支付、订单 | | **failSafe 静默处理** | 吞异常,返回空 | 不影响主流程 | 问题难发现 | 日志、监控、非关键统计 | | **failOver 故障转移** | 试下一个节点 | 高可用 | 实现复杂 | 高可用服务集群 | | **failBack 故障恢复** | 降级到其他服务 | 功能不中断 | 需要准备降级逻辑 | 商品详情、首页推荐 |
myrpc 学习笔记-008 重试机制
> 项目地址 欢迎访问 > > [https://gitee.com/longlong5/myrpc](https://gitee.com/longlong5/myrpc) 笔记总览 [myrpc 学习笔记-001 实现简易版 rpc](https://www.codefather.cn/post/2045008811399344129) [myrpc 学习笔记-002 配置加载](https://www.codefather.cn/post/2045339466129764353) [myrpc 学习笔记-003 Mock 服务代理](https://www.codefather.cn/post/2045684792053231618) [myrpc 学习笔记-004 序列化实现和 SPI 机制](https://www.codefather.cn/post/2046078441710710785) [myrpc 学习笔记-005 注册中心](https://www.codefather.cn/post/2046443766398697473) [myrpc 学习笔记-006 自定义协议](https://www.codefather.cn/post/2046803165977886721) [myrpc 学习笔记-007 负载均衡](https://www.codefather.cn/post/2047265158895616002) [myrpc 学习笔记-008 重试机制](https://www.codefather.cn/post/2048619664480780290) [myrpc 学习笔记-009 容错机制](https://www.codefather.cn/post/2048932474569916417) [myrpc 学习笔记-0010 启动机制和注解驱动](https://www.codefather.cn/post/2048935098383892481) 架构图v8.0.0 - 实现重试策略 - 支持不重试 固定时间间隔 重试策略 - 支持自定义重试策略  这是 RPC 框架的**重试机制**的具体实现,负责在调用失败时自动重试,提升调用成功率,基于 **Guava Retrying** 实现,支持 **不重试 / 固定间隔重试** 两种策略,可扩展、可配置、SPI 加载。 # 一、模块结构 ``` fault.retry/ ├── RetryStrategy.java 重试策略接口 ├── NoRetryStrategy.java 不重试策略 ├── FixedIntervalRetryStrategy.java 固定时间间隔重试 ├── RetryStrategyFactory.java 重试策略工厂(SPI) └── RetryStrategyKeys.java 策略常量 ``` # 二、代码笔记 ## 1. 重试策略接口 统一规范,所有重试策略都必须实现 `doRetry` 方法。 ```java public interface RetryStrategy { RpcResponse doRetry(Callable<RpcResponse> callable) throws Exception; } ``` ## 2. 不重试策略 直接执行一次,失败立即抛出异常。 ```java public class NoRetryStrategy implements RetryStrategy { @Override public RpcResponse doRetry(Callable<RpcResponse> callable) throws Exception { return callable.call(); } } ``` ## 3. 固定间隔重试策略 基于 **Guava Retrying** 实现,功能强大。 ### 重试规则: - 出现 **Exception** 就重试 - 每次间隔 **3秒** - 最多 **3次尝试**(1次正常+2次重试) - 每次重试打印日志 ```java public class FixedIntervalRetryStrategy implements RetryStrategy { @Override public RpcResponse doRetry(Callable<RpcResponse> callable) throws Exception { Retryer<RpcResponse> retryer = RetryerBuilder.<RpcResponse>newBuilder() .retryIfExceptionOfType(Exception.class) // 异常重试 .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) // 等待3秒 .withStopStrategy(StopStrategies.stopAfterAttempt(3)) // 最多试3次 .withRetryListener(attempt -> { log.info("重试次数 {}", attempt.getAttemptNumber()); }) .build(); return retryer.call(callable); } } ``` ## 4. 重试策略工厂 + SPI 支持 SPI 动态加载,可无缝扩展更多重试策略。 ```java public class RetryStrategyFactory { static { SpiLoader.load(RetryStrategy.class); } private static final RetryStrategy DEFAULT = new NoRetryStrategy(); public static RetryStrategy getRetryStrategy(String key) { return SpiLoader.getInstance(RetryStrategy.class, key); } } ``` ## 5. 策略常量 ```java public interface RetryStrategyKeys { String NO = "no"; // 不重试 String FIXED_INTERVAL = "fixedInterval"; // 固定间隔 } ``` # 三、使用方式(代理层调用) ```java RetryStrategy retryStrategy = RetryStrategyFactory.getRetryStrategy("fixedInterval"); RpcResponse response = retryStrategy.doRetry(() -> VertxTcpClient.doRequest(rpcRequest, serviceMetaInfo) ); ``` --- # 四、重试策略对比 | 策略 | 行为 | 优点 | 缺点 | 适用场景 | |------|------|------|------|---------| | **no(不重试)** | 一次失败立即抛错 | 最快、无延迟 | 不稳定 | 实时性高、非幂等接口 | | **fixedInterval(固定间隔)** | 失败等3秒再试,最多3次 | 提高成功率、稳定 | 增加调用耗时 | 网络波动、幂等接口 |
求助一下,未初始化的List为什么值是[]而不是null,做RPC项目时遇到一个很奇怪的问题,在做注册中心本地缓存时,服务缓存private List<ServiceMetaInfo> serviceCache;没有做任何初始化,但第一次执行readCache方法时,serviceCache的值不是null,而是[],自己排查过程: 1,类没有使用@Data注解 2,成员变量私有,唯一对外提供修改的方法为writeCache 3,在方法writeCache打断点,重新启动项目,发现没有执行该方法,直至在服务发现时第一次执行readCache,进入该方法,serviceCache的值[]. 类图及执行结果:
又又又又又完结,本来是点赞系统之前就在写rpc,点赞系统出来后就搁置了,现在终于完结
最近面试遇到有面试官询问,如何考虑RPC框架的版本兼容问题?大家知道怎么回答嘛?😥
手写 RPC 框架项目教程 - 鱼友项目笔记 笔记
(负载均衡策略、重试策略、容错策略,都在服务消费端配置) 负载均衡策略:轮询:原子类下标自增取模。随机:随机下标。一致性哈希:有序Map (TreeMap、SortedMap) 构建哈希环,key为服务实例哈希值,value为服务实例,每个服务实例构建N个虚拟节点添加到哈希环上,对请求计算哈希值,取出哈希环上第一个哈希值大于等于请求哈希值的节点 (ceilingEntry、tailMap),或者第一个节点。 重试策略:重试时机 (发生指定异常),等待策略 (固定间隔、指数退避),停止策略 (尝试指定次数后、指定延迟时间后),参数为Callable (包装执行方法,结构为无参,返回一个值),响应为Callable返回值。 容错策略:快速失败:重新抛出异常。安全失败:不抛出异常,静默处理。 序列化器类型:JDK、JSON、Kryo、Hessian。 注册中心类型:Etcd、Zookeeper。 客户端请求封装 (http、tcp协议):构造请求,发送请求,接收响应。 服务器请求处理器封装 (http、tcp协议):接收请求,处理请求,返回响应。 服务代理工厂。 可配置的自定义SPI机制 (SPI配置加载工厂、SPI实例加载工厂、SPI配置文件、服务接口、各服务接口实现类):获取指定服务接口下指定key的服务接口实例: 1. SPI配置加载:配置文件名为服务接口全类名,内容每行为key=服务接口实例全类名,扫描指定路径下的所有配置文件并构建<服务接口全类名,<key,服务接口实例全类名>>映射 2. 获取指定服务接口实例:根据服务接口全类名和key找到对应的服务接口实例全类名,反射实例化并存入<服务接口实例全类名,服务接口实例对象>映射中 3. 服务工厂:每个服务接口都可以配置一个对应的加载工厂,类静态初始化时加载服务配置信息,并提供根据key加载对应服务接口实例对象的方法,使用时,读取配置key,根据key获取指定服务接口实例对象并使用 配置对象:提供各种服务实现的配置,提供默认值,项目启动时从指定配置文件中加载配置信息,使用时从配置对象中获取配置信息并从对应工厂中获取指定服务实例。 编解码 (在这里针对消息结构的转换,**在消息编解码中对消息体进行序列化和反序列化**):针对消息结构整体,包括消息头和消息体,通常是字节对字节,或字节对基本类型数据的转换。关注**数据的格式转换**,将数据从一种格式转换为另一种格式。 序列化反序列化:针对字节数组和java对象的转换,通常针对消息体数据的转换。关注**对象的状态和结构**,将对象转换为可以存储或网络传输的格式。 装饰器模式 (调用的并不是目标对象本身,而是一个装饰器对象,装饰器对象中引用了目标对象,在装饰器对象执行时可以控制目标对象的执行) 解决TCP半包粘包问题:请求处理器执行时需要指定一个Handler (消息到来时执行),这里实现两个Handler,第一个Handler用于解决半包粘包问题,在其handle方法中,通过RecordParser,第一次先读取消息头长度 (固定配置) 的字节数据,从中获取消息体长度,第二次读取消息体长度的字节数据,并拼接到消息头后面,此时才调用第二个Handler的handle方法具体去执行请求 (第一个Handler以第二个Handler作为构造参数,如此便可控制第二个Handler的实际调用),并传入完整的消息对象。 (服务提供者有个本地服务注册器,为了根据请求信息获取对应服务实例并调用执行;服务消费者有个本地服务缓存,为了避免频繁访问注册中心) 服务提供者启动和执行流程: 1. 初始化应用:读取配置信息,初始化注册中心 (启用心跳续约机制),注册JVM-ShutdownHook (关闭时调用注册中心的销毁方法,注销所有已注册的服务实例) 2. 注册服务: 1. 得到需要注册的服务列表,注册到本地服务注册器中 (存储服务名到服务实现实例的映射,这里可以通过反射实例化服务实例然后存储服务实例对象,或者存服务实例类型,然后执行时再反射调用) (注册到本地注册器上,是为了执行调用) 2. 获取注册中心实例,将服务实例注册到注册中心上 (注册到注册中心上,是为了服务发现) 3. 启动服务器:获取服务器实例,启动服务器,等待请求,请求来了,交由请求处理器执行 4. 请求处理器执行: 1. 通过装饰器模式解决TCP半包粘包问题,先拼接得到完整消息对象,再具体执行请求处理逻辑 2. 具体请求处理执行: 1. 根据协议类型进行不同处理,如tcp自定义协议:解码消息 (Buffer转为消息对象),反序列化消息体 (消息体字节数组转为请求对象) 2. 根据请求信息,获取服务名、方法信息,从本地服务注册器中获取对应服务实现实例,调用指定方法获取结果对象 3. 反序列化响应对象 (响应对象转为字节数组),构造响应消息,编码响应消息 (消息对象转为Buffer),响应给客户端 服务消费者启动和执行流程: 1. 初始化应用:加载配置信息 2. 获取服务接口代理对象: 1. 若启用mock,则返回mock代理MockProxy 2. 否则,返回服务代理ServiceProxy 3. 执行服务接口代理对象方法,会执行服务代理对象的invoke方法 1. 服务发现:根据调用的服务信息,从注册中心获取服务实例列表 (会先走本地服务缓存查询) 2. 负载均衡:根据配置的负载均衡策略,从服务实例列表中选择一个服务实例 3. 请求客户端处理: 1. 构建消息对象,将请求对象反序列化为字节数组并设置为消息体,将消息对象编码为Buffer 2. 发送请求 3. 接收响应Buffer,将Buffer解码为消息对象,将消息体反序列化为响应对象并返回 4. 重试策略:用重试策略包装请求客户端处理逻辑 5. 容错策略:捕获请求处理过程中的异常 (在重试之外),在异常处理中执行容错策略 注册中心实现: 1. 初始化:根据配置信息,与注册中心建立连接,获取客户端操作对象,启用心跳续约机制 2. 注册服务:将服务实例注册到注册中心上,key为/前缀/服务全类名/ip:port,**value为服务实例对象的JSON字符串表示**,并设置过期时间,将key添加到已注册key集合中 3. 注销服务:将服务实例从注册中心上移除,并从已注册key集合中删除该key 4. 销毁注册信息 (应用主动停机时需要执行,此时服务已不可用,可通过JVM的shuedown钩子实现):遍历已注册的key集合,将所有注册的服务实例从注册中心上移除 5. 心跳续约机制:遍历已注册的key集合,重置其过期时间 6. 服务发现 (针对服务消费者):先从本地服务缓存中读取服务实例列表。不存在则以服务全类名为前缀到注册中心查询服务实例列表 (获取其值列表并通过JSON解析为服务实例对象),并存入本地服务缓存中,本地服务缓存格式为<服务全类名,<服务实例key,服务实例对象>>,同时监听`/前缀/服务全类名`前缀节点 (懒执行机制,在远程调用时才缓存服务实例并监听节点变更) 7. 监听节点变更 (针对服务消费者):(监听的是针对服务全类名的前缀节点) 1. 当该节点下有服务实例新增时:将该服务实例加入本地服务缓存中 2. 当该节点下有服务实例删除时:将该服务实例从本地服务缓存中删除 3. 当该节点下有服务实例更新时:将旧的服务实例从本地服务缓存中删除,将新的服务实例加入本地服务缓存中 服务实例对象:服务接口全类名、ip、port。 注解驱动自动装配实现: 1. @RpcReference (为bean中标有@RpcReference注解的属性注入代理对象):通过`BeanPostProcessor#postProcessAfterInitialization`,遍历bean所有属性,若其标注有@RpcReference注解,则根据其类型从服务代理工厂中获取其代理对象实例并设置为该属性的值 2. @RpcService (将标注有@RpcService的bean作为服务注册到本地注册表和注册中心上):通过`BeanPostProcessor#postProcessAfterInitialization`,若bean标注有@RpcService注解,则根据其类型,将其注册到本地服务注册表和注册中心上 3. @EnableRpc (若项目中有配置类标注有@EnableRpc注解,则启动Rpc框架,初始化配置,再看情况初始化注册中心和启用服务器):通过`ImportBeanDefinitionRegistrar#registerBeanDefinitions`,在@Import该类的配置类实例化前,会执行该方法,在该方法中启动Rpc框架 (@EnableRpc组合@Import注解导入该类,则在项目中任意一个配置类上使用@EnableRpc时,都会导入该类并执行初始化逻辑)
鱼皮的RPC框架项目 maven项目初始化的时候 这个应该选什么呀
手写 RPC 框架 - 个人笔记+梳理+总结+扩展点实现
## 前言 跟着鱼哥又做完一个项目了。 相比传统的面向业务的项目,比如商城管理系统等。本次手写框架项目深刻学习体会了开发底层框架的思路。 并且接触了许多以前偏于写应用而未能接触到的知识点 再次做个记录总结,个别实现思路可能有些糙。 个人水平有限,多多指教! *★,°*:.☆( ̄▽ ̄)/$:*.°★* 。 1. **GitHub 代码仓库:** [https://github.com/Jools-hzx/jools-rpc](https://github.com/Jools-hzx/jools-rpc) 2. **示意图汇总:** [示意图大汇总](https://www.yuque.com/wakoo-fvkfd/pu8unv/ez7g36wbl7ut0gop) ## 阶段 00 - 导学和入门 个人笔记: [原文链接](https://www.yuque.com/g/wakoo-fvkfd/pu8unv/lp1zmnhee7sremyp/collaborator/join?token=MUmAfnKnSek8ylhC&source=doc_collaborator#%20《RPC%20框架导学和介绍》) ### 什么是 RPC? > 本身并不是一种协议,而是一种调用 > > 常用的 RPC 协议实现: > > + gRPC > + thrift > ### 核心 + 服务消费者 - 请求处理器:根据客户端的请求参数来进行不同的处理、调用不同的服务和方法 + 服务提供者 - 本地服务注册器:记录服务和对应实现类的映射。 + 序列化 / 反序列化器 + 注册中心: Etcd / Redis / ZooKeeper + 负载均衡:选取提供者 + 容错机制:调用失败 + 替他: - 服务提供者节点下线,删除失效节点 - 缓存拉取的服务信息 - 合适的网络框架,或者自定义协议头、节约传输体积 - 优化扩展性:SPI机制、配置优化 | **模块** | **描述** | | --- | --- | | **服务消费者** | **请求处理器**:根据客户端的请求参数进行处理,调用不同的服务和方法。 | | **服务提供者** | **本地服务注册器**:记录服务与对应实现类的映射关系。 | | **序列化 / 反序列化器** | 提供请求与响应对象的高效序列化和反序列化支持。 | | **注册中心** | 支持 **Etcd**、**Redis** 和 **ZooKeeper** 用于服务注册与发现。 | | **负载均衡** | 选取最佳服务提供者,实现多种负载均衡策略(如轮询、一致性哈希等)。 | | **容错机制** | 在调用失败时提供容错策略,如 FailSafe、FailFast、FailOver 等。 | | **其他功能** | - **服务提供者节点下线**:删除失效节点,保持服务列表一致性。 | | | - **缓存服务信息**:本地缓存拉取的服务信息,减少注册中心访问频率。 | | | - **优化网络传输**:通过自定义协议头减少传输体积,选择合适的网络框架。 | | | - **优化扩展性**:支持 SPI 机制扩展,结合配置文件实现灵活优化。 | ## 阶段 01 - 开发极简的 RPC 框架 [原文链接 - 开发简易的 RPC 框架](https://www.yuque.com/wakoo-fvkfd/pu8unv/oypurc1mxzhgy9gx) ### 阶段成果 1. 搭建项目和模块: 1. `exp-common`: 示例代码的公共依赖,包括接口、Model 等 2. `exp-consumer`: 示例服务消费者代码 3. `exp-provider`: 示例服务提供者代码 4. `jools-rpc-basic`: RPC框架 - 简易版 ### exp-common 模块 | **功能** | **核心组件** | | --- | --- | | 实体类 model | User 类,返回字段值 name | | 服务接口 Service | UserService | | 服务方法 | getUser() 返回 User | ### exp-provider 模块 | **功能** | **核心组件** | | --- | --- | | 服务实现类 | UserServiceImpl | | 实现服务方法 | getUser | ### exp-consumer 模块 | **功能** | **核心组件** | | --- | --- | | 请求客户端 | BasicProviderExample | | 请求发送方式 | 基于 **JDK 动态代理**,返回代理对象通过 `HTTPRequest` (Hutool 工具包) 发送请求 | | 调用服务 | UserService | | 调用服务方法 | getUser | | 消费方代理方式选型 | 静态代理 与 动态代理 (区别 + 分别实现方式 + 优缺点) | ### jools-rpc-basic 模块 | **功能** | **核心组件** | | --- | --- | | Web 服务器 | 使用 **Vert.x**,可选 **Tomcat** 或 **Netty** | | 本地服务注册器 | `LocalRegistry` <br/>基于 `ConcurrentHashMap` <br/>+ `key` 为服务名称 <br/>+ `value` 为服务实现类全类名 | | 通信请求实体类 | `RpcRequest` 和 `RpcResponse` | | `RpcRequest` 请求消息体,支持序列化 | 请求服务名 `serviceName`<br/>方法名 `methodName`<br/>方法参数类型 `paramTypes`<br/>传入实参 `params` | | `RpcResponse` 响应消息体,支持序列化 | 响应数据 `data`<br/>响应数据类型 `datatype`<br/>响应信息 `msg` <br/>异常信息 `exception` | | 序列化器 | `JdkSerializer`,基于 JDK 原生序列化方式 | | 请求处理器 | `HttpServerHandler` 借助序列化器反序列化 HTTP 请求,调用本地服务注册/序列化返回响应。 | | 动态代理处理器 | `ServiceProxyFactory`返回 `ServiceProxy`实例,实现透明调用 | ### 示意图 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/M7iZbRUf3g56KhKJ.webp" alt="image.png" width="80%" /> ## 阶段 02 - 全局配置加载 笔记:[原文链接 - 全局配置加载](https://www.yuque.com/wakoo-fvkfd/pu8unv/fferwgcfykpg2zg7) 个人博客积累:[稀土掘金 - 好用的解析配置工具类(Hutool+SnakeYAML)](https://juejin.cn/post/7432503365266554932) ### 阶段成果 1. 支持基于 `application.properties` 文件加载全局配置 2. 支持区分 `dev`, `prod` 多环境配置文件 3. `(扩展)` 工具类 SnakeYAML 支持基于 `.yml / .yaml` 格式文件加载全局配置 4. `(扩展)` 支持监听配置文件变更,借助 `Hutool.autoLoad()` 5. `(扩展)` 配置文件支持中文 ### 简示图 + 橙色部分为本阶段新增内容 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/gIWr4X90DpGmjQvx.webp" alt="" width="80%" /> ### 全局配置信息设计 [原生 API 配置 - Dubbo 官方](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/config/api/api/) > 参考 Dubbo 官方配置 `ApplicationConfig` 中至少含有: > > 1. 注册中心地址:服务提供者与消费者均需指定,用于服务注册和发现。 > 2. 服务接口:提供者指定提供的,消费者指定调用的。 > 3. 序列化方式:双方均需指定,用于网络数据传输的序列化与反序列化。 > 4. 网络通信协议:双方选择合适的,如 TCP、HTTP 等。 > 5. 超时设置:双方均需设置,用于调用服务超时处理。 > 6. 负载均衡策略:消费者指定,决定调用哪个服务提供者实例。 > 7. 服务端线程模型:提供者指定,决定处理客户端请求方式。 > 解析配置文件生成配置类实例借助官方文档: [Hutool - Props 工具](https://doc.hutool.cn/pages/Props/%E3%80%82) **设计实现 - RpcConfig** 1. name (String):服务名称,默认值 `jools-rpc` 2. version (String) : 版本, 默认值 `1.0` 3. serverHost (String): 主机名称,默认值 `localhost` 4. serverPort (String): 服务端口, 默认值 `8888` ### 开发实现:扩展 `rpc-core`模块 1. 在 `utils` 包下创建工具类 `ConfigUtils`,使用 Hutool 的 `Props` 工具加载配置。 2. 在 `constant` 包下创建接口 `RpcConstant`,用于存储常用配置常量的默认值: - 默认配置项前缀为 `DEFAULT_CONFIG_PREFIX = "rpc"`。 - 默认配置文件格式为 `PROP_CONFIG_SUFFIX = ".properties"`。 3. 方法参数支持通过 `prefix` 字段配合 `-environment` 字段加载多环境配置。 4. 使用 ⌈双检索单例模式⌋ 确保全局配置类的唯一性。 5. 支持用户自定义 `application.properties` 文件,若未提供则使用默认配置。默认配置的值由 `RpcConfig` 中各字段的默认值决定。 ### (扩展) - 支持不同格式 `.yml / .yaml` 参考文献: [SnakeYaml 工具快速入门](https://www.baeldung.com/java-snake-yaml) **导入依赖配置** ```xml <!-- Yaml配置类解析--> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>2.2</version> </dependency> ``` SnakeYaml 工具支持: 1. 直接读取 `.yml / .yaml`配置转换为 `Map` 2. 直接读取 `.yml / .yaml` 配置并封装成指定类型 `RpcConfig` 3. 支持基于前缀 `key`区分配置组,`RpcConfig`分配前缀 `rpc` **扩展 **`**ConfigUtils**`**:** + 接口常量支持添加 `YAML_CONFIG_SUFFIX` 用于辨识 `.yaml/.yml` 配置后缀 + 支持基于 `.yaml / .yml` 格式和不同 `enivronment` 加载不同环境配置 **加载配置规则:** 1. 若用户未添加配置文件,项目内不存在 `.properties` 配置文件,加载默认值 2. 若项目内存在 `.properties` 配置文件,加载 3. 若用户已配置但是配置了多个,优先加载 `.properties` 4. 若用户无 `.properties`但是存在 `.yml`; 优先加载 `.yml` 5. 若用户未配置 `.properties`但是配置了 `.yaml`, 加载 `.yaml` ### (扩展) - 监听配置文件,支持自动更新 参考文档 + [Hutool 中操作和监听文件](https://blog.csdn.net/ZGL_cyy/article/details/118575786) + [Hutool 工具类中 - 监听文件工具类](https://cloud.tencent.com/developer/article/2133023) + [Hutool - Props - JavaDoc](https://apidoc.gitee.com/loolly/hutool/cn/hutool/setting/dialect/Props.html) 官方说明 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/i5ZWDqxQtkB0pamo.webp" alt="" width="80%" /> **引用** 应用Hutools 工具类中 `loadConfig`的同时完成监听 1. 修改 `ConfigUtils` 工具类 2. autoLoad() 方法源码简单分析 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/qvmsvVxxxBOuaMXk.webp" alt="" width="80%" /> **简介:** > Hutool 的 WatchMonitor 封装了 JDK 7 的 WatchService,用于监听文件和目录的变动(如创建、更新、删除)。你可以在 Watcher 中定义处理这些变动的逻辑。例如,你可以用 WatchMonitor 来监测配置文件的变化,并自动将其加载到内存中。 > > 支持的监听机制: > > + ENTRY_MODIFY (文件修改的事件) > + ENTRY_CREATE ()文件或目录创建的事件) > + ENTRY_DELETE (文件或目录删除的事件) > + OVERFLOW (丢失的事件) > 3. 添加测试方法,修改配置文件后再读取,查看是否修改成功 ### (扩展) 配置支持中文 默认 Hutool - Props 类支持的编码为 `ISO-8859-1` [官方关于编码的 - issue](https://gitee.com/dromara/hutool/issues/I72IP0?skip_mobile=true) 修改 `loadConfig` 方法 + 指定编码类型为 `StandardCharsets.UTF_8` ## 阶段 03 - 接口 Mock 笔记原文:[原文链接 - 接口 Mock](https://www.yuque.com/wakoo-fvkfd/pu8unv/cq3874qqg3ddegqn) 个人博客积累:[哪些工具可以实现测试 Mock](https://juejin.cn/post/7437464363034656806) ### 示意图 + 橙色为本阶段的扩展内容 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/Db8Q3ByuckOcoFuT.webp" alt="" width="80%" /> ### 阶段成果 1. 接口 Mock 的需求分析和设计 2. 支持基于配置开启接口 Mock 3. 基于 JDK + JavaFaker 支持多种数据类型返回默认值 4. `扩展` - 完善 Mock 机制,基于 JavaFaker 库;官方文档:[GitHub - JavaFaker](https://github.com/DiUS/java-faker) ### 简介 1. Mock 机制简介 > 指模拟对象,通常用于测试代码中,特别是在单元测试中,便于泡桐业务流程 > 2. 为什么要支持 `Mock` > 开发者能轻松调用服务接口、跑通业务流程,无需依赖真实远程服务,提升使用体验。 > ### 开发实现 1. `RpcConfig` 配置项新增 `mock (boolean 类型)`, 方便开发者快速开启 ```java public class RpcConfig { .... /** * 开启接口 mock, true 表示开启; false 表示关闭 */ private boolean mock = false; } ``` 2. 借助动态代理,返回 `mock` 代理服务 `MockServiceProxy`,针对指定返回类型返回模拟数据 3. 服务代理工厂 `ServiceProxyFactory`支持返回 mock 代理服务实体 ```java @SuppressWarnings("all") public static <T> T getMockProxy(Class<T> mockClass) { return (T) Proxy.newProxyInstance( mockClass.getClassLoader(), new Class[]{mockClass}, new MockServiceProxy() ); } ``` ### (扩展) - 完善 Mock 机制 基于 JavaFaker 支持 `mock` 更多种数据类型。 **参考文档** 1. [Java-Faker-Github仓库](https://github.com/DiUS/java-faker) 2. [中文参考-掘金博客](https://juejin.cn/post/7086763649410269214) 3. [英文参考-快速入门案例](https://www.baeldung.com/java-faker) 4. 本地化可配置参数: [参考 - Supported Locales](https://github.com/DiUS/java-faker) ```xml <dependency> <groupId>com.github.javafaker</groupId> <artifactId>javafaker</artifactId> <version>1.0.2</version> </dependency> ``` **快速入门 - 测试方法** 1. 测试 `internet()` 相关 API, 模拟域名 + IP 2. 测试 `bothify()`方法,`?` 支持替换为随机字母;`#`支持替换为随机数字 3. 测试 `address()`方法,返回模拟地址数据 新增支持 Mock 的数据类型 + 增加 RpcRequest / RpcResponse + 增加 RpcConfig + 增加 HttpServer ## 阶段 04 - 序列化器与 SPI 机制 个人笔记原文:[序列化器与 SPI 机制](https://www.yuque.com/wakoo-fvkfd/pu8unv/sywvpope1cs4leuz#c4Ic7) 个人博客积累: 1. [设计模式 - 单例 (稀土掘金)](https://juejin.cn/post/7451121379976052787) 2. [工具[序列化解析] - 序列化器实现方式 (支持 JSON、Hession、Kryo、ProtoBuf)](https://www.yuque.com/wakoo-fvkfd/novg6x/cvormg5rcdegi97n) 3. [序列化难题何解?Java 中的支持 JSON、Hessian、Kryo、Protobuf 序列化器实现和应用探究 Jav - 掘金](https://juejin.cn/post/7438239865391529984) ### 阶段成果 1. 支持多种序列化器实现方式 `JDK + JSON + Kryo + Hessian` 实现序列化器 2. `扩展`- 实现 `Protobuf`序列化器,支持 `RpcConfig`默认配置和 SPI 配置 3. `优化`- 基于静态内部类方法实现 `懒汉式 - 单例模式` 创建序列化工厂 4. `优化`- 基于 `双重检验锁校验机制`实现 `懒汉式 - 单例模式`创建序列化工厂 ### 简示图 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/KawskkgaXkBmV63D.webp" alt="" width="80%" /> ### 序列化器实现方式比较 | **序列化器** | **优点** | **缺点** | | --- | --- | --- | | **原生 Java Serializable** | 1. 简单易用,便于Java应用中的对象持久化。<br/>2. 兼容性好,与Java语言及框架无缝集成,操作流畅。 | 1. 性能差。<br/>2. 调试困难。<br/>3. 无版本控制,类结构调整易引发反序列化问题或数据不一致,而 Protobuf 具有良好的版本兼容性。 | | **JSON** | 1. 易读性好,可读性强,便于人类理解和调试。 <br/>2. 跨语言支持广泛,几乎所有编程语言都有 JSON 的解析和生成库。 | 1. 序列化后的数据量较大,因为 JSON 以文本格式存储,需要额外的字符来表示键、值和结构。<br/><br/>2. JSON 在处理复杂数据结构和循环引用时能力较弱,可能导致性能下降或序列化失败。 | | **Hessian** | 1. 二进制序列化数据量小,传输效率高。<br/><br/>2. 支持跨语言,适合分布式系统服务调用。 | 1. 性能较JSON略低,因为需要将对象转换为二进制格式。 <br/>2. 对象必须实现Serializable接口,限制了可序列化的对象范围。 | | **Kryo** | 1. 高性能,序列化和反序列化速度快。 <br/>2. 支持循环引用和自定义序列化器,适用于复杂的对象结构。 <br/>3. 无需实现Serializable接口,可以序列化任意对象。 | 1. 不跨语言,只适用于Java。 <br/>2. 对象的序列化格式不够友好,不易读懂和调试。 | | **Protobuf** | 1. 高效的二进制序列化,序列化后的数据量极小。 <br/>2. 跨语言支持,并且提供了多种语言的实现库。 <br/>3. 支持版本化和向前/向后兼容性。 | 1. 配置相对复杂,需要先定义数据结构的消息格式。<br/> 2. 对象的序列化格式不易读懂,不便于调试。 | **参考 Dubbo 配置序列化器的方式** [Dubbo - 序列化](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/serialization/hessian/) <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/5XXXpVWmSdHy2bKB.webp" alt="" width="80%" /> ### SPI 机制 > SPI (Service Provider Interface) 是 Java 中的一种机制,用于支持模块化开发和插件扩展。它允许服务提供者文件通过配置文件注册实现,系统通过反射动态加载这些实现。不修改原有代码的情况下实现解耦和增强可扩展性 > ### 实现 之前在简易版 RPC 框架内已经实现了基于 JDK 的序列化器 新建包 `com.jools.joolsrpc.serializer` 存放所有序列化器相关 1. 实现 `JsonSerializer`, 基于 `jackson-databind` 2. 实现 `KryoSerializer`,基于 `kryo`和 `ThreadLocal` 保证每个线程有一个单独的 `Kryo` 对象实例 3. 实现 `HessianSerializer`, 基于 `hessian` 版本 `4.0.66` 4. 接口 `SerializerKeys` 列举所有支持的序列化器 Key **序列化器工厂实现方案一:** 1. 简单工厂,基于 `Map`存储 `SerializerKeys`-> `Serializer`的映射关系,默认使用 JDK。支持基于 key 查询 `Map`返回相应的序列化器实例 2. 扩展 `RpcConfig`, 支持配置指定序列化器 ```java public class RpcConfig { .... private String serializer = SerializerKeys.JDK; } ``` 3. 优化提供者基于工厂和配置类获取指定序列化器 ```java //动态基于 RpcConfig 配置获取序列化器 final Serializer serializer = SerializerFactory.getInstance(RpcApplication.getRpcConfig().getSerializer()); ``` **序列化器工厂实现方案二:** 1. 自定义 SPI 机制的扫描路径 `/resources/META-INF/rpc/` 2. 分为 `cutom`子目录(用户自定义)和 `system`子目录(配置系统自带设置) 3. `system`子目录下添加 `SerializerKeys` 和该序列化器实现类全类名 ```properties jdk=com.jools.joolsrpc.serializer.impl.JdkSerializer hessian=com.jools.joolsrpc.serializer.impl.HessianSerializer json=com.jools.joolsrpc.serializer.impl.JsonSerializer kryo=com.jools.joolsrpc.serializer.impl.KryoSerializer ``` 4. 编写 `SpiLoader`加载器,扫描并转载自定义 SPI 机制目录下的所有配置 5. 关键属性如下: 1. `Map<String, Map<String, Class<?>>> loaderMap`: `接口名 -> { 配置键名[例如 SerialzerKeys 内配置] , 实现类全类名 }` 2. `instanceCache`: 对象实例缓存, 存储 `全类名` -> `对象实例` 3. `SPI_RPC_SYSTEM_DIR`: 默认读取的系统目录 `META-INF/rpc/system` 4. `SPI_RPC_CUSTOM_DIR`:默认读取的用户自定义目录 `META-INF/rpc/custom` 5. `loadClassesList`: 动态加载类的列表 目前只有: Serializer.class 6. `SPI_LOAD_DIR`:需要扫描的所有目录,注意顺序 `先扫描默认,再扫描自定义` `{SPI_RPC_SYSTEM_DIR, SPI_RPC_CUSTOM_DIR}` 6. 关键方法包含: 1. `getInstance(Class<?> cls, String key)`: 根据接口类名获取其支持的所有 key 标识,再通过这些标识找到对应的实现类全名。然后使用反射创建实例,并通过 `instanceCache` 缓存来确保单例,从而提高系统吞吐量。 2. `load(Class<?> loadClass)`:基于接口名,扫描所有 SPI 目录,读取文件内容, 同时将其加入到 `loaderMap` 7. `SerializerFactory` 初始化时,通过 `SpiLoader` 的 load 方法加载所有序列化器实现类。之后,使用 getInstance 方法获取特定的序列化器实例。 8. 测试: 1. 非法序列化器实现类全类名会导致反射生成实例失败 2. 测试,获取非法 `SerializerKeys`例如 `aa`, 会获取失败 3. 测试配置相同的key, 若实现类配置得不同,自定义配置会覆盖系统配置 4. 测试支持基于 `.properties/.yaml/.yml` 配置可切换序列化器 ### (扩展) - 实现更多不同协议的序列化器 protobuf **参考文档** 1. [Protocol Buffers - 官网](https://protobuf.dev/) 2. [ProtoBuf 入门教程 - 梯子教程网](https://www.tizi365.com/archives/367.html) 3. [Releases · protocolbuffers/protobuf 下载](https://github.com/protocolbuffers/protobuf/releases) **依赖** ```xml <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.21.12</version> </dependency> ``` **实现步骤:** 1. 安装 `ProtoBuf`编译器 2. 安装 `IDEA` 插件支持,插件名称 `Protobuf Generator` 3. 配置 `IDEA` 快速编译 `GenProtobuf` 1. Tools->Configure GenProtobuf 2. Protoc Path:安装 protoc 编译器的路径 3. 构建 `Java`, 生成 Protobuf 的路径 4. 实现 `Serializer`接口,支持基于 `Protobuf`的序列化器,测试 5. 扩展序列化器常量 `SerializerKeys`,添加 `PROTOBUF` 选项 6. `\META-INF\rpc\system`目录下新增 `protobuf` = `实现全类名` 7. 测试 - 注册新序列化器 8. 测试 - 切换序列化器 ### (扩展) - 序列化工厂修改为懒汉式单例 实现方式: 1. 基于静态内部类实现 > 1. 原理 > > 当外部类加载时,并不会立即加载静态内部类。只有在外部类访问静态内部类的方法或成员时,静态内部类才会被加载。 在使用静态内部类实现单例模式时,单例对象是静态内部类的一个静态成员。当外部类第一次调用获取单例对象的方法时,静态内部类会被加载,并创建单例对象。这样就实现了延迟加载,即在需要时才创建单例对象。。 > > > > 2. 保证线程安全 > > 静态内部类的延迟加载有助于保证线程安全,并且单例对象仅在需要时才创建,从而降低了多线程环境下的竞争条件风险。 > ```java @Slf4j public class SerializerFactory { private static class SerializerFactoryHolder { private static final SerializerFactory SERIALIZER_FACTORY = new SerializerFactory(); } public static SerializerFactory getInstance() { return SerializerFactoryHolder.SERIALIZER_FACTORY; } .... } ``` 2. 测试 ### (扩展) - 修改 SpiLoader 用懒加载获取实例 实现方式: 1. 基于双重校验锁机制实现 ```java public class SpiLoader { private SpiLoader() { log.info("Enter SpiLoader Class `private` Constructor...."); } public static SpiLoader getSpiLoaderInstance() { if (spiLoaderInstance == null) { synchronized (SpiLoader.class) { if (spiLoaderInstance == null) { spiLoaderInstance = new SpiLoader(); } } } return spiLoaderInstance; } } ``` ## 阶段 05 - 实现注册中心 **个人笔记原文:**[注册中心实现](https://www.yuque.com/wakoo-fvkfd/pu8unv/oz0fnwp8kn6dc1tu) **个人博客积累:** 1. 工厂模式 `特征、实现方法` 1. 简单工厂 [简单工厂 Simple Factory](https://juejin.cn/post/7451145897809412133) 2. 工厂模式 [工厂模式 Factory ](https://juejin.cn/post/7455990872204263436) 3. 抽象工厂模式 [抽象工厂](https://juejin.cn/post/7460789387518787603) 2. Etcd `常用操作 + 需求场景 (注册、监听、上线、下线)` [Etcd](https://www.yuque.com/wakoo-fvkfd/uc6nk1/pcw3f6whcu6n1a0a) **参考文献:** [创建型 - 简单工厂(Simple Factory)](https://pdai.tech/md/dev-spec/pattern/3_simple_factory.html#%E6%80%BB%E7%BB%93) [【创建型模式一】简单工厂(Simple Factory)](https://www.jianshu.com/p/a9f397c4ff98) [Carson带你学设计模式:简单工厂模式(SimpleFactoryPattern)](https://www.jianshu.com/p/e55fbddc071c) [品设计模式 - (创建型) 简单工厂模式 Simple Factory](https://juejin.cn/post/7451145897809412133) 《图解设计模式》 - Factory / Abstract Factory [Carson带你学设计模式:抽象工厂模式(Abstract Factory)](https://www.jianshu.com/p/7deb64f902db) [【创建型模式三】抽象工厂(Abstract Factory)](https://www.jianshu.com/p/e873855e88a0) ### 阶段成果 1. 实现基于 Etcd 的注册中心,借助租约 (Lease)、监听 (Watch) 特性实现注册中心核心能力 ### 简示图 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/2M9hvex4M9HXq9kJ.webp" alt="" width="80%" /> **新增类 - UML 图梳理** 注册中心相关: + 通过 `SpiLoader` 机制加载注册中心配置 + 基于 `RpcConfig` 中的 `RegistryConfig` 获取到对应的 `RegistryKeys`内配置的注册中心类型 + `Registry` 注册中心具体实现类 `EtcdRegistry` 完成服务注册、续期、监听机制 + 服务注册信息借助 `ServiceMetaInfo` 进行封装 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/vxhdsZsgrCnpPuhy.webp" alt="" width="80%" /> **注册中心工作示意图** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/LDPl2eQCtTuXqBp3.png" alt="" width="80%" /> ### 实现 #### Etcd 入门 GitHub 仓库: [Etcd仓库](https://github.com/etcd-io/etcd) 官方文档: [Etcd-Doc](https://etcd.io/) 可视化工具: [etcdkeeper](https://github.com/evildecay/etcdkeeper) Etcd Java 客户端:[主流 Java Etcd 客户端 - Jetcd](https://github.com/etcd-io/jetcd) **步骤** 1. 注册中心选型:Etcd; > Go 语言实现的、开源的、分布式 的 键值存储系统,它主要用于分布式系统中的服务发现、配置管理和分布式锁场景 > 2. **注册中心核心能力:**数据分布式存储 + 服务注册 + 服务发现 + 心跳检测 + 服务注销 + 扩展(注册中心容错、服务消费者缓存) 3. Etcd 快速入门 + 核心数据结构 + 特性 + Raft 一致性算法 4. Etcd 安装 `版本: 3.5.16` + 启动 + 命令行基本操作 (`put + get + del`) 5. Etcd 可视化工具安装 - `EtcdKeeper` 6. Etcd Java 客户端安装 `Jetcd` + `Jetcd`快速入门 7. Jetcd 内常用客户端梳理, 基于 `io.etcd.jetcd.Client`获取 | **客户端名称** | **作用** | | --- | --- | | **KVClient** | 操作键值对:设置值、获取值、删除值、列出目录。 | | **LeaseClient** | 管理租约:创建、续约、撤销租约,为键值对分配生存时间,自动删除过期键值对。 | | **WatchClient** | 监视键变化:实时监听键的变化并接收通知。 | | **ClusterClient** | 管理集群:添加/移除成员,获取健康状态和成员列表,执行选举操作。 | | **AuthClient** | 身份验证:管理用户、角色等权限信息,授予或撤销权限。 | | **MaintenanceClient** | 维护操作:健康检查、数据备份、快照、压缩、成员维护等。 | | **LockClient** | 分布式锁:创建、获取、释放锁,实现并发控制。 | | **ElectionClient** | 分布式选举:创建选举、提交选票、监视选举结果。 | #### 存储结构设计 要点: 一个服务可能有多个服务提供者(负载均衡) 1. **层级结构 (比如 Etcd)** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/0qb9rnLj5hYxl28p.webp" alt="" width="80%" /> 2. **列表结构 (比如:Redis 中的 List 数据结构)** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/i3cW0T3hzNs69ZEf.webp" alt="" width="80%" /> #### 代码实现 1. 定义服务注册信息类 `ServiceMetaInfo` 封装注册信息包括: `服务名称 + 服务版本号 + 服务地址 + 服务分组` 2. 支持获取服务注册键名 - 格式: `serviceName:serviceVersion` 3. 支持获取服务注册节点键名 - 格式: `serviceName:serviceVersion:IP:Port` 方便注册 4. 扩充 `RpcConstant` 和 `RpcRequest` 新增服务版本号字段 `serviceVersion` 5. 新增注册中心配置类 `RegistryConfig`, 维护注册中心配置 6. 支持全局配置 `RpcConfig` 持有 `RegistryConfig`实例;默认实现为 `Etcd` 7. 新增注册中心接口 `Registry`,定义核心功能方法 1. 初始化 `init(RegistryConfig registryConfig)` 2. 注册服务:基于服务注册信息 `ServiceMetaInfo` 构建注册节点信息 `/rpc/serviceName:serviceVersion/serviceHost:servicePort` 3. 注销服务 `unRegistry(ServiceMetaInfo serviceMetaInfo)` 4. 服务发现 `serviceDiscovery(String serviceKey)`(获取服务节点列表):基于服务注册信息构建查询服务的 key `/rpc/serviceName:serviceVersion` 5. 服务销毁 `destory()` 8. 基于 Etcd 实现注册中心接口 `EtcdRegistry` 9. 新增注册中心常量 `RegistryKeys` 类, `key`标记注册中心类型 `ETCD="etcd"`, 默认支持 `etcd` 10. 实现基于自定义 SPI 机制配合 SpiLoador 实现的简单工厂模式的 `RegistryFactory` 11. 自定义 SPI 资源目录下新增关于注册中心的实现, 默认 `etcd` 12. 扩充代理类的实现逻辑,通过配置 `RpcConfig` 获取 `RegistryConfig` 实例构建 HTTP 向 `注册中心` 发送服务发现请求; 向服务发现结果发送请求并响应结果 13. 测试 - Provider 注册服务 + 启动消费者实现 RPC 请求调用 + 成功相应结果 ### 流程梳理 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/TQT514aLPaTdVe0M.webp" alt="" width="80%" /> ## 阶段 06 - 注册中心优化 个人笔记:[注册中心优化](https://www.yuque.com/wakoo-fvkfd/pu8unv/bhlfat7y76rofsgc#Lbv25) 个人博客积累: 1. Hutool - `CronUtil.schedule()` 实现心跳检测 [Hutool - CronUitl](https://www.yuque.com/wakoo-fvkfd/novg6x/hmsu7lh2rxl09ntf#WJjLT) 2. JVM 虚拟机的 `ShutdownHook` [JVM安全/异常退出机制](https://www.yuque.com/wakoo-fvkfd/novg6x/hu0nuk52dwglifl2#QNJIa) 3. Zookeeper 入门 - 常用操作 (注册、上线、下线、监听) 1. ZooKeeper 概念简介入门 [ZooKeeper基础概念](https://www.yuque.com/wakoo-fvkfd/uc6nk1/awnl6024bbqo4blr) 2. [ZooKeeper 基本操作指令](https://www.yuque.com/wakoo-fvkfd/uc6nk1/cz813o3no9h09glw#RHw0z) 3. [Java操纵 ZooKeeper ](https://www.yuque.com/wakoo-fvkfd/uc6nk1/giumw597gyg2dfe7) 4. 了解学习观察者模式 [个人博客 - 行为型观察者模式](https://juejin.cn/spost/7461208406046195751) 5. 了解学习策略模式[个人博客 - 行为型策略模式](https://juejin.cn/post/7461208406046195751) 6. 复习 `Redis`: [Redis基础-数据结构](https://www.yuque.com/wakoo-fvkfd/mysql/iy0m8lghyr7xlrts) ### 阶段成果 1. Etcd 注册中心实现心跳检测和续期机制 2. 实现服务节点下线后清除注册信息缓存 3. 添加注册中心服务信息缓存机制 4. 实现基于 ZooKeeper 的注册中心 5. (优化) 完善注册信息,扩展更多字段,增加: 1. `registerTime` 节点注册时间 2. `startTime` 节点启动时间 3. `protocol` 服务通信协议,比如可扩展:HTTP、HTTPS、gRPC 等 4. serviceWeight 服务权重,用于后期实现权重轮询 5. metadata 自定义元数据,支持未来扩展 6. (优化) 实现支持 Redis 作为注册中心 7. (优化) 构建 Etcd 集群 8. (优化) 采用策略模式实现 key 监听 9. (优化) 增加消费者端缓存,实现服务注册信息失效兜底策略 ### 示意图 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/XcpiQOwap4B1qm99.webp" alt="" width="80%" /> <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/grgc2AIU1mZYF8K1.webp" alt="" width="80%" /> ### 优化 Etcd 作为注册中心实现 #### 实现心跳续期 思路: 1. Provider 向 Etcd 注册的时候,设置 `TTL`,过期之后自动删除该服务信息 2. Provider 定时请求 `Etcd` 续签自己的注册信息,更新 `TTL` 实现: 1. Registry 新增 `heartBeat` 心跳检测 2. 借助 Hutool 工具类中的 `CronUtil` 实现定时任务, 对所有查询到的注册节点信息进行重新注册 #### 实现服务节点下线 下线方案分类 1. 主动下线:服务提供者项目正常退出时,从注册中心移除注册信息。 2. 被动下线:服务提供者项目异常退出,利用 Etcd 的 key 过期机制自动移除。 实现: 1. 借助JVM 的 ShutdownHook : Java 虚拟机提供的机制,可让开发者在 JVM 即将关闭时执行清理工作或必要操作,如关闭数据库连接、释放资源、保存临时数据。 2. 在 Registry 实例启动时 `init`方法内创建并注册 `Shutdown Hook`, 实现 JVM 退出的时候主动下线 #### 实现服务注册信息缓存 **思路:**多服务,需要基于本地 JVM `Map`集合实现;其中 `serviceKey` 作为 `key`;查询到的 `List<ServiceMetaInfo>` 作为 `value` 实现: 1. 实现操作缓存的方法,包括: 写缓存 `writeCache`、读缓存 `readCache`、清空缓存 `clear` 2. 在 registry 包下新增缓存类 `RegistryServiceCache` 3. 注册中心实现 `EtcdRegistry` 添加 `RegistryServiceCache`字段 4. 修改服务发现 `serviceDiscovery`方法:先读缓存,后查询更新缓存 #### 实现监听机制 > 当服务注册信息发生变更的时候,需要即时更新消费端缓存。 > <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/6BOiPJegI5Z8yHoL.webp" alt="" width="80%" /> 实现: 1. `EtcdRegistry`注册中心实现类实现 `watch(String serviceKey)` 中,新建监听 `key` 的集合;可以使用 `ConcurrentHashSet` 防止并发冲突 2. `EtcdRegistry`内借助 Jetcd 的 `watchClient` 监听 `WatchEvent` 3. 当 `Event` 为 **DELETE** 则实现清除服务缓存 #### 问题与解决 <font style="background-color:#E4495B;">报错</font> - 删除缓存失败 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/VkHdWm8Z68g7skMC.webp" alt="" width="100%" /> 1. 修复 - 键名添加到缓存时没有携带 `/rpc/` 前缀`watch`基于 ServiceNodeKey (serviceKey/ip:port) ```java //解析服务名称 List<ServiceMetaInfo> serviceMetaInfos = keyValueList .stream() .map((kv) -> { //监听key String key = kv.getKey().toString(StandardCharsets.UTF_8); //监控 serviceNodeKey watch(key); String value = kv.getValue().toString(StandardCharsets.UTF_8); //映射成 ServiceMetaInfo return JSONUtil.toBean(value, ServiceMetaInfo.class); }).collect(Collectors.toList()); ``` 2. 监听机制清除注册服务缓存时要基于 `serviceKey` (/rpc/全类名:version) 作为 key 清除 3. 清空缓存时需要添加 `ETCD_ROOT_PATH` (/rpc/)前缀,作为缓存的 key ### 实现 ZooKeeper 作为注册中心 #### 参考文档 + [Zookeeper 官方手册](https://zookeeper.apache.org/doc/r3.8.4/index.html) + [Linux - 安装 Zookeeper](https://blog.csdn.net/qq_18402475/article/details/135623697) + [Window下安装 Zookeeper](https://cloudyw.cn/2024/09/15/Zookeeper%E5%9C%A8windows%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E9%85%8D%E7%BD%AE/) Java 操作 ZooKeeper 的客户端 `Curator` 参考文档 + [Curator - 示例代码仓库](https://github.com/apache/curator/tree/master/curator-examples/src/main/java) + [Curator - 快速入门](https://curator.apache.org/docs/getting-started) 1. 基于 Curator 实现注册中心的核心功能: 服务发现 `serviceDiscovery` + 监听 `watch` + 注册 `registry` + 下线 `unRegistry` + 销毁 `destory` 2. 服务注册之前, 需要借助 `createServiceInstance()` 建造者模式将 `ServiceMetaInfo`封装为 `ServiceInstance` 3. `Zookeeper` 也实现服务注册信息缓存 4. 扩充 SPI 资源目录内容,新增关于 `ZooKeeper`的内容 5. 测试支持基于 `RpcConfig` 配置 `rpc.registryConfig.registryType` 项灵活切换注册中心 ### (扩展) - 服务注册信息`ServiceMetaInfo`添加更多字段 增加: + registerTime **<font style="background-color:#117CEE;">节点注册时间</font>** + startTime **<font style="background-color:#117CEE;">节点启动时间</font>** + protocol **<font style="background-color:#117CEE;">服务协议</font>** - 明确定义服务的通信协议(如 HTTP、HTTPS、GRPC、Dubbo 等) + serviceWeight **<font style="background-color:#117CEE;">服务权重 </font>**- 权重字段 `权重可选 0, 1, 2`;后期用于负载均衡 + metadata **<font style="background-color:#117CEE;">自定义元数据</font>** - 支持未来扩展 #### 实现 + 添加时间工具类 `DateUtils` + 在 `Provider` 注册服务构建 `ServiceMetaInfo` 的时候设置通信协议 (默认 HTTP);注册时添加注册时间 + 基于简单工厂 `RequestSender`,实现支持基于 `protocol`字段切换请求发送者;当前先实现基于 HTTP + 修改构建代理实例的 ServiceProxy, 基于请求得到的 `serviceMetaInfo` 内的 `protocol`字段调用相应的 `RequestSender` ### (扩展) - 实现 Redis 作为注册中心 实现步骤: 1. 实现 Registry 接口,基于 Jedis 实现 `RedisRegistry` 2. `init()` 方法:基于 `ip + port` 实例化一个 `Jedis` 3. `heartBeat()`: 借助 Hutool CronUtil; 支持秒级单位 4. `registry()`: 基于 `SETNX` 指令实现注册,默认设置过期时间 TTL 为 30s 5. `serviceDiscovery()`: 基于 `SCAN`迭代返回基于服务名前缀查询到的所有服务节点信息;再基于 `GET`操作基于查询详细的 `serviceMetaInfo` 6. `unRegistry()`: 删除本地服务节点注册信息和缓存 7. `destory()`: 清空本地服务节点注册信息和缓存 + 停止心跳检测任务 `CronUtil.stop()` 8. `watch()`: 独立线程进行订阅, 基于 `psubscribe`; 需要打开 Redis 事件监听配置 `config set notify-keyspace-events Ex` 9. 扩充 SPI 资源目录内容,新增支持 `Redis` 作为服务注册中心; 10. 测试支持基于 RpcConfig 配置项基于 `rpc.registryConfig.registryType`灵活切换为 Redis 作注册中心 ### (扩展) - 搭建 Etcd 集群 参考文档 [搭建 Window 下的 Etcd 集群](https://cloud.tencent.com/developer/article/2360396) [Etcd 官方 - How to Set Up a Demo Etcd Cluster](https://etcd.io/docs/v3.5/tutorials/how-to-setup-cluster/) ### (扩展) 策略模式实现注册中心 key 监听 1. 定义 `WatchStrategy` 接口持有 `watch(String serviceNodeKey)` 2. 分别实现基于 Etcd、ZooKeeper、Redis 的监听机制 ### (扩展) 服务注册信息失效(过期)兜底策略 - 建立消费端缓存服务信息 实现 1. 新建 `ConsumerServiceCache`类,持有 List 集合,消费端缓存兜底的注册信息 `ServiceMetaInfo` 2. 读缓存逻辑:当 `serviceDiscovery()` 查询信息为空则尝试读消费端缓存的服务信息。如果缓存为空,返回默认服务节点信息 3. 写缓存逻辑:如果 `serviceDiscovery()`查询不为空,则更新缓存; ### 扩展后示意图 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/qM5IprpMWPreMmGO.webp" alt="" width="80%" /> ### 可扩展点 1. 实现支持更多协议的请求 `HTTPS + gRPC + UDP + Dubbo` 等 (后续已经实现基于 TCP) 2. 处理逻辑 —— 如果服务注册信信息携带元数据 `MetaData` 3. 尝试搭建 `ZooKeeper \ Redis` 集群 4. 服务注册信息兜底 —— 添加更完备的兜底服务 [比如真实的 IP + port] ## 阶段 07 - 自定义协议 个人笔记原文:[自定义协议](https://www.yuque.com/wakoo-fvkfd/pu8unv/heugvpofiyofnxty) 博客积累: 1. 装饰器模式 [结构型 - 装饰器模式](https://juejin.cn/post/7467173944706269222) ### 阶段成果 1. 目标,用更少的空间传递必要的信息。基于 TCP 的更高效、简洁且灵活的RPC框架。 2. 定义自定义的RPC消息体结构`ProtocolMessage`。 3. 开发针对该自定义消息体结构的编码器和解码器。 4. 将请求处理器升级为支持TCP加自定义消息体,并通过编码/解码器来处理消息收发与解析。 5. 采用Vert.x的RecordParser解决TCP传输中的半包或粘包问题。 6. 利用装饰器模式实现(TcpBufferHandlerWrapper)增强客户端和服务端的消息处理能力。 7. (扩展) - 优化 RequestSenderFactory,支持基于 SPI 机制加载相应协议的发射器,支持自定义扩展 ### 参考文献 1. [HTTP/1.1 的缺点有哪些?](https://xiaolincoding.com/network/2_http/http_interview.html#http-1-1-%E7%9A%84%E4%BC%98%E7%82%B9%E6%9C%89%E5%93%AA%E4%BA%9B) 2. [Dubbo 协议详解](https://cn.dubbo.apache.org/zh-cn/blog/2018/10/05/dubbo-%e5%8d%8f%e8%ae%ae%e8%af%a6%e8%a7%a3/) ### 示意图 ### <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/CRAgTebRzmVFI8aY.webp" alt="" width="80%" /> **解码器 + 编码器** 工作流程 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/lFHWTFHIUmd3fsgy.webp" alt="" width="80%" /> **自定义消息体 ProtocolMessage 类图** 借助 Lombok 的 `@Builder`注解 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/3XsNMCRuUZiETDow.webp" alt="" width="80%" /> ### 消息体结构设计 1. 网络传输设计 1. 分析当前使用 HTTP 的劣势 2. 追求性能和自定义空间,转用 TCP 2. 消息结构设计 + **消息头信息** | 字段 | 数据类型 | 长度 | 作用 | | --- | --- | --- | --- | | messageId (唯一请求标识) | long | 64 bit / 8 个字节 | 唯一标识来追踪请求 | | magic (魔数) | byte | 8 bit / 1 个字节 | 魔数;安全 | | version (版本号) | byte | 8 bit / 1 个字节 | 保证请求和响应的一致性 | | serializerType (序列化方式) | byte | 8 bit / 1 个字节 | 告诉服务端和客户端如何解析数据 | | messageType<br/>(消息类型) | byte | 8 bit / 1 个字节 | 标识请求还是响应 | | messageState<br/>(消息状态) | byte | 8 bit / 1 个字节 | 如果是响应,携带响应码,比如 200 | | bodySize (请求体长度) | int | 32 bit / 4 个字节 | 标识 body 部分的长度;用于完整地获取到 `body` 内容信息 | + **消息体** | 字段 | 数据类型 | 长度 | | --- | --- | --- | | body | 不限制 (T) | 不限制 | **设计消息体结构图示** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/syohZBVEYMZaUEp3.webp" alt="" width="100%" /> 参考对比 Dubbo Protocol <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/LcnuZWQBGpEeDDpa.webp" alt="" width="100%" /> 3. **解决: TCP 本身存在的 半包 / 粘包问题** 1. 消息头中新增字段,标记 `请求体数据长度`;保证能够完整地获取 `body`内容信息 2. 而消息头是固定长度的:**17 个字节** (见上文中的消息结构设计) 3. 因此可以通过消息头获取到应该截取的请求体长度 4. 设计优势:不需要基于 `k - v` 形式携带消息,不需要借助字符串作为建,而是直接按照字节截取(比如前 8 bit) 就能够获取到 ### 开发实现 #### 实现自定义消息体 + 协议 1. 定义自定义TCP协议消息体 `ProtocolMessage`,包括消息头及其字段。 2. 创建协议常量 `ProtocolConstant`,提供默认的消息字段值。 3. 构建消息状态枚举 `ProtocolMessageStateEnum`,解析消息体内携带的 `messageState`涵盖请求成功 `2xx`、请求失败 `4xx` 和响应失败 `5xx` 等状态。 4. 设计消息类型枚举 `ProtocolMessageTypeEnum`, 解析消息体携带的 `messageType` 涵盖请求 Request, Response, Heartbeat等,其中键为 Byte 类型,值为字符串。 5. 设计消息序列化类型枚举 `ProtocolSerializerTypeEnum`,用于解析消息体携带的 `serializerType`字节区间,其键 Byte 类型,值为字符串。 6. 基于 Vert.x 框架实现 TCP 服务端 `VertxTcpServer` 与客户端处理器 `VertxTcpClient`。 #### 实现基于自定义消息题的编码 / 解码器 工作示意图 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/Gnc5YZ2ZucsIXyfs.webp" alt="" width="100%" /> 1. 实现消息编码器 `ProtocolMessageEncoder`:基于传入的 `ProtocolMessage` 实例构建字节数组的 ⌈ 消息头 + 消息体 ⌋,并根据序列化类型选择合适的序列化器进行数据序列化。 2. 实现消息解码器 `ProtocolMessageDecoder`:依据协议定义的 **17** 字节长度解析消息头,注意解析顺序需要和构造顺序一致, 获取到消息体的大小解析主体内容。目前仅支持 `REQUEST + RESPONSE` 类型 3. 基于 Vert.x 构建TCP请求处理器 `TcpServerHandler`,其功能包括: 1. 接收并使用 `ProtocolMessageDecoder` 解码请求以获得`RpcRequest`对象,进一步获取请求的服务类名 `serviceName`、方法名 `methodName` 等信息。 2. 根据解析的服务类名 `serviceName` 结果查找服务注册表并通过反射调用对应方法。 3. 将响应结果封装成 `RpcResponse`并通过`ProtocolMessageEncoder`编码后返回给客户端。 10. 修改消费者端的`ServiceProxy`,支持基于 TCP 传输和解码编码 11. 扩展通信协议选项,增加对TCP的支持;引入简单工厂模式以便根据协议类型(如 `HTTP` 或 `TCP`)获取对应的请求发送器 ( `HttpRequestSender` 或 `TcpRequestSender` )。 #### 解决 TCP 存在的半包粘包问题 思路:在消息头中设置请求体的长度,基于规定的长度截取消息头;在根据消息头内的长度截取消息头 1. 借助 `RecordParser` + 装饰器模式 2. `RecordParser` 先完整获取前 **17 Byte** 长度的消息头结构 3. 再根据请求头的解析到的消息体长度 `bodySize` 长度更改 `RecordParser`的固定长度 ## 阶段 08 - 负载均衡 **个人笔记原文** 1. [负载均衡](https://www.yuque.com/wakoo-fvkfd/pu8unv/poi1hgdgzr455n0u) **参考文章** 1. [常见的负载均衡算法](https://juejin.cn/post/7135802504826060837?searchId=202411301118034F107796282D84ABB02F) 2. [看懂 NGINX 负载均衡](https://juejin.cn/post/6844904106541203464?searchId=20241130113229AA1CB8D4674FA0B07290) 3. [五分钟看懂一致性哈希算法](https://juejin.cn/post/6844903598694858766?searchId=20241130185854C4DC98890E6215E1BCE7) 4. [Java 基于 TreeMap 实现一致性 Hash](https://juejin.cn/post/7275533017295798333?searchId=20241130185854C4DC98890E6215E1BCE7) ### 阶段成果 1. 了解负载均衡:介绍 + 目的 + 常用的负载均衡技术 2. 实现负载均衡算法:`轮询`+`随机` + `一致性 Hash` + `加权轮询` 负载均衡器 ### 示意图 **核心部件简示图** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/8iZfz4gypkJytUOh.webp" alt="" width="80%" /> **简示图** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/3lJIEuw7498e6fe2.webp" alt="" width="100%" /> **类图 - 负载均衡策略类图**<img src="https://pic.code-nav.cn/post_picture/1819386569259450369/cezU78gFdUR7Yyb0.webp" alt="" width="80%" /> ### 代码实现 1. 入门了解一致性哈希负载均衡:原理、特性及优势。 2. 开发基础负载均衡器:基于 `AtomicInteger` 实现轮询 `Round`;基于 `Random` 实现随机 `Random` 策略。 3. 实现一致性哈希负载均衡器 `ConsistentHash`: - 使用TreeMap存储节点。 - 节点选择规则:优先选取大于或等于请求哈希值的最近节点;若无,则返回环首节点。 4. 定义`LoadBalancerKeys` 接口,管理负载均衡类型常量,并通过工厂模式支持SPI机制。 5. 在自定义SPI资源中配置负载均衡选项,允许用户/开发者修改`RpcConfig`文件来切换不同的负载均衡策略。 6. 更新消费端`ServiceProxy`代码以利用负载均衡器获取服务节点。 7. 设置多个具有不同端口的服务提供者(Provider),并启动这些服务以测试负载均衡效果。 ### (扩展) - 优化一致性负载均衡器;基于 `Guava 的 MurmurHash()`实现 参考文献: [Java 基于 TreeMap 实现一致性 Hash](https://juejin.cn/post/7275533017295798333?searchId=20241130185854C4DC98890E6215E1BCE7) 选择 `Guava`包下的 `MurmurHash()`算法 + 针对每个服务节点 `hash( IP + Port )` + 这意味着相同的请求会被路由到同一个虚拟节点(从而映射到同一个真实服务节点) ### (扩展) - 实现 加权轮询 负载均衡算法 参考文献:[各种负载均衡算法实现](https://juejin.cn/post/7186835933725982781?searchId=20241201164210695573B1D891BE6221FE) **实现: ** 1. 目标:实现加权轮询方法 `RoundWeight` 2. 基于之前在 `ServiceMetaInfo` 内扩充的 `ServiceWeight`字段;`默认权重为 1` 3. 新增 `ServiceWeight` 接口存储权重常量 4. 权重计算逻辑: > + 计算所有服务节点的权重总和 `totalWeight` > + 每次选取服务节点之前,遍历各个节点,选取最大权重 > + 之后更新被选中的节点为 `currentWeight - totalWeight` > + 每次请求之后所有节点的权重累加上自身权重,但是总权重不变 > + 这样可以防止权重高的节点连续多次被选中,为其他节点留出机会。 > 4. 实现 `LoadBalancer`接口,基于权重计算逻辑实现加权轮询负载均衡器 5. 每次负载均衡选取完服务节点后,需要更新各个服务节点的权重,借助 `RegistryServiceUpdater` 1. `RegistryServiceUpdater` 借助 ⌈线程池 + CompletableFuture⌋ 实现服务节点的重新注册 2. 测试算法以及每轮的权重更新 ## 阶段 09 - 重试机制 个人笔记原文:[重试机制](https://www.yuque.com/wakoo-fvkfd/pu8unv/vqhqsixe502g8and#Gw3Rk) 参考文章: [参考文章 - 使用 guava-retrying 实现灵活的重试机制](https://cloud.tencent.com/developer/article/1752086) ### 阶段成果 1. 了解重试机制: 重试机制触发条件 + 重试时间 + 停止重试策略 + 重试工作 2. 了解常用的重试时间策略: 1. 固定重试间隔 (Fix Retry Interval) 2. 递增避退重试 (Increment Wait) 3. 指数退避重试 (Exponential Backoff Retry) 4. 随机延迟重试 (Random Delay Retry) 5. 可变延迟重试 (Variable Delay Retry) 3. 了解 Google 的 `Guava - Retrying`工具 4. 代码实现: 不重试策略 + 固定时间间隔重试策略 5. 优化:基于工厂模式 + SPI 机制,支持基于配置灵活切换重试策略 6. **(扩展)-** 支持实现 ⌈间隔递增避退⌋ + ⌈间隔指数递增避退⌋ + ⌈间隔随机避退⌋ ### 示意图 **核心部件简示图** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/xeDWzrj9B5C4MG0f.webp" alt="" width="100%" /> ### 实现 #### 快速入门 `Guava - Retrying` 1. `RetryBuilder`方法介绍:Retry + WaitStrategy + BlockStrategy + StopStrategy + AttemptTimeLimiter + RetryListener 2. 重试条件 - `Retry` > 根据执行结果 + 根据异常发生 > 3. 等待策略 - `WaitStrategy` > 固定时长 + 随机等待时长 + 递增等待时长 + 指数增长时长 + 斐波那契递增等待时长 + 异常等待时长 + 组合复合时长 > 5. 阻塞策略 - `ThreadSleepStrategy` 6. 停止策略 - StopStrategy > 永不停止 NeverStopStrategy + 指定最多重试次数 StopAfterAttemptStrategy + 指定最长重试时间 StopAfterDelaysStrategy > 7. 超时限制 - `AttemptTimeLimiter` > 不限制执行时间:NoAttemptTimeLimit > > 限制执行时间为固定值:FixedAttemptTimeLimit > 8. 重试监听器 - `RetryListener` 观察者模式,可以注册监听器 #### 基于上述工具实现重试 9. 开发实现 - 不重试策略:直接执行 `call`方法 10. 开发实现 - 固定间隔重试:重试条件(异常) + 等待策略(固定间隔)+停止策略(超过最大尝试次数后停止)+ 重试监听 (输出当前重试次数) 11. 新建配置重试常量 `RetryStrategyKeys` 12. 新建重试策略工厂类 `RetryStrategyFactory` 支持通过 SPI 机制加载 13. RpcConfig 内新增配置项 `retryStrategy`支持用户/开发者基于配置切换 ### (扩展) - 实现递增避退重试策略 基于 Retrying 工具集内的 + 等待策略 `WaitStrategy`设置为 递增等待规则 `incrementingWait` + 停止策略 `StopStrategy` 设置重试总数为: 4 **规则设计** | 起始间隔 | 3s | | --- | --- | | 递增间隔 | 3s | | 重试停止策略 | 重试 3 次之后 | ### (扩展) - 实现指数避退重试策略 基于 Retrying 工具集内的 + 等待策略 `WaitStrategy`设置为 递增等待规则 `exponentialWait` + 停止策略 `StopStrategy` 设置重试总数为: 4 **规则设计 ** | 起始间隔 | 1s | | --- | --- | | 指数递增间隔 | 2 的幂次方 | | 重试停止策略 | 重试 3 次之后 | ### (扩展) - 实现随机延迟重试策略 基于 Retrying 工具集内的 + 等待策略 `WaitStrategy`设置为 递增等待规则 `randomWait` + 停止策略 `StopStrategy` 设置重试总数为: 4 **规则设计** | 最小间隔 | 4s | | --- | --- | | 最大间隔 | 16s | ## 阶段 10 - 容错策略 个人笔记原文:[容错机制](https://www.yuque.com/wakoo-fvkfd/pu8unv/xkmga43iixi69zgy#xO8aq) ### 阶段成果 1. 了解常见的容错策略 2. 基于容错策略接口实现多种容错机制,支持基于 SPI 和配置文件灵活修改 3. 重试策略之后启用生效后使用容错策略处理 4. 容错策略实现: 1. `Fail - Fast` 快速失败 (直接抛出相关异常) 2. `Fail - Safe` 静默处理 (仅返回 RpcResponse) 5. (扩展) 实现 `Fail - Back` 失败自动恢复 (参考 Dubbo 的本地伪装服务) 6. (扩展) 实现 `Fail - Over` 故障转移 (基于 ServiceDiscovery 获取到所有服务节点后尝试访问其他服务节点) ### 示意图 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/4jfnj2bA3GgEuaDq.webp" alt="" width="100%" /> #### 重试策略设计 UML <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/ZRsu3FOZLRM39QVl.webp" alt="" width="80%" /> #### 负载均衡 + 重试策略 + 容错机制流程 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/pzugkpCEuTM1olQ7.webp" alt="" width="80%" /> ### 实现 1. 常见的容错策略实现:FailOver`故障转移` + FailBack`失败故障恢复`+ FailSafe`静默处理`+ FailFast`快速失败` 2. 常见的容错工作机制有:继续重试 + 限流 + 降级 + 熔断 + 超时控制 3. 定义重试机制接口 `ErrorTolerantStrategy`;各个容错策略实现该接口 4. 实现 FailFast: `FailFastTolerantStrategy`直接抛出异常 5. 实现 FailSafe: `FailSafeTolerantStrategy` 遇到异常后返回一个相应对象 `RpcResponse` 6. 添加容错配置常量 `ErrorTolerantStrategyKeys` 列举所有支持的容错策略键名;支持基于配置 `RpcConfig`灵活切换重试策略 7. 基于简单工厂模式实现 `ErrorTolerantStrategyFactory`;支持根据容错策略键名返回对象; 8. 添加自定义 SPI 资源文件目录下,RpcConfig 新增配置项,支持基于配置切换 9. 修改消费端 `ServiceProxy`,应用容错策略。 ### 扩展 - 实现 FailBack 容错机制 参考文档: [Dubbo - 服务讲解 - 本地伪装](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/tasks/framework/more/local-mock/) [Dubbo Mock 本地伪装示例代码](https://github.com/apache/dubbo-samples/blob/master/2-advanced/dubbo-samples-mock/) **服务容错 - 本地伪装** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/ltCyS10dX72oDZm2.jpg" alt="" width="100%" /> 实现: 1. 消费者端实现 `UserService` 接口,作为本地的伪装服务。 2. 在消费者端的 `exp-consumer` 模块中,使用 `ConcurrentHashMap` 来维护一个本地的 Mock 服务注册中心 (`LocalServiceMockRegistry`)。 3. 当重试策略完成后,采用 FallBack 策略来查询这个本地 Mock 服务注册中心。 4. 查询成功则返回结果;如果失败,则输出错误信息。 5. 将 `LocalServiceMockRegistry` 功能优化并迁移至 `rpc-core` 模块中。 6. 构建一个简单的消息队列。当重试机制失效时,启用容错机制,并把当前请求的信息放入队列尾部。 7. 在启用 FailBack 容错策略的情况下,消费端从队列中取出请求信息,并再次尝试通过本地 Mock 服务注册中心进行处理。 这样处理后,既保留了原有逻辑的核心部分,又使得表述更加简洁明了。 **流程梳理** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/RllSrelriGvDHPEc.webp" alt="" width="80%" /> ### (扩展) - 实现 FailOver 容错机制 **思路** + 通过 serviceDiscovery 获取多个服务节点。 + 当某个节点的重试策略失败时,切换到下一个服务节点。 **实现步骤** 1. 修改 ServiceProxy,利用 `ErrorTolerantStrategy` 接口定义的方法参数 `Map<String, Object> context` 传递以下信息: - `RpcRequest:` 请求的服务信息 - `selectedServiceMetaInfo`: 已访问的服务节点信息 - `serviceInfos`: 所有可用的服务节点信息 - `retryStrategy`: 重试策略 - `sender`: 基于协议的请求发送者 2. 实现 ErrorTolerantStrategy 接口,定义 FailOver 逻辑以尝试下一个可用的服务节点。如果请求成功,则返回响应;否则继续尝试其他节点,直到所有节点都被尝试过为止。 + 启动多个服务提供者,并将它们注册为相同名称的服务到注册中心。 ### (扩展) - 基于注解驱动实现 FailBack 策略 #### 注解设计 `@JRpcFailBack` 设计 - 仅支持作用于属性字段 `@Target(ElementType.FIELD)` 1. `serviceName`: 本地伪装的服务名 (全类名) 2. `mock`: 可以指定本地伪装实现类的全类名;如果未设置,默认选用第一个查询到的服务作为伪装服务 `@MockScanPackage` 设计 1. basePackage: 指定扫描的包名,扫描注册该包下的所有伪装服务 #### 实现 1. 优化本地伪装服务注册中心,使用`ConcurrentHashMap`存储服务类名和服务实现类列表。 2. 启动时,Consumer扫描标记了`@MocScanPackage`注解的包,并将这些类注册到`LocalServiceMockRegister`中。 3. 扫描所有带有`@JRpcFailBack`注解的字段,检查其服务是否支持本地伪装。 4. 若发现带`@JRpcFailBack`注解但未设置RPC框架为FailBack模式,则抛出错误。 5. 测试配置正确性,配置不当时报错。 6. 测试未注册伪装服务的情况。 7. 验证已注册的本地伪装服务能否正常工作。 #### 优化 1. **问题分析**:当前仅支持默认调用首个配置的伪装服务(按照 `serviceDiscovery()` 查询到后存储在LocalServiceMockRegister中的顺序)。 2. **优化思路**: - 扩展`@JRpcFailBack`注解,添加`mockServiceName`字段指定本地伪装类全名。 - `LocalServiceMockRegistry`新增`bindMockService`方法来绑定特定`mockServiceName`与指定的伪装服务类。 - 在扫描`@JRpcFailBack`注解时,如果`mockServiceName`非空,则建立服务类与指定伪装类之间的映射。 - 容错触发时,根据`serviceName`查找对应的本地伪装服务类;若指定了伪装类则使用该指定类,否则使用列表中第一个服务作为默认。 3. **修改后的 **`@JRpcFailBack`** 注解** 包括: | 字段名 | 类型 | 说明 | 默认值 | | --- | --- | --- | --- | | serviceName | String | 服务名称(全类名); | 空字符串 | | mockServiceName | String | 本地伪装服务名称(全类名) | 空字符串 | | mock | boolean | 是否开启本地伪装机制 | false | 4. 修改后的 **`@MockScanPackage`** | 字段名 | 类型 | 说明 | 默认值 | | --- | --- | --- | --- | | basePackage | String | 指定扫描的包名 | com.jools.exp.consumer.api | 5. 扩展 `LocalServiceMockRegistry` 支持为每个服务绑定一个特定的本地伪装服务。 6. **测试**: - 测试默认情况下的行为,即调用列表中的第一个服务。 - 测试指定单一服务的情况。 ## 阶段 11 - 启动机制和注解驱动 **个人笔记原文:**[启动机制和注解驱动](https://www.yuque.com/wakoo-fvkfd/pu8unv/ow2y6n14ff5w81te) **参考文献:** 1. [Dubbo API 开发微服务应用](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/quick-start/api/) ### 阶段成果 1. 注解设计:`@EnableJRpc`+ `@JRpcReference` + `@JRpcService` 2. 参考 Dubbo 为服务提供者和消费者写启动类并简化代码。用三大注解(@EnableJRpc、@JRpcService、@JRpcReference)。 3. 采用 Bean 监听机制,实现 BeanPostProcessor 接口,依注解执行服务注册和代理对象注入。 4. 基于 SpringBoot 的 RPC starter 模块,扫描启动类 @EnableJRpc 注解,支持启动后基于注解驱动。 ### 示意图 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/Mn46mflYNa6rdwNn.webp" alt="" width="100%" /> **设计的启动器类** <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/U1ABNqsOrLTkk4T3.png" alt="" width="80%" /> <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/gQYzoZ0z2T86VATs.png" alt="" width="50%" /> ### 实现 #### 封装启动器 1. 参考 Dubbo 设计和示例。测试基于 Dubbo 启动器实现 2. 注解驱动设计:主动扫描 + 监听 Bean 的加载 3. 将 Provider 服务注册信息: `serviceName`+ `implClass`封装成 `ServiceRegisterInfo` 4. 实现 `ProviderBootstrap`: 将传入的 ServiceRegisterInfo 集合注册服务实现类到本地服务中心,并封装成 ServiceMetaInfo 注册到服务注册中心。 5. 简化 Provider 启动类,调用 `ProviderBootstrap` 6. 修改 Consumer 启动类,调用 `ConsumerBootstrap` #### 基于 Spring Boot 的注解驱动 7. 新建基于 Spring Boot 的项目,开发注解驱动 8. 参考Dubbo 中支持的三大核心注解: `@EnableDubbo` + `@DubboReference` + `@DubboService` 自定义类似注解 **设计实现 **`@EnableJRpc`** 注解** | 注解字段名 | 数据类型 | 默认值 | 内容 | | --- | --- | --- | --- | | needServer | boolean | true | 是否需要启动 Web 服务器 (区分 Consumer / Provider) | | useStarteSDK | boolean | true | 是否启用 SDK 配置 RPC 框架 | **设计实现 `@JRpcService`注解** | 注解字段名 | 数据类型 | 默认值 | 内容 | | --- | --- | --- | --- | | serviceClass | Class<?> | Void.class | 提供服务的服务接口类全类名 | | serviceVersion | String | RpcConstant.<br>DEFAULT_SERVICE_VERSION | 服务版本 | **设计实现 `@JRpcReference` 注解** | 注解字段名 | 数据类型 | 默认值 | 内容 | | --- | --- | --- | --- | | interfaceClas | Class<?> | Void.class | 查询的服务接口类全类名 | | serviceVersion | String | RpcConstant.<br>DEFAULT_SERVICE_VERSION | 服务版本 | | retryStrategy | String | RetryStrategyKeys.fixInterval | 重试策略 | | loadBalanceStrategy | String | LoadBalancerKeys.ROUND_ROBIN | 负载均衡策略 | | errorTolerantStrategy | String | ErrorTolerantKeys.FAIL_FAST | 容错策略 | | enableMock | boolean | false | 是否开启接口 Mock 测试 | 12. 实现 Rpc 框架的全局启动类 `RpcInitBootstrap`:扫描 `@EnableRpc` 注解并解析属性, 如果 `needServer 为 true` 则需要初始化基于 Vert.x 的 TCP 处理器。 13. 服务提供者启动类 `RpcProviderBootstrap`:实现扫描将被 `@JRpcService` 标识的类注册到本地服务中心 `Local Registry` (后期请求调用)+ 远端服务注册中心[Etcd / ZooKeeper / Redis] (供查询) 14. 服务消费者启动类 `RpcConsumerBootstrap`: 实现 Bean 后置处理器,在 Bean 初始化后,反射获取所有属性字段。若字段有`@RpcReference` 注解,为该属性生成代理对象后赋值。 15. 给 `@EnableRpc` 增加 `@Import` 注解,注册自定义启动类,启用加载器 ### (扩展) JRPC - Spring - Boot - Starter 项目支持读取 `.yml/.yaml` 格式文件 #### 配置加载规则设计 1. 基于传入实参后缀格式加载相应配置文件。 2. 若后缀合法,按格式匹配加载,优先级为 `.properties>.yaml>.yml`。 3. 若不传入后缀,按此顺序加载,成功一份即返回,否则加载 `rpc-core`模块内 RpcConfig 的默认配置。 4. 若后缀非法,若 starter 模块有 `application.properties` 则加载返回,否则加载 `rpc-core`模块内 RpcConfig 默认配置。 #### 思路 1. 添加工具类 `StarterConfigUtils`,解析 `/resources/` 目录下配置文件。 2. 优先级:`.properties >.yaml >.yml`。用默认 RpcConfig 字段值作配置兜底。 #### 实现 1. 复用 `rpc-core`模块内已经由的配置解析工具 2. 在 starter模块内新建配置解析类 `StarterConfigUtils` 3. 测试是否按照设计的优先级加载配置 ### (扩展) 区分消费端和服务端,简化消费端启动流程。 #### 实现 1. 扩展 `RpcApplication`,支持 key 传入区分消费与提供端,依 `needServer` 分辨消费者和提供者。 2. `StarterConfigUtils` 添加 `initStarterRegistry()`。调用 `RpcApplication` 的 `initRegistry()`。基于可变参数,传入 `key = true` 则启用 Web 服务器并开启心跳续期机制。 ### (扩展) JRPC - Spring - Boot - Starter项目支持基于注解启动本地伪装服务(容错) `容错策略`阶段内已经实现基于注解 `@JRpcFailBack`指定本地伪装。参考之前设计,优化并实现基于 SpringBoot Start 启动器的本地伪装 FailBack #### 注解设计 1. 定义 `@FailBackService` 注解,标记为本地伪装服务 | 字段名 | 类型 | 默认值 | 解释 | | --- | --- | --- | --- | | mockServiceName | String | 空字串 (以注解标记类实现的接口名) | 为mockServiceName 服务名提供本地注册服务 | 2. 定义 `@LocalMockScanPackage` 注解, 指定本地伪装扫描的包 | 字段名 | 类型 | 默认值 | 解释 | | --- | --- | --- | --- | | basePackage | String | com.jools.rpc.springboot.service.localmock | 扫描该包下的所有被@FailBackService 的类 | 3. 定义 `@FailBackReference` 注解,指定服务名称 + 绑定伪装服务名称 | 字段名 | 类型 | 默认值 | 解释 | | --- | --- | --- | --- | | bindServiceName | String | 空子串 (默认以查询到的所有伪装服务的首个服务) | 绑定的伪装服务类名 | #### 注解驱动逻辑 1. 扫描带有 `@FailBackService` 注解的类,并根据接口全名和注解中的 `mockServiceName` 字段将其注册到 `LocalServiceMockRegistry`。 2. 对于带有 `@FailBackReference` 注解的字段,根据 `serviceName` 或接口名(如果 `serviceName` 为空)以及 `@FailBackService` 注解中的 `mockServiceName` 字段(如果存在),在 `LocalServiceMockRegistry` 中建立关联。 3. 当重试机制触发时,从 `LocalServiceMockRegistry` 中查询对应的模拟服务类。如果有唯一绑定,则返回该绑定;否则返回第一个找到的模拟服务类。 #### 实现步骤 1. 定义所需注解。 2. 在消费者端创建多个本地模拟服务,并用 `@FailBackService` 标记。 3. 消费者启动配置中添加 `@LocalMockScanPackage` 来指定扫描路径。 4. 需要调用的服务字段上加上 `@FailBackReference` 注解。 5. 在 `JRpc-SpringBoot-Starter` 中开发 `LocalMockServiceBootstrap`,实现 `ImportBeanDefinitionRegistrar` 接口负责扫描并注册标记了 `@FailBackService` 的服务及其实现类至 `LocalServiceMockRegistry`。 6. 同样在 `JRpc-SpringBoot-Starter` 中实现 `LocalMockReferBootstrap`实现 `BeanPostProcessor` 后置处理器,用于处理 `@FailBackReference` 注解下的字段与特定或默认本地模拟服务之间的关联。 7. 在 `rpc-core` 模块内新增 `ErrorFailBackHandler` 类,用来处理失败回退消息,通过查询 `LocalServiceMockRegistry` 获取并反射调用合适的模拟服务。 8. 测试:确保当未启用 FailBack 功能但使用了 `@FailBackReference` 时能够正确抛出异常。 9. 测试:验证启用了 FailBack 时是否能自动调用首个注册的本地 Mock Service。 10. 测试:检查基于 `@FailBackReference` 中 `bindMockServiceName` 属性设置能否准确绑定唯一的本地模拟服务。 ### (扩展) JRPC - Spring - Boot - Starter 基于注解 @RpcReference (mock字段)支持本地 Mock 测试 #### 需求 1. 通过 `RpcConsumerBootstrap` 启动器扫描被 `@JRpcReference` 注解标记的 Bean,获取注解相关字段。 2. 其中 `@JRpcReference` 注解内含有 `mock()` 字段可配置 3. 设计:如果 `mock()` 字段,被标记为 true; 通过 `ServiceProxyFactory` 直接返回相应的 Mock 实例 #### 实现 1. 修改 `RpcConsumerBootstrap` 支持 Consumer 端基于 `@JRpcReferenec` 注解的 mock 字段,直接注入 Mock 示例,后续调用实现接口Mock 2. 测试:设置 `@JRpcReferenec` 注解 `enableMock` 字段为 true ### 扩展 - JRPC-Spring-Boot-Starter 支持基于 Spring Boot SDK 方式自定义全局配置 参考文档:鱼皮 API 项目- 实现一个 SpringBoot Starter SDK 实现: 1. 简化后的步骤如下: 2. 在 `jrpc-spring-boot-starter` 项目模块中创建`StarterRpcConfig`类,映射`RpcConfig`。 3. 为`StarterRpcConfig`添加注解:`@Configuration`标记其为配置类,`@ComponentScan`用于组件扫描与自动注册Bean,`@ConfigurationProperties`定义`.yml`配置文件的前缀。 4. 在`/resources/META-INF/spring.factories`中添加`EnableAutoConfiguration`键及其值为`StarterRpcConfig`全类名。 5. 将rpc-core模块发布到本地Maven仓库。 6. 将starter模块发布到本地Maven仓库。 7. 刷新Maven后,在Consumer项目的`application.yml`里输入指定前缀时应出现提示。 8. 测试基于SDK注册`StarterRpcConfig`。 #### 问题 如何确定是通过哪种方式配置`RpcConfig` + 目前支持两种配置方法: 1. 通过SDK配置`RpcConfig` 2. 直接从`resources`目录下的`.properties`或`.yml`文件加载 #### 解决方案 1. 在`@EnableRpc`注解中增加`useStarterSDK`布尔字段,当设置为`true`时启用SDK配置。 2. 修改`BootInitBootstrap`启动器,实现`EnvironmentAware`接口,并重写`setEnvironment()`方法。 3. 在`setEnvironment`方法中注入`Environment`对象。 4. 使用`Binder`工具读取配置文件内以 "jrpc" 为前缀的属性,并绑定至`StarterRpcConfig`类。 5. 测试 - 更新Consumer端启动类使用`@EnableJRpc(needServer = false, useStarterSDK = true)`。 6. 测试 - 通过SDK配置注入`RpcConfig`。 7. 确认 - 注入SDK配置后RPC框架正常运行。 修改后完整的 `@EnableJRpc` ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import({ RpcConsumerBootstrap.class, RpcProviderBootstrap.class, RpcInitBootstrap.class, LocalMockReferBootstrap.class, LocalMockServiceBootstrap.class }) public @interface EnableJRpc { /** * 需要启动 Server * * @return */ boolean needServer() default true; /** * 是否基于 SDK 加载配置 * * @return */ boolean useStarterSDK() default false; } ``` ## 可选扩展点 - from 鱼皮建议 扩展点实现笔记(2025.3.2 更新):[所有扩展点实现把笔记](https://reurl.cc/yDpOal) 完整可选扩展点请参考鱼皮笔记原文 ### (已实现) 项目支持读取 `.yml /.yaml` 等更多类型的配置文件 + rpc-core模块项目支持解析 yml / yaml 配置文件 + jrpc-springboot-starter模块也支持解析 yml / yaml配置文件 + jrpc-springboot-starter模块支持基于 Spring Boot SDK 方式配置 ### (已实现)服务注册信息缓存优化,支持区分服务 key, 而不是公用同一个缓存对象 `rpc-core`模块内的 `RegistryServiceCache`支持服务注册信息缓存 + 基于 `serviceKey`作为 key 区分不同服务 + 以 `List` 作为集合存储所有服务发现得到的服务节点信息 <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/i8z02s9qfGXJCus4.webp" alt="" width="100%" /> ### (已实现)实现拦截器机制,服务调用前和服务调研后可以执行额外操作 参考思路: > 可参考 Spring MVC 或 Servlet 的 Filter 机制,用责任链模式实现。在服务提供者处理前后、服务消费者调用前后加拦截器,用于日志校验、安全校验等。也可用 SPI 机制支持用户自定义拦截器。 > > + 定义 Interceptor 接口 > + 实现接口类 > + 在自定义 SPI 资源目录下配置 `key`+ `Interceptor 实现类全类名` > + RpcConfig 添加配置项 **个人思路梳理:** > > 1. 设计一个 Interceptor 接口 `RpcHandlerInterceptor`,参考 Spring 生态的拦截器 > 2. 设计一个拒绝访问无效服务 Interceptor - `InvalidServiceNameInterceptor` > 3. 一个拦截非法用词取名的 Interceptor - `IllegalIpInterceptor` > 4. 实现 `HandlerInterceptor`接口 > 5. `InvalidServiceNameInterceptor`:如果申请失效服务名返回 false > 6. `IllegalParamInterceptor`: 携带非法实参值则拒绝 > 7. 在 TcpServerHandler Decode 得到 ProtocolMessage 之后可以通过 body 部分获取到携带的 `RpcRequest` 部分,并且从中获取到 > > 1. serviceName: 请求服务名称 > 2. params: 请求实参列表 > > 8. 新增 `InterceptorKey` 枚举类: 通过 key 控制获取注册的拦截器 > 9. 新增 `RpcConfig`配置项 `enableInterceptor`,可以配置关闭拦截器或者开启 > > 1. 全部开启 `true` > 2. 全部禁止 `false` > > 10. 创建 `InterceptorFactory` 简单工厂类,支持基于 RpcConfig 配置控制行为 > 具体步骤参考上述给予的笔记链接 ### (写思路未实现) 支持消费方指定某个服务级别的负载均衡器、重试策略、容错机制 参考思路: > 目前只能通过全局配置改变对所有服务的负载均衡调用规则。实现的话可能需要修改 ServiceProxy 类,让它支持传参,根据 **消费端的配置来动态创建 ServiceProxy**。 > > + 目前在 Consumer 启动器内可以基于注解为单个服务指定 > <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/gUdWmzFx6JvVimoi.webp" alt="" width="100%" /> **个人思路梳理:** > 1. 基于传入的 RpcRequest 信息 > 2. 在响应后如果有异常,基于存储的本次 RpcRequest 的负载均衡、容错机制处理 > 3. 如果没有设置,则使用配置文件设置兜底 ### (已实现) 服务服务消费方支持设定超时时间 参考思路: > 可以通过修改 TCP 客户端请求相关的代码实现。 > > 目前为固定值 > > Consumer 调用 Sender 等待 10s 后超时 > <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/RnvhCtrSXBbxD1qN.webp" alt="" width="100%" /> **个人思路梳理:** > 1. `RpcConfig` 内新增配置项 `rpcRespWaitDuration` 用于记录等待下一个响应的最长等待时间 > 2. 支持基于 SpringBoot Starter SDK 配置或者直接修改 RpcConfig 的默认值 > 3. RpcConstant 内新增 RpcConfig 的默认值常量 `DEFAULT_RESP_WAIT_DURATION` 设置为 10s > 4. TcpSender 和 HttpSender 内启动时通过配置加载好 具体步骤参考上述给予的笔记链接 ### (已实现) - 处理 Bean 注入问题:目前本地服务注册表存储的是 class,然后通过反射创建实例 但是如果 Bean 包含有参构造函数,或者给属性注入了其他示例,这种方式就行不通了。 **参考思路:** > 本地注册时要放入实现类的对象实例,而不是 `class`类型 具体步骤参考上述给予的笔记链接 ### (写思路未实现) - 服务注册信息缓存增加过期时间,定时刷新缓存 参考思路: > 基于 `Caffeine`构建缓存 [Caffeine 快速入门](https://www.baeldung.com/java-caching-caffeine)
RPC 的实现原理
RPC(Remote Procedure Call,远程过程调用)是一种允许程序调用另一台计算机上的子程序或方法的技术。通过 RPC,客户端可以像调用本地方法一样调用远程服务器上的方法,而不需要了解底层网络通信的细节。以下是 RPC 的实现原理: 1. 基本概念 客户端:发起远程调用的一方。 服务端:提供远程服务的一方。 接口:定义了客户端和服务端之间交互的方法和数据结构。 传输协议:用于在网络上传输数据的协议,如 HTTP、TCP 等。 序列化/反序列化:将数据转换为字节流以便在网络上传输,以及将字节流还原为数据。 2. 实现步骤 2.1 定义接口 首先,需要定义一个接口,该接口描述了客户端可以调用的服务端方法及其参数和返回值类型。例如: ```java public interface HelloService { String sayHello(String name); } ``` 2.2 服务端实现 服务端需要实现这个接口,并提供一个方法来处理客户端的请求。例如: ```java public class HelloServiceImpl implements HelloService { @Override public String sayHello(String name) { return "Hello, " + name; } } ``` 2.3 服务注册与发现 服务端需要将实现类注册到一个服务注册中心,客户端通过服务注册中心发现并连接到服务端。常见的服务注册中心有 ZooKeeper、Eureka 等。 2.4 客户端代理 客户端需要一个代理对象来调用远程方法。代理对象负责将方法调用转换为网络请求,并将响应结果返回给客户端。例如: ```java public class HelloServiceProxy implements HelloService { private String serviceAddress; public HelloServiceProxy(String serviceAddress) { this.serviceAddress = serviceAddress; } @Override public String sayHello(String name) { // 构建请求 String request = "sayHello(" + name + ")"; // 发送请求 String response = sendRequest(serviceAddress, request); // 解析响应 return parseResponse(response); } private String sendRequest(String address, String request) { // 使用 HTTP 或其他协议发送请求 // 这里简化为直接返回示例响应 return "Hello, " + request.substring(9, request.length() - 1); } private String parseResponse(String response) { // 解析响应 return response; } } ``` 2.5 序列化与反序列化 为了在网络上传输数据,需要将数据进行序列化(转换为字节流),并在接收端进行反序列化(还原为数据)。常见的序列化框架有 JSON、protobuf、Hessian 等。 2.6 传输协议 选择合适的传输协议,如 HTTP、TCP 等,用于在网络上传输请求和响应。 . 工作流程 客户端调用:客户端调用代理对象的方法。 构建请求:代理对象将方法调用转换为网络请求。 序列化:将请求数据序列化为字节流。 发送请求:通过传输协议将请求发送到服务端。 反序列化:服务端接收到请求后,将字节流反序列化为请求数据。 方法调用:服务端调用实际的方法处理请求。 构建响应:服务端将方法的返回值构建为响应数据。 序列化:将响应数据序列化为字节流。 发送响应:通过传输协议将响应发送回客户端。 反序列化:客户端接收到响应后,将字节流反序列化为响应数据。 返回结果:代理对象将响应结果返回给客户端。 4. 常见的 RPC 框架 gRPC:基于 HTTP/2 和 protobuf 的高性能 RPC 框架。 Dubbo:阿里巴巴开源的高性能 Java RPC 框架。 Thrift:Facebook 开源的跨语言 RPC 框架。 Spring Cloud:基于 Spring Boot 的微服务框架,支持多种 RPC 调用方式。
