0%

项目介绍

功能架构

image-20241125122839960

技术选型

image-20241125122858635

MD5

1.修改数据库中明文密码,改为MD5加密后的密文

2.修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对

Swagger

使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。

员工管理

面试问题

  • nginx反向代理好处
1
2
3
提高访问速度
进行负载均衡(所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器)
保证后端服务安全
  • DTO
1
当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据
  • JWT

image-20241125192608560

  • 日期处理

在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理

  • 公共字段自动填充

使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。

实现步骤:

1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

3). 在 Mapper 的方法上加入 AutoFill 注解

技术点:枚举、注解、AOP、反射

  • 新增菜品,包括图片文件

使用第三方的存储服务,选用了阿里云的OSS服务进行文件存储。

  • 删除菜品的性能优化

在DishServiceImpl中,删除菜品是一条一条传送执行的,大大降低了执行效率,为了提高性能,进行修改,使用动态sql执行删除操作

  • redis

基于键值对的非关系数据库。

常用数据类型

1
2
3
4
5
6
7
8
9
字符串 string

哈希 hash

列表 list

集合 set

有序集合 sorted set / zset

image-20241126104927160

  • 微信登录

业务规则:

基于微信登录实现小程序的登录功能

如果是新用户需要自动完成注册

要完成微信登录的话,最终就要获得微信用户的openid。在小程序端获取授权码后,向后端服务发送请求,并携带授权码,这样后端服务在收到授权码后,就可以去请求微信接口服务。最终,后端向小程序返回openid和token等数据。

  • 应用层

SpringBoot: 快速构建Spring项目, 采用 “约定优于配置” 的思想, 简化Spring项目的配置开发。

SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。

Spring Task: 由Spring提供的定时任务框架。

httpclient: 主要实现了对http请求的发送。

Spring Cache: 由Spring提供的数据缓存框架

JWT: 用于对应用程序上的用户进行身份验证的标记。

阿里云OSS: 对象存储服务,在项目中主要存储文件,如图片等。

Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。

POI: 封装了对Excel表格的常用操作。

WebSocket: 一种通信网络协议,使客户端和服务器之间的数据交换更加简单,用于项目的来单、催单功能实现。

  • 数据层

MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。

Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存。

Mybatis: 本项目持久层将会使用Mybatis开发。

  • Swagger
  1. 使得前后端分离开发更加方便,有利于团队协作
  2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
  3. 功能测试
  • 解析出登录员工id后,如何传递给Service的save方法

通过ThreadLocal进行传递。

  • 分页

使用 mybatis 的分页插件 PageHelper 来简化分页代码的开发。

1
2
3
4
5
6
7
8
9
PageHelper是MyBatis的一个插件,内部实现了一个PageInterceptor拦截器。Mybatis会加载这个拦截器到拦截器链中。在我们使用过程中先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用PageInterceptor这个分页拦截器拦截,从ThreadLocal中拿到分页的信息,如果有分页信息拼装分页SQL(limit语句等)进行分页查询,最后再把ThreadLocal中的东西清除掉。

设置分页参数:在执行查询之前,首先通过 PageHelper.startPage(int pageNum, int pageSize) 方法设置分页的参数,调用该方法时,通过 ThreadLocal 存储分页信息。
拦截查询语句:PageHelper 利用 MyBatis 提供的插件 API(Interceptor 接口)来拦截原始的查询语句。MyBatis 执行任何 SQL 语句前,都会先通过其插件体系中的拦截器链,PageHelper 正是在这个环节介入的。
修改原始 SQL 语句:在拦截原始查询语句后,PageHelper 会根据分页参数动态地重写或添加 SQL 语句,使其成为一个分页查询。
执行分页查询:修改后的 SQL 语句被执行,返回当前页的数据。
查询总记录数(可选):如果需要获取总记录数,PageHelper 会自动执行一个派生的查询,以计算原始查询(不包含分页参数)的总记录数。这通常通过移除原始 SQL 的排序(ORDER BY)和分页(LIMIT、OFFSET 等)条件,加上 COUNT(*) 的包装来实现。
返回分页信息:查询结果被封装在 PageInfo 对象中(或其他形式的分页结果对象),这个对象除了包含当前页的数据列表外,还提供了总记录数、总页数、当前页码等分页相关的信息,方便在应用程序中使用。

  • 前端小程序的微信登录流程
1
2
3
4
5
6
7
8
9
10
微信登录的核心是通过微信小程序提供的临时凭证code换取永久凭证openid的过程

首先微信小程序会向微信官方申请一个临时登录code
然后,小程序带着code向后台服务发送请求
后台接收到code后,会调用微信官方接口验证code是否合法,如果合法,官方会返回一个openid;这个openid就是此用户在我们系统中的唯一标识,同时也代表用户身份合法
后台服务接收到来着微信的openid之后,会去数据库查询一下是否存在此账户;如果存在,代表这是一个老用户,如果不存在,则代表这是一个新用户首次使用我们的系统,我们需要将其信息保存到用户表中
登录成功之后,需要生成一个标识用户身份的token,返回给前端,前端会将token保存起来
用户后面访问系统的时候,需要携带着这个token,而我们后端需要编写一个拦截器,用于拦截请求,校验token
校验通过,则放行请求,正常访问;校验失败,则禁止通行,返回提示

  • redis应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们项目中有两处地方用到了Redis,分别是:店铺营业状态标识和小程序端的套餐、菜品列表数据

店铺营业状态标识,仅仅需要在redis中保存一个0|1值即可。这里之所以选择redis,有两个原因

而没有采用数据库来存储,就是因为这个字段太简单了,没有必要在数据库中新建一张表

这个状态访问比较频繁,放在redis中,提高了查询速度的同时,可以减轻数据库的访问压力

小程序端的套餐、菜品列表数据,由于小程序端以后的访问量比较大,所以采用Redis提高访问速度

具体的操作步骤就是:在查询列表的时候,先判断Redis缓存中是否有数据,如果有,直接返回给前端

如果没有,再去查询数据库,并将查询结果保存到redis中的同时,再返回给前端

为了保证Redis和数据库中数据的实时一致性,在对数据库相关数据进行增删改操作时,需要同时清理Redis中数据

  • SpringCache在项目中的应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SpringCache是Spring提供的一个缓存框架,它可以通过简单的注解实现缓存的操作,我们常用的注解有下面几个:

@EnableCaching: 开启基于注解的缓存

@CachePut: 一般用在查询方法上,表示将方法的返回值放到缓存中

@Cacheable: 一般用在查询方法上,表示在方法执行前先查看缓存中是否有数据,如果有直接返回;如果没有,再调用方法体查询数据并将返回结果放到缓存中;他有两个关键属性:

value: 缓存的名称,每个缓存名称下面可以有多个key

key: 缓存的key,支持Spring的表达式语言SPEL语法

@CacheEvict: 一般用在增删改方法上 ,用于清理指定缓存,可以根据key清理,也可以清理整个value下的缓存

SpringCache还有一个有点,就是可以随意切换底层的缓存软件,比如:Redis、内存等等

本项目中菜品和套餐列表的缓存用到了SpringCache
  • SpringTask在项目中的应用
1
2
3
4
5
SpringTask是Spring框架提供的一种任务调度工具,用来按照定义的时间格式执行某段代码。
在我们的项目中,超时订单的状态改变用到了SpringTask,比如:

每隔1分钟检查是否有超过15分钟未支付的订单,如果有就将订单取消
每天凌晨1点检查前一天是否有派送中的订单,如果有将订单状态改成已完成

cron表达式其实就是一个字符串,通过cron表达式可以定义任务的触发时间

  • WebSocket对比HTTP
1
2
3
4
5
6
7
8
HTTP的通信是单向的,要先请求后响应,类似于对讲机

WebSocket的通信双向的、实时的,客户端和服务端可以同时发消息,类似于手机通话

我们在项目中大部分场景下都是使用HTTP协议,只有在高实时场景下,建议使用WebSocket

项目在向商家提醒接单时,用户催单发送提醒时使用了webSocket

  • 核心功能

菜品新增:对菜品表和口味表进行新增操作

首先将前端传过来的菜品信息保存到菜品表并主键返回,然后遍历前端传过来的口味集合,
为每个口味设置刚才返回来的主键并保存到口味表
菜品修改:对菜品表进行更新,对菜品详情表进行增删操作

首先根据前端传过来的菜品信息对菜品表进行修改
然后根据菜品id删除对应的口味列表集合
最后再把前端传过来的口味集合重新加入到口味表中
菜品删除:对菜品表和口味表进行删除操作

遍历前端传过来的菜品id集合得到每个菜品的信息
如果当前菜品是启售状态或者被套餐关联那么就不能被删除
否则就可以通过菜品id对菜品表和口味表中的数据进行删除
套餐新增:对套餐表和套餐菜品关系表进行新增操作

首先将前端传过来的套餐基本信息保存到套餐表中,并返回主键的id
然后为前端传过来的套餐菜品设置套餐id
最后将套餐包含的菜品添加到套餐菜品关系表中
套餐修改:对套餐表进行修改,在对套餐菜品关系表进行增删操作

首先根据前端传过来的套餐基本信息更新到套餐表中
然后根据套餐的id删除所有套餐菜品关系表中包含的菜品信息
最后遍历前端传过来的菜品的列表,设置好套餐的id后重新保存到套餐菜品关系表中
套餐删除:对套餐表和套餐菜品关系表进行删除操作

首先遍历前端传过来套餐id的集合得到每一个套餐的信息
然后根据id查询套餐,判断套餐的状态是否为启售状态,如果是启售状态,则不能删除
如果是在禁售状态,就可以通过套餐的id进行套餐菜品关系表的删除操作
分类删除

分类删除的核心逻辑就是根据前端传过来的分类id去分类表进行一个删除操作
但是要对这个分类里面是否有菜品和套餐做一个判断,拿着这个id去菜品表和套餐表做一个统计查询
如果查出来数量大于0,就不能删除,如果为0,直接删除
添加购物车:将用户选择的商品基本数据信息添加到数据库表中进行保存

利用到的数据库表(本次项目):购物车表,菜品表、套餐表,保存的信息就是从表中查到的

首先根据id查询购物车中是否有相同商品

有:则不用添加,只修改查询到的商品number属性+1并重新赋值即可,执行mapper更新。

无:则判断是菜品还是套餐,查询对应商品的数据库得到基本信息,补全购物车需要的参数执行保存。

  • websocket

img

  • threadlocal

img

项目困难

image-20241126203501259

image-20241126203511824

image-20241126203519631

image-20241126203526823

Redis

使用场景

缓存

缓存穿透

  • 定义:查询一个不存在数据,每次查询都请求数据库
  • 解决方案一:缓存空数据。

优点:简单

缺点:消耗内存,可能发生不一致问题

  • 方案二:布隆过滤器

优点:内存占用少,没有多余key

缺点:实现复杂,存在误判

缓存击穿

  • 定义:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能会瞬间把DB压垮。
  • 解决方案一:互斥锁

image-20241113173234318

可以保证数据强一致性,但是性能较差

  • 方案二:逻辑过期

image-20241113173410421

性能较高,不能保证数据绝对一致。

缓存雪崩

  • 定义:同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库

image-20241113173610988

双写一致性

image-20241113174048777

一致性高:

image-20241113174219179

读写锁效率更高一点。

一致性要求不高:

image-20241113174336123

允许延时一致。

image-20241113174506462

持久化

  • RDB

image-20241113175006561

  • AOF

image-20241113180658834

image-20241113180919940

数据过期策略

  • 惰性删除

image-20241113181025752

  • 定时删除

image-20241113181041510

Redis的过期策略是二者结合

数据淘汰策略

image-20241113181313121

image-20241113181334256

分布式锁

image-20241113182238011

image-20241113182418127

image-20241206103952684

redisson实现的分布式锁-可重入。

其他

Redis集群

主从复制

image-20241113183241827

全量同步:

image-20241113183404603

增量同步:

image-20241113183902910

哨兵

image-20241113184040244

image-20241113184200081

分片集群

image-20241113184906722

image-20241113185253280

其他

image-20241113185344002

MYSQL

优化

定位慢查询

image-20241113193527339

image-20241113193610868

如何分析

image-20241113194008431

image-20241113194108483

image-20241113194144778

image-20241113194235780

索引

image-20241113194829230

image-20241113195204510

image-20241113200121864

覆盖索引

image-20241113200156128

超大分页

image-20241113200333291

image-20241113200355259

索引创建原则

image-20241113200928741

索引失效

image-20241113201501827

image-20241113201640636

image-20241113201654548

image-20241113201714881

image-20241113201757677

经验

image-20241113202207342

  • 表的设计优化

image-20241113202252800

  • SQL语句优化

image-20241113202318812

  • 主从复制、读写分离

image-20241113202522118

事务

特性

image-20241113202950902

并发事务

image-20241114134351047

解决方法

image-20241114134617042

log

image-20241114134947783

image-20241114135054074

image-20241114135133135

MVCC

image-20241114135315206

image-20241206104426924

主从同步原理

image-20241114135757985

image-20241206111130223

分库分表

image-20241206160019201

框架

Spring

bean

spring中的单例bean不是线程安全的。

image-20241114140622142

AOP

  • 定义

image-20241114141048318

image-20241114141748278

事务失效场景

image-20241114142404580

image-20241114142553578

image-20241114142716007

image-20241114142744275

bean的生命周期

image-20241206161638418

循环引用

image-20241114150316128

SpringMVC执行流程

image-20241114151239697

image-20241114151348773

Springboot自动配置原理

自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置。

在 Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可以实现自动装配。

image-20241114152123776

常见注解

image-20241114152423611

MyBatis

执行流程

image-20241114153450089

延迟加载

image-20241114154312368

image-20241114154940442

一、二级缓存

image-20241114161250932

集合

List

数组

ArrayList源码

image-20241115125549338

ArrayList底层原理

image-20241115131352180

数组和List之间的转换

image-20241115131634843

image-20241115132854166

image-20241115133642645

HashMap

  • 散列表

image-20241115134428936

image-20241115134435017

实现原理

image-20241115135057467

PUT

image-20241115140059260

扩容机制

image-20241115142626905

image-20241115142849019

寻址算法

image-20241115143951187

image-20241115144012925

多线程

线程基础

进程和线程

image-20241115145010258

创建线程方式

image-20241115145725636

image-20241115145832750

image-20241115145947677

状态

image-20241115150521157

顺序执行

image-20241115150736530

image-20241115151017584

wait和sleep

image-20241115154722556

如何终止线程

image-20241115155204426

线程安全

synchronized底层原理

image-20241116085705439

底层实现是Monitor

image-20241116090021112

image-20241116091004674

一旦锁发生竞争,都会升级为重量级锁。

JMM

image-20241116091838454

CAS

image-20241116092430536

volatile

  • 保证线程间可见性

image-20241116093135680

  • 禁止指令重排序

image-20241116093215311

image-20241116094023739

AQS

image-20241116094116845

image-20241116094652430

image-20241205162529721

AQS核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是⽤CLH队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。

ReentrantLock

image-20241116095154783

synchronized和Lock

image-20241116100046920

死锁

image-20241116100927080

ConcurrentHashMap

image-20241116101451404

并发问题

image-20241116101745631

线程池

线程池核心参数

image-20241117190318040

image-20241117193538266

常见阻塞队列

image-20241117194306438

如何缺点核心线程数

image-20241117194847129

线程池种类

image-20241117195111040

使用场景

image-20241206162046309

JVM

JVM组成

程序计数器

image-20241117200427200

java堆

image-20241117200756694

虚拟机栈

image-20241118144631044

image-20241118144647293

方法区

image-20241118144710602

直接内存

image-20241118144737789

类装载过程

image-20241118144351326

垃圾回收

何时回收

image-20241118145115656

引用计数法可能会循环引用,造成泄露,所以现在经常用可达性分析。

垃圾回收算法

image-20241118145357265

分代回收

image-20241118145731845

引用

image-20241118174802208

内存泄露排查思路

image-20241206170711343

CPU排查思路

image-20241206170735783

企业场景

设计模式

工厂设计模式

image-20241118180800590

策略模式

image-20241118182117601

责任链模式

image-20241118182228159

技术场景

单点登录

image-20241118182524832

image-20241118182657373

权限认证

image-20241118183410591

image-20241118183419380

上传数据安全性

image-20241118183758848

棘手问题

采集日志

怎么快速定位系统瓶颈

image-20241118192428648

补充1

基础

  • 重载和重写

image-20241205221942136

  • 装箱和拆箱

image-20241205222002926

  • == equals

image-20241205222039872

  • hashCode equals

image-20241205222159699

  • Java 中的异常处理

image-20241205222228335

image-20241205222241278

image-20241205222250416

  • Java 序列化中如果有些字段不想进⾏序列化,怎么办?

对于不想进⾏序列化的变量,使⽤ transient 关键字修饰。

transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。

  • Java 中 IO 流

    image-20241205222418104

image-20241205222356524

image-20241207184109427

NIO:基于同步非阻塞 I/O 模型。尽管 NIO 支持非阻塞模式,但所有的 I/O 操作最终都是同步完成的。这意味着当一个 I/O 操作被发起时,它不会立即返回结果,而是需要应用程序通过轮询或选择器来检查操作是否完成。

AIO:真正的异步 I/O 模型。在 AIO 中,所有的 I/O 操作都是异步完成的。一旦发起一个 I/O 请求,该请求将立即返回,允许程序继续执行其他任务。操作系统会在 I/O 操作完成后通知应用程序。

  • 集合框架底层数据结构总结

image-20241205222540249

  • 进程和线程

image-20241205222628899

image-20241205222706738

  • 死锁

image-20241205222735487

  • 为什么我们调⽤ start() ⽅法时会执⾏run() ⽅法,为什么我们不能直接调⽤run() ⽅法

image-20241205222818227

  • 双重校验锁实现对象单例(线程安全)

image-20241205223056871

  • java对象创建过程

image-20241205223644141

image-20241205223653879

  • 对象的访问定位有哪两种⽅式?

image-20241205223725944

  • 常⻅的垃圾回收器有那些?

image-20241205223807138

Serial收集器

Serial(串⾏)收集器收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它的 单线程 的意义不仅仅意味着它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( “Stop The World”),直到它收集结束。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控参数、收集算法、回收策略等等)和Serial收集器完全⼀样。

Parallel Scavenge收集器

image-20241205224253533

image-20241205224318164

image-20241205224329444

  • 双亲委派模型

image-20241205224433027

image-20241205224439218

  • image-20241205224622648
  • ⼀条SQL语句在MySQL中如何执⾏的

image-20241205225225043

image-20241205225340608

  • 索引实现为什么是B树而不是红黑树

image-20241206104728029

  • 受检异常和非受检异常

image-20241206105050884

  • b+而不是b

image-20241206105931070

  • 守护线程

image-20241206110838097

  • mysql索引类型

image-20241206111247698

  • springboot约定优先于配置

image-20241206112124441

image-20241206112132390

  • Spring中事务的传播行为

image-20241206114452006

image-20241206114500379

  • Dubbo如何动态感知服务下线

image-20241206114748010

image-20241206114801949

当 Dubbo 服务提供方出现故障导致 Zookeeper 剔除了这个服务的地址,

image-20241206114823628

  • Mybatis 中#{}和${}的区别是什么?

image-20241206115154486

  • 数据库连接池

数据库连接池是一种池化技术,池化技术的核心思想是实现资源的复用,避免资源重复创建销毁的开销。

而在数据库的应用场景里面,应用程序每次向数据库发起 CRUD 操作的时候,都需要创建连接在数据库访问量较大的情况下,频繁的创建连接会带来较大的性能开销。

(如图)而连接池的核心思想,就是应用程序在启动的时候提前初始化一部分连接保存到连接池里面,当应用需要使用连接的时候,直接从连接池获取一个已经建立好的链接。连接池的设计,避免了每次连接的建立和释放带来的开销。

image-20241206115308060

  • new String(“abc”)到底创建了几个对象?

image-20241206115527338

  • String、StringBuffer、StringBuilder 区别

image-20241206143347813

  • JVM年龄代为什么是15次

一个对象的GC年龄是储存在对象头里的,而对象头里有4位储存GC年龄,最大值为15。

  • 深拷贝和浅拷贝

image-20241206145323610

  • Spring IOC 和 DI

image-20241206145508721

image-20241206145518569

image-20241206145529526

  • finally块一定执行吗

image-20241206150428445

  • Integer 和 int 的区别

image-20241206151407495

  • 零拷贝
  • 在2G文件中,找出高频top100

image-20241206152107404

image-20241206152114028

  • 表数据量大的时候,影响查询效率的主要原因

image-20241206152310633

  • 两个 Integer 对象比较大小,为什么 100 等于 100,1000 不等于 1000

因为Intefer的valueOf方法,判断时如果目标值在-128-127则会直接从cache取值。

  • MQ(消息中间件)

MQ 全称是 Message Queue,直译过来叫做消息队列,主要是作为分布式应用之间实现异步通信的方式。

主要由三个部分组成,分别是生产者、消息服务端和消费者

image-20241206153425201

流量消峰

image-20241206153442652

应用解耦

image-20241206153455894

异步处理

image-20241206153529960

  • 给你 ab,ac,abc 字段,你是如何加索引 的

image-20241206154635372

  • 补充2

  • CPU飚高系统反应慢怎么排查

image-20241205162936984

  • Dubbo的服务请求失败怎么处理

image-20241205163458787

  • 如何实现Redis和Mysql的一致性

当应用程序需要去读取某个数据的时候,首先会先尝试去 Redis 里面加载,如果命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据缓存到 Redis里面。

在这种情况下,能够选择的方法只有几种。

  1. 先更新数据库,再更新缓存

  2. 先删除缓存,再更新数据库

如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和 Redis 中的数据不一致。

如果是先删除缓存,再更新数据库,理想情况是应用下次访问 Redis 的时候,发现 Redis里面的数据是空的,就从数据库加载保存到 Redis 里面,那么数据是一致的。但是在极端情况下,由于删除 Redis 和更新数据库这两个操作并不是原子的,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题。

所以,如果需要在极端情况下仍然保证 Redis 和 Mysql 的数据一致性,就只能采用最终一致性方案。(如图)比如基于 RocketMQ 的可靠性消息通信,来实现最终一致性。

image-20241205165736374

因为这里是基于最终一致性来实现的,如果业务场景不能接受数据的短期不一致性,那就不能使用这个方案来做。

  • Dubbo核心功能

image-20241205170107231

image-20241205170117694

  • Dubbo工作原理

image-20241205170215391

  • Mysql优化

image-20241205171151724

image-20241205171234879

image-20241205171242190

image-20241205171253143

image-20241205171303324

RPC核心代码

基础

编写基于Vert.x实现的web服务器VertxHttpServer,能够监听指定端口并处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.yupi.yurpc.server;

import io.vertx.core.Vertx;

public class VertxHttpServer implements HttpServer {

public void doStart(int port) {
// 创建 Vert.x 实例
Vertx vertx = Vertx.vertx();

// 创建 HTTP 服务器
io.vertx.core.http.HttpServer server = vertx.createHttpServer();

// 监听端口并处理请求
server.requestHandler(request -> {
// 处理 HTTP 请求
System.out.println("Received request: " + request.method() + " " + request.uri());

// 发送 HTTP 响应
request.response()
.putHeader("content-type", "text/plain")
.end("Hello from Vert.x HTTP server!");
});

// 启动 HTTP 服务器并监听指定端口
server.listen(port, result -> {
if (result.succeeded()) {
System.out.println("Server is now listening on port " + port);
} else {
System.err.println("Failed to start server: " + result.cause());
}
});
}
}

本地服务注册中心

image-20241213144820474

image-20241213144835523

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.yupi.yurpc.registry;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* 本地注册中心
*/
public class LocalRegistry {

/**
* 注册信息存储
*/
private static final Map<String, Class<?>> map = new ConcurrentHashMap<>();

/**
* 注册服务
*
* @param serviceName
* @param implClass
*/
public static void register(String serviceName, Class<?> implClass) {
map.put(serviceName, implClass);
}

/**
* 获取服务
*
* @param serviceName
* @return
*/
public static Class<?> get(String serviceName) {
return map.get(serviceName);
}

/**
* 删除服务
*
* @param serviceName
*/
public static void remove(String serviceName) {
map.remove(serviceName);
}
}

请求处理器

image-20241213144959372

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.yupi.yurpc.server;

import com.yupi.yurpc.model.RpcRequest;
import com.yupi.yurpc.model.RpcResponse;
import com.yupi.yurpc.registry.LocalRegistry;
import com.yupi.yurpc.serializer.JdkSerializer;
import com.yupi.yurpc.serializer.Serializer;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;

import java.io.IOException;
import java.lang.reflect.Method;

/**
* HTTP 请求处理
*/
public class HttpServerHandler implements Handler<HttpServerRequest> {

@Override
public void handle(HttpServerRequest request) {
// 指定序列化器
final Serializer serializer = new JdkSerializer();

// 记录日志
System.out.println("Received request: " + request.method() + " " + request.uri());

// 异步处理 HTTP 请求
request.bodyHandler(body -> {
byte[] bytes = body.getBytes();
RpcRequest rpcRequest = null;
try {
rpcRequest = serializer.deserialize(bytes, RpcRequest.class);
} catch (Exception e) {
e.printStackTrace();
}

// 构造响应结果对象
RpcResponse rpcResponse = new RpcResponse();
// 如果请求为 null,直接返回
if (rpcRequest == null) {
rpcResponse.setMessage("rpcRequest is null");
doResponse(request, rpcResponse, serializer);
return;
}

try {
// 获取要调用的服务实现类,通过反射调用
Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName());
Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs());
// 封装返回结果
rpcResponse.setData(result);
rpcResponse.setDataType(method.getReturnType());
rpcResponse.setMessage("ok");
} catch (Exception e) {
e.printStackTrace();
rpcResponse.setMessage(e.getMessage());
rpcResponse.setException(e);
}
// 响应
doResponse(request, rpcResponse, serializer);
});
}

/**
* 响应
*
* @param request
* @param rpcResponse
* @param serializer
*/
void doResponse(HttpServerRequest request, RpcResponse rpcResponse, Serializer serializer) {
HttpServerResponse httpServerResponse = request.response()
.putHeader("content-type", "application/json");
try {
// 序列化
byte[] serialized = serializer.serialize(rpcResponse);
httpServerResponse.end(Buffer.buffer(serialized));
} catch (IOException e) {
e.printStackTrace();
httpServerResponse.end(Buffer.buffer());
}
}
}

TCP版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.yupi.yurpc.server.tcp;

import com.yupi.yurpc.model.RpcRequest;
import com.yupi.yurpc.model.RpcResponse;
import com.yupi.yurpc.protocol.ProtocolMessage;
import com.yupi.yurpc.protocol.ProtocolMessageDecoder;
import com.yupi.yurpc.protocol.ProtocolMessageEncoder;
import com.yupi.yurpc.protocol.ProtocolMessageTypeEnum;
import com.yupi.yurpc.registry.LocalRegistry;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;

import java.io.IOException;
import java.lang.reflect.Method;

public class TcpServerHandler implements Handler<NetSocket> {

@Override
public void handle(NetSocket netSocket) {
// 处理连接
netSocket.handler(buffer -> {
// 接受请求,解码
ProtocolMessage<RpcRequest> protocolMessage;
try {
protocolMessage = (ProtocolMessage<RpcRequest>) ProtocolMessageDecoder.decode(buffer);
} catch (IOException e) {
throw new RuntimeException("协议消息解码错误");
}
RpcRequest rpcRequest = protocolMessage.getBody();

// 处理请求
// 构造响应结果对象
RpcResponse rpcResponse = new RpcResponse();
try {
// 获取要调用的服务实现类,通过反射调用
Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName());
Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs());
// 封装返回结果
rpcResponse.setData(result);
rpcResponse.setDataType(method.getReturnType());
rpcResponse.setMessage("ok");
} catch (Exception e) {
e.printStackTrace();
rpcResponse.setMessage(e.getMessage());
rpcResponse.setException(e);
}

// 发送响应,编码
ProtocolMessage.Header header = protocolMessage.getHeader();
header.setType((byte) ProtocolMessageTypeEnum.RESPONSE.getKey());
ProtocolMessage<RpcResponse> responseProtocolMessage = new ProtocolMessage<>(header, rpcResponse);
try {
Buffer encode = ProtocolMessageEncoder.encode(responseProtocolMessage);
netSocket.write(encode);
} catch (IOException e) {
throw new RuntimeException("协议消息编码错误");
}
});
}
}

请求发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package com.yupi.yurpc.proxy;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.yupi.yurpc.RpcApplication;
import com.yupi.yurpc.config.RpcConfig;
import com.yupi.yurpc.constant.RpcConstant;
import com.yupi.yurpc.model.RpcRequest;
import com.yupi.yurpc.model.RpcResponse;
import com.yupi.yurpc.model.ServiceMetaInfo;
import com.yupi.yurpc.protocol.*;
import com.yupi.yurpc.registry.Registry;
import com.yupi.yurpc.registry.RegistryFactory;
import com.yupi.yurpc.serializer.Serializer;
import com.yupi.yurpc.serializer.SerializerFactory;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.SocketAddress;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;

/**
* 服务代理(JDK 动态代理)
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @learn <a href="https://codefather.cn">编程宝典</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
public class ServiceProxy implements InvocationHandler {

/**
* 调用代理
*
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 指定序列化器
final Serializer serializer = SerializerFactory.getInstance(RpcApplication.getRpcConfig().getSerializer());

// 构造请求
String serviceName = method.getDeclaringClass().getName();
RpcRequest rpcRequest = RpcRequest.builder()
.serviceName(serviceName)
.methodName(method.getName())
.parameterTypes(method.getParameterTypes())
.args(args)
.build();
try {
// 序列化
byte[] bodyBytes = serializer.serialize(rpcRequest);
// 从注册中心获取服务提供者请求地址
RpcConfig rpcConfig = RpcApplication.getRpcConfig();
Registry registry = RegistryFactory.getInstance(rpcConfig.getRegistryConfig().getRegistry());
ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo();
serviceMetaInfo.setServiceName(serviceName);
serviceMetaInfo.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION);
List<ServiceMetaInfo> serviceMetaInfoList = registry.serviceDiscovery(serviceMetaInfo.getServiceKey());
if (CollUtil.isEmpty(serviceMetaInfoList)) {
throw new RuntimeException("暂无服务地址");
}
ServiceMetaInfo selectedServiceMetaInfo = serviceMetaInfoList.get(0);
// 发送 TCP 请求
Vertx vertx = Vertx.vertx();
NetClient netClient = vertx.createNetClient();
CompletableFuture<RpcResponse> responseFuture = new CompletableFuture<>();
netClient.connect(selectedServiceMetaInfo.getServicePort(), selectedServiceMetaInfo.getServiceHost(),
result -> {
if (result.succeeded()) {
System.out.println("Connected to TCP server");
io.vertx.core.net.NetSocket socket = result.result();
// 发送数据
// 构造消息
ProtocolMessage<RpcRequest> protocolMessage = new ProtocolMessage<>();
ProtocolMessage.Header header = new ProtocolMessage.Header();
header.setMagic(ProtocolConstant.PROTOCOL_MAGIC);
header.setVersion(ProtocolConstant.PROTOCOL_VERSION);
header.setSerializer((byte) ProtocolMessageSerializerEnum.getEnumByValue(RpcApplication.getRpcConfig().getSerializer()).getKey());
header.setType((byte) ProtocolMessageTypeEnum.REQUEST.getKey());
header.setRequestId(IdUtil.getSnowflakeNextId());
protocolMessage.setHeader(header);
protocolMessage.setBody(rpcRequest);
// 编码请求
try {
Buffer encodeBuffer = ProtocolMessageEncoder.encode(protocolMessage);
socket.write(encodeBuffer);
} catch (IOException e) {
throw new RuntimeException("协议消息编码错误");
}

// 接收响应
socket.handler(buffer -> {
try {
ProtocolMessage<RpcResponse> rpcResponseProtocolMessage = (ProtocolMessage<RpcResponse>) ProtocolMessageDecoder.decode(buffer);
responseFuture.complete(rpcResponseProtocolMessage.getBody());
} catch (IOException e) {
throw new RuntimeException("协议消息解码错误");
}
});
} else {
System.err.println("Failed to connect to TCP server");
}
});

RpcResponse rpcResponse = responseFuture.get();
// 记得关闭连接
netClient.close();
return rpcResponse.getData();
} catch (IOException e) {
e.printStackTrace();
}

return null;
}
}

序列化器

序列化器JSON

JSON序列化器需要考虑对象转换的兼容问题,主要是因为Java语言中的泛型擦除(Type Erasure)机制和JSON数据格式本身的特性。

  1. 泛型擦除: 在Java中,泛型信息在编译后会被擦除,这意味着运行时无法直接获取泛型参数的具体类型信息。例如,List<String>List<Integer> 在运行时都被视为 List 类型。当使用Jackson等库进行反序列化时,默认情况下它们可能不知道如何将JSON对象映射回原始的Java泛型类型,可能会默认返回如 LinkedHashMap 这样的类型。
  2. JSON与Java对象模型差异: JSON是一种轻量级的数据交换格式,它没有像Java那样的复杂类型系统。JSON只支持几种基本类型(字符串、数字、布尔值、数组、对象和null)。因此,当从JSON反序列化到Java对象时,有时候JSON结构并不能完全对应Java类的结构,特别是对于复杂的嵌套对象或自定义类型。

Kryo 是一个专门为Java设计的高效序列化框架,它与JSON序列化器如Jackson相比,在处理对象转换兼容性问题上有不同的特性,这使得Kryo在某些情况下不需要特别考虑类型转换的兼容问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.yupi.yurpc.serializer;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yupi.yurpc.model.RpcRequest;
import com.yupi.yurpc.model.RpcResponse;

import java.io.IOException;


public class JsonSerializer implements Serializer {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

@Override
public <T> byte[] serialize(T obj) throws IOException {
return OBJECT_MAPPER.writeValueAsBytes(obj);
}

@Override
public <T> T deserialize(byte[] bytes, Class<T> classType) throws IOException {
T obj = OBJECT_MAPPER.readValue(bytes, classType);
if (obj instanceof RpcRequest) {
return handleRequest((RpcRequest) obj, classType);
}
if (obj instanceof RpcResponse) {
return handleResponse((RpcResponse) obj, classType);
}
return obj;
}

/**
* 由于 Object 的原始对象会被擦除,导致反序列化时会被作为 LinkedHashMap 无法转换成原始对象,因此这里做了特殊处理
*
* @param rpcRequest rpc 请求
* @param type 类型
* @return {@link T}
* @throws IOException IO异常
*/
private <T> T handleRequest(RpcRequest rpcRequest, Class<T> type) throws IOException {
Class<?>[] parameterTypes = rpcRequest.getParameterTypes();
Object[] args = rpcRequest.getArgs();

// 循环处理每个参数的类型
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> clazz = parameterTypes[i];
// 如果类型不同,则重新处理一下类型
if (!clazz.isAssignableFrom(args[i].getClass())) {
byte[] argBytes = OBJECT_MAPPER.writeValueAsBytes(args[i]);
args[i] = OBJECT_MAPPER.readValue(argBytes, clazz);
}
}
return type.cast(rpcRequest);
}

/**
* 由于 Object 的原始对象会被擦除,导致反序列化时会被作为 LinkedHashMap 无法转换成原始对象,因此这里做了特殊处理
*
* @param rpcResponse rpc 响应
* @param type 类型
* @return {@link T}
* @throws IOException IO异常
*/
private <T> T handleResponse(RpcResponse rpcResponse, Class<T> type) throws IOException {
// 处理响应数据
byte[] dataBytes = OBJECT_MAPPER.writeValueAsBytes(rpcResponse.getData());
rpcResponse.setData(OBJECT_MAPPER.readValue(dataBytes, rpcResponse.getDataType()));
return type.cast(rpcResponse);
}
}

kryo序列器

image-20241213150853882

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.yupi.yurpc.serializer;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

/**
* Kryo 序列化器
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @learn <a href="https://codefather.cn">编程宝典</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
public class KryoSerializer implements Serializer {
/**
* kryo 线程不安全,使用 ThreadLocal 保证每个线程只有一个 Kryo
*/
private static final ThreadLocal<Kryo> KRYO_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
// 设置动态动态序列化和反序列化类,不提前注册所有类(可能有安全问题)
kryo.setRegistrationRequired(false);
return kryo;
});

@Override
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);
KRYO_THREAD_LOCAL.get().writeObject(output, obj);
output.close();
return byteArrayOutputStream.toByteArray();
}

@Override
public <T> T deserialize(byte[] bytes, Class<T> classType) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream);
T result = KRYO_THREAD_LOCAL.get().readObject(input, classType);
input.close();
return result;
}
}

hessian

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.yupi.yurpc.serializer;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
* Hessian 序列化器
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @learn <a href="https://codefather.cn">编程宝典</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
public class HessianSerializer implements Serializer {
@Override
public <T> byte[] serialize(T object) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
ho.writeObject(object);
return bos.toByteArray();
}

@Override
public <T> T deserialize(byte[] bytes, Class<T> tClass) throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
HessianInput hi = new HessianInput(bis);
return (T) hi.readObject(tClass);
}
}

注册中心

  • 由于一个服务可能有多个提供者,有两种设计结构

image-20241213151432331

image-20241213151441791

image-20241213151453952

对于ZooKeeper和Etcd这种支持层级查询的中间件,用第一种更加清晰。

注册信息定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.yupi.yurpc.model;

/**
* 服务元信息(注册信息)
*/
public class ServiceMetaInfo {


/**
* 服务名称
*/
private String serviceName;

/**
* 服务版本号
*/
private String serviceVersion = "1.0";

/**
* 服务域名
*/
private String serviceHost;

/**
* 服务端口号
*/
private Integer servicePort;

/**
* 服务分组(暂未实现)
*/
private String serviceGroup = "default";

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取服务键名
*
* @return
*/
public String getServiceKey() {
// 后续可扩展服务分组
// return String.format("%s:%s:%s", serviceName, serviceVersion, serviceGroup);
return String.format("%s:%s", serviceName, serviceVersion);
}

/**
* 获取服务注册节点键名
*
* @return
*/
public String getServiceNodeKey() {
return String.format("%s/%s:%s", getServiceKey(), serviceHost, servicePort);
}

注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void register(ServiceMetaInfo serviceMetaInfo) throws Exception {
// 创建 Lease 和 KV 客户端
Lease leaseClient = client.getLeaseClient();

// 创建一个 30 秒的租约
long leaseId = leaseClient.grant(30).get().getID();

// 设置要存储的键值对
String registerKey = ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey();
ByteSequence key = ByteSequence.from(registerKey, StandardCharsets.UTF_8);
ByteSequence value = ByteSequence.from(JSONUtil.toJsonStr(serviceMetaInfo), StandardCharsets.UTF_8);

// 将键值对与租约关联起来,并设置过期时间
PutOption putOption = PutOption.builder().withLeaseId(leaseId).build();
kvClient.put(key, value, putOption).get();
}

假设有一个服务实例:

  • serviceName: “my-service”
  • serviceVersion: “1.0” (默认值)
  • serviceHost: “192.168.1.10”
  • servicePort: 8080

image-20241213152013472

服务发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) {
// 前缀搜索,结尾一定要加 '/'
String searchPrefix = ETCD_ROOT_PATH + serviceKey + "/";

try {
// 前缀查询
GetOption getOption = GetOption.builder().isPrefix(true).build();
List<KeyValue> keyValues = kvClient.get(
ByteSequence.from(searchPrefix, StandardCharsets.UTF_8),
getOption)
.get()
.getKvs();
// 解析服务信息
return keyValues.stream()
.map(keyValue -> {
String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
return JSONUtil.toBean(value, ServiceMetaInfo.class);
})
.collect(Collectors.toList());
} catch (Exception e) {
throw new RuntimeException("获取服务列表失败", e);
}
}

这段代码实现了基于 etcd 的服务发现功能,具体步骤如下:

  1. 构建查询前缀:根据给定的服务键构建一个特定的路径前缀。
  2. 配置查询选项:设置查询条件为前缀匹配。
  3. 执行查询:向 etcd 发起查询请求,获取所有匹配的服务实例。
  4. 解析结果:将查询结果中的每个键值对的值部分(JSON 字符串)反序列化为 ServiceMetaInfo 对象。
  5. 返回结果:将所有解析后的 ServiceMetaInfo 对象收集到一个列表中并返回。
  6. 异常处理:确保在出现错误时能够适当处理,并提供有用的错误信息。

心跳检测和续期机制

image-20241213152937020

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public void heartBeat() {
// 10 秒续签一次
CronUtil.schedule("*/10 * * * * *", new Task() {
@Override
public void execute() {
// 遍历本节点所有的 key
for (String key : localRegisterNodeKeySet) {
try {
List<KeyValue> keyValues = kvClient.get(ByteSequence.from(key, StandardCharsets.UTF_8))
.get()
.getKvs();
// 该节点已过期(需要重启节点才能重新注册)
if (CollUtil.isEmpty(keyValues)) {
continue;
}
// 节点未过期,重新注册(相当于续签)
KeyValue keyValue = keyValues.get(0);
String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
ServiceMetaInfo serviceMetaInfo = JSONUtil.toBean(value, ServiceMetaInfo.class);
register(serviceMetaInfo);
} catch (Exception e) {
throw new RuntimeException(key + "续签失败", e);
}
}
}
});

// 支持秒级别定时任务
CronUtil.setMatchSecond(true);
CronUtil.start();
}

利用重新注册实现续签,定时任务是通过 CronUtil 类来完成的,来自于Hutool

服务节点下线

image-20241213154523357

image-20241213154745096

本地缓存,用一个列表来实现

image-20241213155234761

image-20241213155313057

image-20241213155336978

自定义协议

自定义协议后TCP服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.yupi.yurpc.server.tcp;

import com.yupi.yurpc.server.HttpServer;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetServer;

public class VertxTcpServer implements HttpServer {

private byte[] handleRequest(byte[] requestData) {
// 在这里编写处理请求的逻辑,根据 requestData 构造响应数据并返回
// 这里只是一个示例,实际逻辑需要根据具体的业务需求来实现
return "Hello, client!".getBytes();
}

@Override
public void doStart(int port) {
// 创建 Vert.x 实例
Vertx vertx = Vertx.vertx();

// 创建 TCP 服务器
NetServer server = vertx.createNetServer();

// 处理请求
server.connectHandler(socket -> {
// 处理连接
socket.handler(buffer -> {
// 处理接收到的字节数组
byte[] requestData = buffer.getBytes();
// 在这里进行自定义的字节数组处理逻辑,比如解析请求、调用服务、构造响应等
byte[] responseData = handleRequest(requestData);
// 发送响应
socket.write(Buffer.buffer(responseData));
});
});

// 启动 TCP 服务器并监听指定端口
server.listen(port, result -> {
if (result.succeeded()) {
System.out.println("TCP server started on port " + port);
} else {
System.err.println("Failed to start TCP server: " + result.cause());
}
});
}

public static void main(String[] args) {
new VertxTcpServer().doStart(8888);
}
}

TCP客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.yupi.yurpc.server.tcp;

import io.vertx.core.Vertx;

public class VertxTcpClient {

public void start() {
// 创建 Vert.x 实例
Vertx vertx = Vertx.vertx();

vertx.createNetClient().connect(8888, "localhost", result -> {
if (result.succeeded()) {
System.out.println("Connected to TCP server");
io.vertx.core.net.NetSocket socket = result.result();
// 发送数据
socket.write("Hello, server!");
// 接收响应
socket.handler(buffer -> {
System.out.println("Received response from server: " + buffer.toString());
});
} else {
System.err.println("Failed to connect to TCP server");
}
});
}

public static void main(String[] args) {
new VertxTcpClient().start();
}
}

消息编码器与解码器

image-20241213155702105

半包粘包

image-20241213160311195

RecordParse的作用是:保证下次读到特定长度的字符。

  • image-20241213160435877

image-20241213160443518

负载均衡

轮询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.yupi.yurpc.loadbalancer;

import com.yupi.yurpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
* 轮询负载均衡器
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @learn <a href="https://codefather.cn">鱼皮的编程宝典</a>
* @from <a href="https://yupi.icu">编程导航学习圈</a>
*/
public class RoundRobinLoadBalancer implements LoadBalancer {

/**
* 当前轮询的下标
*/
private final AtomicInteger currentIndex = new AtomicInteger(0);

@Override
public ServiceMetaInfo select(Map<String, Object> requestParams, List<ServiceMetaInfo> serviceMetaInfoList) {
if (serviceMetaInfoList.isEmpty()) {
return null;
}
// 只有一个服务,无需轮询
int size = serviceMetaInfoList.size();
if (size == 1) {
return serviceMetaInfoList.get(0);
}
// 取模算法轮询
int index = currentIndex.getAndIncrement() % size;
return serviceMetaInfoList.get(index);
}
}

随机

1
2
3
4
5
6
7
8

// 只有 1 个服务,不用随机
if (size == 1) {
return serviceMetaInfoList.get(0);
}
return serviceMetaInfoList.get(random.nextInt(size));


一致性Hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.yupi.yurpc.loadbalancer;

import com.yupi.yurpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
* 一致性哈希负载均衡器
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @learn <a href="https://codefather.cn">鱼皮的编程宝典</a>
* @from <a href="https://yupi.icu">编程导航学习圈</a>
*/
public class ConsistentHashLoadBalancer implements LoadBalancer {

/**
* 一致性 Hash 环,存放虚拟节点
*/
private final TreeMap<Integer, ServiceMetaInfo> virtualNodes = new TreeMap<>();

/**
* 虚拟节点数
*/
private static final int VIRTUAL_NODE_NUM = 100;

@Override
public ServiceMetaInfo select(Map<String, Object> requestParams, List<ServiceMetaInfo> serviceMetaInfoList) {
if (serviceMetaInfoList.isEmpty()) {
return null;
}

// 构建虚拟节点环
for (ServiceMetaInfo serviceMetaInfo : serviceMetaInfoList) {
for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
int hash = getHash(serviceMetaInfo.getServiceAddress() + "#" + i);
virtualNodes.put(hash, serviceMetaInfo);
}
}

// 获取调用请求的 hash 值
int hash = getHash(requestParams);

// 选择最接近且大于等于调用请求 hash 值的虚拟节点
Map.Entry<Integer, ServiceMetaInfo> entry = virtualNodes.ceilingEntry(hash);
if (entry == null) {
// 如果没有大于等于调用请求 hash 值的虚拟节点,则返回环首部的节点
entry = virtualNodes.firstEntry();
}
return entry.getValue();
}


/**
* Hash 算法,可自行实现
*
* @param key
* @return
*/
private int getHash(Object key) {
return key.hashCode();
}
}

重试机制

不重试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.yupi.yurpc.fault.retry;

import com.yupi.yurpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;

/**
* 不重试 - 重试策略
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @learn <a href="https://codefather.cn">鱼皮的编程宝典</a>
* @from <a href="https://yupi.icu">编程导航学习圈</a>
*/
@Slf4j
public class NoRetryStrategy implements RetryStrategy {

/**
* 重试
*
* @param callable
* @return
* @throws Exception
*/
public RpcResponse doRetry(Callable<RpcResponse> callable) throws Exception {
return callable.call();
}

}

固定时间重试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.yupi.yurpc.fault.retry;

import com.github.rholder.retry.*;
import com.yupi.yurpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
* 固定时间间隔 - 重试策略
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @learn <a href="https://codefather.cn">鱼皮的编程宝典</a>
* @from <a href="https://yupi.icu">编程导航学习圈</a>
*/
@Slf4j
public class FixedIntervalRetryStrategy implements RetryStrategy {

/**
* 重试
*
* @param callable
* @return
* @throws ExecutionException
* @throws RetryException
*/
public RpcResponse doRetry(Callable<RpcResponse> callable) throws ExecutionException, RetryException {
Retryer<RpcResponse> retryer = RetryerBuilder.<RpcResponse>newBuilder()
.retryIfExceptionOfType(Exception.class)
.withWaitStrategy(WaitStrategies.fixedWait(3L, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
log.info("重试次数 {}", attempt.getAttemptNumber());
}
})
.build();
return retryer.call(callable);
}

}

image-20241213161255244

容错机制

image-20241213161344412

image-20241213161349841

手写RPC框架

简易版本RPC开发

基本概念

什么是RPC

RPC:Remote Procedure Call即远程过程调用,是一种计算机通信协议,允许程序在不同计算机之间进行通信和交互,就像本地调用一样。

为什么需要RPC

  • 透明性:RPC 提供了一种机制,使得远程服务调用看起来就像本地方法调用一样。开发者不需要关心底层的网络通信细节,可以专注于业务逻辑。
  • 封装性:RPC 框架通常会处理序列化、反序列化、网络传输等复杂操作,将这些细节从应用程序中抽象出来。
  • 模块化:通过将服务拆分为多个独立的服务,并使用 RPC 进行通信,可以实现更好的模块化。每个服务都可以独立开发、部署和扩展。
  • 解耦合:服务之间的依赖关系可以通过接口定义来管理,而不是直接的代码依赖。这使得系统的各个部分更加松散耦合,更容易进行修改和升级。

RPC框架实现思路

基本设计

假设有消费者和服务提供者两个角色

image-20241015192036350

消费者想要调用提供者,就需要提供者启动一个web服务,然后通过请求客户端发送HTTP请求或者其他协议来调用。

若有多个服务和方法,每个接口都要单独写一个接口,则较为麻烦。

则提供一个统一的服务调用接口,通过请求处理器根据客户端的请求参数来进行不同的处理、调用不同的服务和方法。

可以在服务提供者维护一个本地服务注册器,记录服务和对应实现类的映射。

image-20241015194556974

另外,需要注意的是,java对象在网络中无法直接传输,所以需要对传输的参数进行序列化和反序列化。

所以,一个简易的RPC框架就此诞生。

image-20241015194719132

开发简易版RPC框架

简易版实现几个模块:

  • example-common:示例代码的公共依赖,包括接口、model等
  • example-consumer:示例服务消费者代码
  • example-provide:示例服务t提供者代码
  • lkj-rpc-easy:简易版rpc框架

公共模块

  • 编写实体类User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.lkj.example.common.model;

import java.io.Serializable;


public class User implements Serializable {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

对象需要实习序列化接口,为后续网络传输序列化提供支持

  • 编写用户服务接口UserService,提供一个获取用户的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.lkj.example.common.service;

import com.lkj.example.common.model.User;


public interface UserService {

/**
* 获取用户
*
* @param user
* @return
*/
User getUser(User user);


}

服务提供者

  • 编写服务实现类,实现公共模块中定义的用户服务接口

功能是打印用户的名称,并且返回参数中的用户对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.lkj.example.provider;

import com.lkj.example.common.model.User;
import com.lkj.example.common.service.UserService;


public class UserServiceImpl implements UserService {

public User getUser(User user) {
System.out.println("用户名:" + user.getName());
return user;
}
}

  • 编写服务提供者启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.lkj.example.provider;

import com.lkj.example.common.service.UserService;
import com.lkj.yurpc.registry.LocalRegistry;
import com.lkj.yurpc.server.HttpServer;
import com.lkj.yurpc.server.VertxHttpServer;


public class EasyProviderExample {

public static void main(String[] args) {
// 注册服务
LocalRegistry.register(UserService.class.getName(), UserServiceImpl.class);

// 启动 web 服务
HttpServer httpServer = new VertxHttpServer();
httpServer.doStart(8080);
}
}

服务消费者

  • 创建服务消费者启动类,编写调用接口的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 package com.lkj.example.consumer;

import com.lkj.example.common.model.User;
import com.lkj.example.common.service.UserService;
import com.lkj.yurpc.proxy.ServiceProxyFactory;


public class EasyConsumerExample {

public static void main(String[] args) {

UserService userService = null;
User user = new User();
user.setName("lkj");
// 调用
User newUser = userService.getUser(user);
if (newUser != null) {
System.out.println(newUser.getName());
} else {
System.out.println("user == null");
}
}
}

这里我们还没有获取userService实例,所以定义为null

web服务器

要让服务提供者提供可远程访问的服务,那么就需要一个web服务器,接受处理并返回请求。

此处使用高性能的NIO框架Vert.x作为RPC框架的web服务器。

  • 编写一个web服务器的接口HttpServer,定义统一的启动服务器方法
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.lkj.yurpc.server;


public interface HttpServer {

/**
* 启动服务器
*
* @param port
*/
void doStart(int port);
}

  • 编写基于Vert.x的web服务器VertxHttpServer,能够箭筒指定端口并处理请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.lkj.yurpc.server;

import io.vertx.core.Vertx;


public class VertxHttpServer implements HttpServer {

/**
* 启动服务器
*
* @param port
*/
public void doStart(int port) {
// 创建 Vert.x 实例
Vertx vertx = Vertx.vertx();

// 创建 HTTP 服务器
io.vertx.core.http.HttpServer server = vertx.createHttpServer();

// 监听端口并处理请求
server.requestHandler(new HttpServerHandler());

// 启动 HTTP 服务器并监听指定端口
server.listen(port, result -> {
if (result.succeeded()) {
System.out.println("Server is now listening on port " + port);
} else {
System.err.println("Failed to start server: " + result.cause());
}
});
}
}

本地服务注册器

在简易版本中,暂时不用第三方注册中心,直接把服务注册到服务提供者本地。

  • 使用线程安全的ConcurrentHashMap存储服务注册信息,key为服务名称,value为服务的实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.lkj.yurpc.registry;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class LocalRegistry {

/**
* 注册信息存储
*/
private static final Map<String, Class<?>> map = new ConcurrentHashMap<>();

/**
* 注册服务
*
* @param serviceName
* @param implClass
*/
public static void register(String serviceName, Class<?> implClass) {
map.put(serviceName, implClass);
}

/**
* 获取服务
*
* @param serviceName
* @return
*/
public static Class<?> get(String serviceName) {
return map.get(serviceName);
}

/**
* 删除服务
*
* @param serviceName
*/
public static void remove(String serviceName) {
map.remove(serviceName);
}
}

序列化器

  • 序列化:将java对象转为可传输的字节数组
  • 反序列化:将字节数组转换为可传输的java对象

为了方便此处选择java原生的序列化器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.lkj.yurpc.serializer;

import java.io.*;


public class JdkSerializer implements Serializer {

/**
* 序列化
*
* @param object
* @param <T>
* @return
* @throws IOException
*/
@Override
public <T> byte[] serialize(T object) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
objectOutputStream.close();
return outputStream.toByteArray();
}

/**
* 反序列化
*
* @param bytes
* @param type
* @param <T>
* @return
* @throws IOException
*/
@Override
public <T> T deserialize(byte[] bytes, Class<T> type) throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
try {
return (T) objectInputStream.readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
objectInputStream.close();
}
}
}

提供者调用-请求处理器

  • RpcRequest的作用是封装调用所需的信息,比如服务名称,方法名称,调用参数的类型列表,参数列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.lkj.yurpc.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RpcRequest implements Serializable {

/**
* 服务名称
*/
private String serviceName;

/**
* 方法名称
*/
private String methodName;

/**
* 参数类型列表
*/
private Class<?>[] parameterTypes;

/**
* 参数列表
*/
private Object[] args;

}
  • 响应类RpcResponse的作用是封装调用方法得到的返回值、以及调用的信息等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.lkj.yurpc.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RpcResponse implements Serializable {

/**
* 响应数据
*/
private Object data;

/**
* 响应数据类型(预留)
*/
private Class<?> dataType;

/**
* 响应信息
*/
private String message;

/**
* 异常信息
*/
private Exception exception;

}
  • 编写请求处理器 HttpServerHandler

业务流程如下:

1.反序列化请求为对象,并从请求对象中获取参数

2.根据服务名称从本地注册器中获取到对应的服务实现类

3.通过反射机制调用方法,得到返回结果

4.对返回结果进行封装和序列化,并写入响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.lkj.yurpc.server;

import com.lkj.yurpc.model.RpcRequest;
import com.lkj.yurpc.model.RpcResponse;
import com.lkj.yurpc.registry.LocalRegistry;
import com.lkj.yurpc.serializer.JdkSerializer;
import com.lkj.yurpc.serializer.Serializer;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;

import java.io.IOException;
import java.lang.reflect.Method;


public class HttpServerHandler implements Handler<HttpServerRequest> {

@Override
public void handle(HttpServerRequest request) {
// 指定序列化器
final Serializer serializer = new JdkSerializer();

// 记录日志
System.out.println("Received request: " + request.method() + " " + request.uri());

// 异步处理 HTTP 请求
request.bodyHandler(body -> {
byte[] bytes = body.getBytes();
RpcRequest rpcRequest = null;
try {
rpcRequest = serializer.deserialize(bytes, RpcRequest.class);
} catch (Exception e) {
e.printStackTrace();
}

// 构造响应结果对象
RpcResponse rpcResponse = new RpcResponse();
// 如果请求为 null,直接返回
if (rpcRequest == null) {
rpcResponse.setMessage("rpcRequest is null");
doResponse(request, rpcResponse, serializer);
return;
}

try {
// 获取要调用的服务实现类,通过反射调用
Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName());
Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs());
// 封装返回结果
rpcResponse.setData(result);
rpcResponse.setDataType(method.getReturnType());
rpcResponse.setMessage("ok");
} catch (Exception e) {
e.printStackTrace();
rpcResponse.setMessage(e.getMessage());
rpcResponse.setException(e);
}
// 响应
doResponse(request, rpcResponse, serializer);
});
}

/**
* 响应
*
* @param request
* @param rpcResponse
* @param serializer
*/
void doResponse(HttpServerRequest request, RpcResponse rpcResponse, Serializer serializer) {
HttpServerResponse httpServerResponse = request.response()
.putHeader("content-type", "application/json");
try {
// 序列化
byte[] serialized = serializer.serialize(rpcResponse);
httpServerResponse.end(Buffer.buffer(serialized));
} catch (IOException e) {
e.printStackTrace();
httpServerResponse.end(Buffer.buffer());
}
}
}

消费方发起调用-代理

代理的实现方式大致分为2类:静态代理和动态代理

静态代理

静态代理是指为每一个特定类型的接口或对象,编写一个代理类。

静态代理虽然很好理解,但缺点也很明显,如果给每个服务接口都写一个实现类,是非常麻烦的,代理方式灵活性很差。

所以RPC框架中,会使用动态代理。

动态代理

动态代理的作用是,根据要生成的对象类型,自动生成一个代理对象。

常用的动态代理实现方式有JDK动态代理和基于字节码生成的动态代理。前者简单易用、无需引入额外的库,但缺点是只能对接口进行代理;后者更灵活、可以对任何类进行代理,但性能略低于JDK动态代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.lkj.yurpc.proxy;

import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.lkj.yurpc.model.RpcRequest;
import com.lkj.yurpc.model.RpcResponse;
import com.lkj.yurpc.serializer.JdkSerializer;
import com.lkj.yurpc.serializer.Serializer;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;


public class ServiceProxy implements InvocationHandler {

/**
* 调用代理
*
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 指定序列化器
Serializer serializer = new JdkSerializer();

// 构造请求
RpcRequest rpcRequest = RpcRequest.builder()
.serviceName(method.getDeclaringClass().getName())
.methodName(method.getName())
.parameterTypes(method.getParameterTypes())
.args(args)
.build();
try {
// 序列化
byte[] bodyBytes = serializer.serialize(rpcRequest);
// 发送请求
// todo 注意,这里地址被硬编码了(需要使用注册中心和服务发现机制解决)
try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080")
.body(bodyBytes)
.execute()) {
byte[] result = httpResponse.bodyBytes();
// 反序列化
RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);
return rpcResponse.getData();
}
} catch (IOException e) {
e.printStackTrace();
}

return null;
}
}

当用户调用某个接口的方法时,会改为调用invoke方法。在invoke方法中,我们可以获取到要调用的方法信息,传入的参数列表等。

需要注意的时,上述代码中,请求的服务提供者地址被硬编码了,需要使用注册中心和服务发现机制来解决。

  • 创建动态代理工厂ServiceProxyFactory,作用是根据指定类创建动态代理对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.lkj.yurpc.proxy;

import java.lang.reflect.Proxy;


public class ServiceProxyFactory {

/**
* 根据服务类获取代理对象
*
* @param serviceClass
* @param <T>
* @return
*/
public static <T> T getProxy(Class<T> serviceClass) {
return (T) Proxy.newProxyInstance(
serviceClass.getClassLoader(),
new Class[]{serviceClass},
new ServiceProxy());
}
}

简易测试

image-20241015222428567

image-20241015222646726

image-20241015222659479

静态代理的调试过程

  • 直接跳到20行,开始调用getuser

image-20241016142105817

  • 进入UserServiceProxy的getUser方法

image-20241016142226322

  • 执行到http请求,进入拦截器部分,即第35行

image-20241016142257749

  • 开始执行拦截器,进行反射调用返回结果,中间进行序列化反序列化等操作

image-20241016142426358

  • 返回代理中,得到结果

image-20241016143828389

动态代理调试过程

  • InvocationHandler 接口ServiceProxy 类实现了 InvocationHandler 接口,这是 Java 动态代理的核心接口。

  • invoke 方法invoke 方法是 InvocationHandler 接口中的唯一方法。这个方法会在每次通过代理对象调用方法时被调用。

    invoke方法接收三个参数:

    • proxy:代理对象本身。
    • method:被调用的方法。
    • args:方法的参数。

动态代理的工作流程

  1. 构造请求invoke 方法首先构建一个 RpcRequest 对象,包含服务名、方法名、参数类型和参数值。
  2. 序列化请求:将 RpcRequest 对象序列化为字节数组。
  3. 发送请求:使用 HTTP 请求库(如 Hutool)将序列化的请求发送到指定的服务器地址(这里是硬编码的 http://localhost:8080)。
  4. 接收响应:从服务器接收响应,并将其反序列化为 RpcResponse 对象。
  5. 返回结果:从 RpcResponse 中提取数据并返回给调用者。

与静态代理的区别

静态代理

  • 手动创建代理类:静态代理需要为每个接口手动创建一个代理类,代理类实现了相同的接口。
  • 编译时确定:静态代理在编译时就已经确定,代理类的代码是固定的。
  • 灵活性差:如果接口发生变化,需要手动修改代理类,维护成本高。

动态代理

  • 自动生成代理类:动态代理在运行时自动生成代理类,不需要手动编写代理类。
  • 运行时确定:动态代理在运行时根据实际调用的方法动态生成代理类。
  • 灵活性高:动态代理可以灵活地添加或修改拦截逻辑,适用于多种场景,特别是当接口经常变化时。

由于动态代理中,invoke方法会在每次通过代理对象调用方法时被调用。拦截器调用次数多,调试较为繁琐,所以不做图片展示。

全局配置加载

需求分析

在RPC框架的运行中,会涉及到很多的配置信息,比如注册中心的地址,序列化方式,网络服务器端口号等等。

在简易版本的RPC项目中,我们采用了硬编码的方式,不利于维护。

同时PRC框架是需要被其他项目作为提供者或消费者引入的,所以我们应当允许引入框架的项目编写配置文件来自定义配置。

因此,需要配置一套全局配置加载功能。

项目实现

配置加载

  • 新建RpcConfig,用于保存配置信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.lkj.yurpc.config;


import lombok.Data;


@Data
public class RpcConfig {

/**
* 名称
*/
private String name = "yu-rpc";

/**
* 版本号
*/
private String version = "1.0";

/**
* 服务器主机名
*/
private String serverHost = "localhost";

/**
* 服务器端口号
*/
private Integer serverPort = 8080;
}
  • 新建工具类ConfigUtils,作用是读配置文件并返回配置对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.lkj.yurpc.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.setting.dialect.Props;


public class ConfigUtils {

/**
* 加载配置对象
*
* @param tClass
* @param prefix
* @param <T>
* @return
*/
public static <T> T loadConfig(Class<T> tClass, String prefix) {
return loadConfig(tClass, prefix, "");
}

/**
* 加载配置对象,支持区分环境
*
* @param tClass
* @param prefix
* @param environment
* @param <T>
* @return
*/
public static <T> T loadConfig(Class<T> tClass, String prefix, String environment) {
StringBuilder configFileBuilder = new StringBuilder("application");
if (StrUtil.isNotBlank(environment)) {
configFileBuilder.append("-").append(environment);
}
configFileBuilder.append(".properties");
Props props = new Props(configFileBuilder.toString());
return props.toBean(tClass, prefix);
}
}

  • 新建RpcConstant接口,用于存储RPC框架相关常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.lkj.yurpc.constant;

/**
* RPC 相关常量

*/
public interface RpcConstant {

/**
* 默认配置文件加载前缀
*/
String DEFAULT_CONFIG_PREFIX = "rpc";

/**
* 默认服务版本
*/
String DEFAULT_SERVICE_VERSION = "1.0";

}

维护全局配置对象

RPC框架中需要维护一个全局的配置对象,在引入RPC框架的项目启动时,从配置文件中读取配置并创建对象实例,之后就可以集中的从这个对象获取配置信息,而不是每次加载配置时重新读取配置,并创建新的对象,减少了性能开销。

使用设计模式中的单例模式。

单例模式(Singleton Pattern)是设计模式中的一种创建型模式,它确保一个类只有一个实例,并提供一个全局访问点。单例模式的主要目的是控制共享资源的访问,例如数据库连接或线程池。

单例模式的特点

  1. 唯一实例:确保一个类只有一个实例。
  2. 全局访问点:提供一个全局的访问点来获取这个唯一的实例。
  3. 延迟初始化:可以实现懒加载(Lazy Initialization),即在第一次使用时才创建实例。
  4. 线程安全:在多线程环境下,必须保证单例的创建是线程安全的。

一般情况下,我们会使用holder来维护全局配置对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.lkj.yurpc;

import com.lkj.yurpc.config.RpcConfig;
import com.lkj.yurpc.constant.RpcConstant;
import com.lkj.yurpc.utils.ConfigUtils;
import lombok.extern.slf4j.Slf4j;

/**
* RPC 框架应用
* 相当于 holder,存放了项目全局用到的变量。双检锁单例模式实现
*/
@Slf4j
public class RpcApplication {

private static volatile RpcConfig rpcConfig;

/**
* 框架初始化,支持传入自定义配置
*
* @param newRpcConfig
*/
public static void init(RpcConfig newRpcConfig) {
rpcConfig = newRpcConfig;
log.info("rpc init, config = {}", newRpcConfig.toString());
}

/**
* 初始化
*/
public static void init() {
RpcConfig newRpcConfig;
try {
newRpcConfig = ConfigUtils.loadConfig(RpcConfig.class, RpcConstant.DEFAULT_CONFIG_PREFIX);
} catch (Exception e) {
// 配置加载失败,使用默认值
newRpcConfig = new RpcConfig();
}
init(newRpcConfig);
}

/**
* 获取配置
*
* @return
*/
public static RpcConfig getRpcConfig() {
if (rpcConfig == null) {
synchronized (RpcApplication.class) {
if (rpcConfig == null) {
init();
}
}
}
return rpcConfig;
}
}

测试

在example-consumer项目的resourses目录编写配置文件application.properties

1
2
3
4
rpc.name=yurpc
rpc.version=2.0
rpc.mock=false
rpc.serializer=jdk

创建ConsumerExample作为扩展后RPC项目的示例消费者类,测试配置文件读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.lkj.yurpc.config.RpcConfig;
import com.lkj.yurpc.utils.ConfigUtils;

/**
* 简易服务消费者示例
*

*/
public class ConsumerExample {

public static void main(String[] args) {
RpcConfig rpc = ConfigUtils.loadConfig(RpcConfig.class, "rpc");
System.out.println(rpc);

}
}

image-20241017153826043

接口Mock

需求分析

什么是Mock

RPC框架的核心功能是调用其他远程服务。但是在实际开发和测试过程中,有时候可能无法访问真实的远程服务,并且远程服务可能产生不可控的影响,例如网络延迟,服务不稳定等。在这种情况下,就需要使用mock服务来模拟远程服务的行为,以便进行接口的测试开发和调试。

mock是指模拟对象,通常用于测试代码中,特别是在单元测试中,便于我们跑通业务流程。

设计方案

通过动态代理创建一个调用方法时返回固定值的对象。

开发实现

  • 首先给全局配置类RpcConfig新增mock字段,默认值为false
  • 在Proxy包下新增MockServiceProxy类,用于生成mock代理方法。

在这个类中,需要提供一个根据服务接口类型返回固定值的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.lkj.yurpc.proxy;

import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
* Mock 服务代理(JDK 动态代理)

*/
@Slf4j
public class MockServiceProxy implements InvocationHandler {

/**
* 调用代理
*
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 根据方法的返回值类型,生成特定的默认值对象
Class<?> methodReturnType = method.getReturnType();
log.info("mock invoke {}", method.getName());
return getDefaultObject(methodReturnType);
}

/**
* 生成指定类型的默认值对象(可自行完善默认值逻辑)
*
* @param type
* @return
*/
private Object getDefaultObject(Class<?> type) {
// 基本类型
if (type.isPrimitive()) {
if (type == boolean.class) {
return false;
} else if (type == short.class) {
return (short) 0;
} else if (type == int.class) {
return 0;
} else if (type == long.class) {
return 0L;
}
}
// 对象类型
return null;
}
}

  • 给ServiceProxyFactory服务代理工厂新增获取mock代理对象的方法getMockProxy。
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据服务类获取 Mock 代理对象
*
* @param serviceClass
* @param <T>
* @return
*/
public static <T> T getMockProxy(Class<T> serviceClass) {
return (T) Proxy.newProxyInstance(
serviceClass.getClassLoader(),
new Class[]{serviceClass},
new MockServiceProxy());
}

测试

  • 在UserService中写个具有默认实现的新方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.lkj.example.common.service;

import com.lkj.example.common.model.User;


public interface UserService {

/**
* 获取用户
*
* @param user
* @return
*/
User getUser(User user);

/**
* 新方法 - 获取数字
*/
default short getNumber() {
return 1;
}

}

  • 修改ConsumerExample类,编写调用的新方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConsumerExample {

public static void main(String[] args) {
//RpcConfig rpc = ConfigUtils.loadConfig(RpcConfig.class, "rpc");
//System.out.println(rpc);
// 获取代理
UserService userService = ServiceProxyFactory.getProxy(UserService.class);
User user = new User();
user.setName("lkj");
// 调用
User newUser = userService.getUser(user);
if (newUser != null) {
System.out.println(newUser.getName());
} else {
System.out.println("user == null");
}
long number = userService.getNumber();
System.out.println(number);

}
}

可以看到输出的结果为0,而不是1,说明调用了模拟服务代理。

image-20241017193602008

SPI

  • SPI(Service Provider Interface) 服务提供接口是Java的机制,主要用于实现模块化开发和插件化开发扩展。
  • SPI机制允许服务提供者通过特定的配置文件将自己的实现注册到系统,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。

一个典型的SPI应用场景是JDBC

  • 编写SpiLoder加载器

相当于一个工具类,提供了读取配置并加载实现类的方法。

关键实现如下:

  • 用map来存储已知的配置信息,键名->实现类
  • 扫描指定路径,读取每个配置文件,获取到键名->实现类信息并存储在map中
  • 定义获取实例方法,根据用户传入的接口和键名,从map中寻找对应的实现类,然后通过反射获取到实现类对象。可以维护一个对象实例缓存,创建过一次的对象从缓存中读取即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package com.lkj.yurpc.spi;

import cn.hutool.core.io.resource.ResourceUtil;
import com.lkj.yurpc.serializer.Serializer;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* SPI 加载器
* 自定义实现,支持键值对映射
*

*/
@Slf4j
public class SpiLoader {

/**
* 存储已加载的类:接口名 =>(key => 实现类)
*/
private static final Map<String, Map<String, Class<?>>> loaderMap = new ConcurrentHashMap<>();

/**
* 对象实例缓存(避免重复 new),类路径 => 对象实例,单例模式
*/
private static final Map<String, Object> instanceCache = new ConcurrentHashMap<>();

/**
* 系统 SPI 目录
*/
private static final String RPC_SYSTEM_SPI_DIR = "META-INF/rpc/system/";

/**
* 用户自定义 SPI 目录
*/
private static final String RPC_CUSTOM_SPI_DIR = "META-INF/rpc/custom/";

/**
* 扫描路径
*/
private static final String[] SCAN_DIRS = new String[]{RPC_SYSTEM_SPI_DIR, RPC_CUSTOM_SPI_DIR};

/**
* 动态加载的类列表
*/
private static final List<Class<?>> LOAD_CLASS_LIST = Arrays.asList(Serializer.class);

/**
* 加载所有类型
*/
public static void loadAll() {
log.info("加载所有 SPI");
for (Class<?> aClass : LOAD_CLASS_LIST) {
load(aClass);
}
}

/**
* 获取某个接口的实例
*
* @param tClass
* @param key
* @param <T>
* @return
*/
public static <T> T getInstance(Class<?> tClass, String key) {
String tClassName = tClass.getName();
Map<String, Class<?>> keyClassMap = loaderMap.get(tClassName);
if (keyClassMap == null) {
throw new RuntimeException(String.format("SpiLoader 未加载 %s 类型", tClassName));
}
if (!keyClassMap.containsKey(key)) {
throw new RuntimeException(String.format("SpiLoader 的 %s 不存在 key=%s 的类型", tClassName, key));
}
// 获取到要加载的实现类型
Class<?> implClass = keyClassMap.get(key);
// 从实例缓存中加载指定类型的实例
String implClassName = implClass.getName();
if (!instanceCache.containsKey(implClassName)) {
try {
instanceCache.put(implClassName, implClass.newInstance());
} catch (InstantiationException | IllegalAccessException e) {
String errorMsg = String.format("%s 类实例化失败", implClassName);
throw new RuntimeException(errorMsg, e);
}
}
return (T) instanceCache.get(implClassName);
}

/**
* 加载某个类型
*
* @param loadClass
* @throws IOException
*/
public static Map<String, Class<?>> load(Class<?> loadClass) {
log.info("加载类型为 {} 的 SPI", loadClass.getName());
// 扫描路径,用户自定义的 SPI 优先级高于系统 SPI
Map<String, Class<?>> keyClassMap = new HashMap<>();
for (String scanDir : SCAN_DIRS) {
List<URL> resources = ResourceUtil.getResources(scanDir + loadClass.getName());
// 读取每个资源文件
for (URL resource : resources) {
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.openStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
String[] strArray = line.split("=");
if (strArray.length > 1) {
String key = strArray[0];
String className = strArray[1];
keyClassMap.put(key, Class.forName(className));
}
}
} catch (Exception e) {
log.error("spi resource load error", e);
}
}
}
loaderMap.put(loadClass.getName(), keyClassMap);
return keyClassMap;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
loadAll();
System.out.println(loaderMap);
Serializer serializer = getInstance(Serializer.class, "e");
System.out.println(serializer);
}

}

注册中心基本实现

需求分析

RPC框架的一个核心模块式注册中心,其目的是帮助服务消费者获取到服务提供者的调用地址,而不是把调用地址硬编码到项目中

image-20241020182959528

设计方案

注册中心核心功能

  • 数据分布式存储:集中的注册信息数据存储、读取和共享
  • 服务注册:服务提供者上报服务信息到注册中心
  • 服务发现:服务消费者从注册中心拉取服务信息
  • 心跳检测:定期检查服务提供者的存活状态
  • 服务注销:手动剔除节点,或者自动删除失效节点

技术选型

  • 需要一个能够集中存储和读取数据的中间件。还需要有数据过期,数据监听的能力,使我们移除失效节点,更新节点列表。

ETCD

数据结构

采用层次化的键值对来存储数据,支持类似于文件系统路径的层次结构

核心结构:

  • key:Etcd的基本数据单元,类似于文件系统的文件名。每个键都唯一标识一个值,并且可以包含子键,形成于类似路径的层次结构。
  • value:与键关联的数据,通常为字符型

ETCD核心特性:

  • Lease(租约):用于对键值对进行TTL超时设置,即设置键值对的过期时间。
  • Watch(监听):可以监视特定键的变化,当键的值发生变化时,会触发相应通知。

开发实现

注册中心开发

  • 注册信息定义

在model包下新建ServiceMetaInfo类,封装服务的注册信息,包括服务名称、服务版本号、服务地址和服务分组等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.lkj.yurpc.model;

import cn.hutool.core.util.StrUtil;
import com.lkj.yurpc.constant.RpcConstant;
import lombok.Data;

/**
* 服务元信息(注册信息)
*

*/
@Data
public class ServiceMetaInfo {

/**
* 服务名称
*/
private String serviceName;

/**
* 服务版本号
*/
private String serviceVersion = RpcConstant.DEFAULT_SERVICE_VERSION;

/**
* 服务域名
*/
private String serviceHost;

/**
* 服务端口号
*/
private Integer servicePort;

/**
* 服务分组(暂未实现)
*/
private String serviceGroup = "default";
/**
* 获取服务键名
*
* @return
*/
public String getServiceKey() {
// 后续可扩展服务分组
// return String.format("%s:%s:%s", serviceName, serviceVersion, serviceGroup);
return String.format("%s:%s", serviceName, serviceVersion);
}

/**
* 获取服务注册节点键名
*
* @return
*/
public String getServiceNodeKey() {
return String.format("%s/%s:%s", getServiceKey(), serviceHost, servicePort);
}

/**
* 获取完整服务地址
*
* @return
*/
public String getServiceAddress() {
if (!StrUtil.contains(serviceHost, "http")) {
return String.format("http://%s:%s", serviceHost, servicePort);
}
return String.format("%s:%s", serviceHost, servicePort);
}

  • 注册中心配置

在config包下编写注册中心配置类RegistryConfig,让用户配置连接注册中心所需的信息,比如注册中心类别、注册中心地址、用户名、密码、连接超时等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.lkj.yurpc.config;

//import com.lkj.yurpc.registry.RegistryKeys;
import lombok.Data;

/**
* RPC 框架注册中心配置
*

*/
@Data
public class RegistryConfig {

/**
* 注册中心类别
*/
private String registry = "etcd";

/**
* 注册中心地址
*/
private String address = "http://localhost:2380";

/**
* 用户名
*/
private String username;

/**
* 密码
*/
private String password;

/**
* 超时时间(单位毫秒)
*/
private Long timeout = 10000L;
}

  • 注册中心接口

遵循可扩展机制,先写一个注册中心接口,后续可以实现多种不同的注册中心,使用SPI机制动态加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.lkj.yurpc.registry;

import com.lkj.yurpc.config.RegistryConfig;
import com.lkj.yurpc.model.ServiceMetaInfo;

import java.util.List;

/**
* 注册中心
*

*/
public interface Registry {

/**
* 初始化
*
* @param registryConfig
*/
void init(RegistryConfig registryConfig);

/**
* 注册服务(服务端)
*
* @param serviceMetaInfo
*/
void register(ServiceMetaInfo serviceMetaInfo) throws Exception;

/**
* 注销服务(服务端)
*
* @param serviceMetaInfo
*/
void unRegister(ServiceMetaInfo serviceMetaInfo);

/**
* 服务发现(获取某服务的所有节点,消费端)
*
* @param serviceKey 服务键名
* @return
*/
List<ServiceMetaInfo> serviceDiscovery(String serviceKey);

/**
* 心跳检测(服务端)
*/
//void heartBeat();

/**
* 监听(消费端)
*
* @param serviceNodeKey
*/
// void watch(String serviceNodeKey);

/**
* 服务销毁
*/
void destroy();
}
  • etcd注册中心实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package com.lkj.yurpc.registry;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.cron.CronUtil;
import cn.hutool.cron.task.Task;
import cn.hutool.json.JSONUtil;
import com.lkj.yurpc.config.RegistryConfig;
import com.lkj.yurpc.model.ServiceMetaInfo;
import io.etcd.jetcd.*;
import io.etcd.jetcd.options.GetOption;
import io.etcd.jetcd.options.PutOption;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class EtcdRegistry implements Registry {

private Client client;

private KV kvClient;

/**
* 根节点
*/
private static final String ETCD_ROOT_PATH = "/rpc/";

@Override
public void init(RegistryConfig registryConfig) {
client = Client.builder().endpoints(registryConfig.getAddress()).connectTimeout(Duration.ofMillis(registryConfig.getTimeout())).build();
kvClient = client.getKVClient();
}

@Override
public void register(ServiceMetaInfo serviceMetaInfo) throws Exception {
// 创建 Lease 和 KV 客户端
Lease leaseClient = client.getLeaseClient();

// 创建一个 30 秒的租约
long leaseId = leaseClient.grant(30).get().getID();

// 设置要存储的键值对
String registerKey = ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey();
ByteSequence key = ByteSequence.from(registerKey, StandardCharsets.UTF_8);
ByteSequence value = ByteSequence.from(JSONUtil.toJsonStr(serviceMetaInfo), StandardCharsets.UTF_8);

// 将键值对与租约关联起来,并设置过期时间
PutOption putOption = PutOption.builder().withLeaseId(leaseId).build();
kvClient.put(key, value, putOption).get();
}

@Override
public void unRegister(ServiceMetaInfo serviceMetaInfo) {
kvClient.delete(ByteSequence.from(ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey(), StandardCharsets.UTF_8));
}

@Override
public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) {
// 前缀搜索,结尾一定要加 '/'
String searchPrefix = ETCD_ROOT_PATH + serviceKey + "/";

try {
// 前缀查询
GetOption getOption = GetOption.builder().isPrefix(true).build();
List<KeyValue> keyValues = kvClient.get(
ByteSequence.from(searchPrefix, StandardCharsets.UTF_8),
getOption)
.get()
.getKvs();
// 解析服务信息
return keyValues.stream()
.map(keyValue -> {
String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
return JSONUtil.toBean(value, ServiceMetaInfo.class);
})
.collect(Collectors.toList());
} catch (Exception e) {
throw new RuntimeException("获取服务列表失败", e);
}
}

@Override
public void destroy() {
System.out.println("当前节点下线");
// 释放资源
if (kvClient != null) {
kvClient.close();
}
if (client != null) {
client.close();
}
}
}

支持配置和扩展注册中心

一个成熟的rpc框架可能会支持多个注册中心,我们的需求是,让开发者能够填写配置来指定使用的注册中心,并且支持自定义注册中心

  • 注册中心常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.lkj.yurpc.registry;

/**
* 注册中心键名常量
*

*/
public interface RegistryKeys {

String ETCD = "etcd";

String ZOOKEEPER = "zookeeper";

}

  • 工厂模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RegistryFactory {

// SPI 动态加载
static {
SpiLoader.load(Registry.class);
}

/**
* 默认注册中心
*/
private static final Registry DEFAULT_REGISTRY = new EtcdRegistry();

/**
* 获取实例
*
* @param key
* @return
*/
public static Registry getInstance(String key) {
return SpiLoader.getInstance(Registry.class, key);
}

}
  • 修改ServiceProxy类
  • [ ] ```
    package com.lkj.yurpc.proxy;

    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.http.HttpRequest;
    import cn.hutool.http.HttpResponse;
    import com.lkj.yurpc.RpcApplication;
    import com.lkj.yurpc.config.RpcConfig;
    import com.lkj.yurpc.constant.RpcConstant;
    import com.lkj.yurpc.model.RpcRequest;
    import com.lkj.yurpc.model.RpcResponse;
    import com.lkj.yurpc.model.ServiceMetaInfo;
    import com.lkj.yurpc.registry.Registry;
    import com.lkj.yurpc.registry.RegistryFactory;
    import com.lkj.yurpc.serializer.Serializer;
    import com.lkj.yurpc.serializer.SerializerFactory;

    import java.io.IOException;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.util.List;

    /**

    • 服务代理(JDK 动态代理)
      *

      */
      public class ServiceProxy implements InvocationHandler {

      /**

      • 调用代理
        *
      • @return
      • @throws Throwable
        */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 指定序列化器
        final Serializer serializer = SerializerFactory.getInstance(RpcApplication.getRpcConfig().getSerializer());

        // 构造请求
        String serviceName = method.getDeclaringClass().getName();
        RpcRequest rpcRequest = RpcRequest.builder()

             .serviceName(serviceName)
             .methodName(method.getName())
             .parameterTypes(method.getParameterTypes())
             .args(args)
             .build();
        

        try {

         // 序列化
         byte[] bodyBytes = serializer.serialize(rpcRequest);
        
         // 从注册中心获取服务提供者请求地址
         RpcConfig rpcConfig = RpcApplication.getRpcConfig();
         Registry registry = RegistryFactory.getInstance(rpcConfig.getRegistryConfig().getRegistry());
         ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo();
         serviceMetaInfo.setServiceName(serviceName);
         serviceMetaInfo.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION);
         List<ServiceMetaInfo> serviceMetaInfoList = registry.serviceDiscovery(serviceMetaInfo.getServiceKey());
         if (CollUtil.isEmpty(serviceMetaInfoList)) {
             throw new RuntimeException("暂无服务地址");
         }
         ServiceMetaInfo selectedServiceMetaInfo = serviceMetaInfoList.get(0);
        
         // 发送请求
         try (HttpResponse httpResponse = HttpRequest.post(selectedServiceMetaInfo.getServiceAddress())
                 .body(bodyBytes)
                 .execute()) {
             byte[] result = httpResponse.bodyBytes();
             // 反序列化
             RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);
             return rpcResponse.getData();
         }
        

        } catch (IOException e) {

         e.printStackTrace();
        

        }

        return null;
        }
        }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    ## 注册中心优化

    ### 需求分析

    之前我们完成了基础的注册中心,能够注册和获取服务和节点信息。

    对于数据一致性、性能优化、高可用性、可扩展性还有很多优化空间。

    ### 注册中心优化

    #### 心跳检测和持续机制

    ##### 介绍

    心跳检测是一种用于检测系统是否正常工作的机制。通过定期发送心跳信号来检测目标系统的状态。

    ##### 方案设计

    实现心跳检测一般需要两个关键:定时、网络请求

    因为etcd自带了key过期机制,我们可以给节点注册信息一个生命倒计时,让节点定期续期,重置自己的倒计时。如果一直不重置,则删除。

    * 服务提供者向etcd注册自己的服务信息,并在注册时设置TTL
    * etcd在收到信息后,自动维护ttl,并在ttl过期时删除
    * 服务提供者定期请求etcd续签自己的注册信息,重写ttl

    ##### 开发实现

    * 给注册中心补充心跳检测方法

    /**

    • 心跳检测(服务端)
      */
      void heartBeat();
      1
      2
      3

      * 维护续期节点集合

      /**
    • 本机注册的节点 key 集合(用于维护续期)
      */
      private final Set localRegisterNodeKeySet = new HashSet<>();

      1
      2
      3

      * 实现hearBeat方法

      @Override
      public void heartBeat() {
      // 10 秒续签一次
      CronUtil.schedule(“/10 “, new Task() {

       @Override
       public void execute() {
           // 遍历本节点所有的 key
           for (String key : localRegisterNodeKeySet) {
               try {
                   List<KeyValue> keyValues = kvClient.get(ByteSequence.from(key, StandardCharsets.UTF_8))
                           .get()
                           .getKvs();
                   // 该节点已过期(需要重启节点才能重新注册)
                   if (CollUtil.isEmpty(keyValues)) {
                       continue;
                   }
                   // 节点未过期,重新注册(相当于续签)
                   KeyValue keyValue = keyValues.get(0);
                   String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
                   ServiceMetaInfo serviceMetaInfo = JSONUtil.toBean(value, ServiceMetaInfo.class);
                   register(serviceMetaInfo);
               } catch (Exception e) {
                   throw new RuntimeException(key + "续签失败", e);
               }
           }
       }
      

      });

      // 支持秒级别定时任务
      CronUtil.setMatchSecond(true);
      CronUtil.start();
      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      #### 服务节点下线机制

      当服务提供者节点宕机时,应该从注册中心移除已注册的节点。

      ##### 方案设计

      服务节点下线又分为:

      * 主动下线:服务提供者项目正常退出
      * 被动下线:异常退出

      被动下线已经实现,现在开发主动下线。

      利用JVM的ShutdownHook机制。

      ##### 开发实现

      * 完善destory

      public void destroy() {
      System.out.println(“当前节点下线”);
      // 下线节点
      // 遍历本节点所有的 key
      for (String key : localRegisterNodeKeySet) {
      try {

       kvClient.delete(ByteSequence.from(key, StandardCharsets.UTF_8)).get();
      

      } catch (Exception e) {

       throw new RuntimeException(key + "节点下线失败");
      

      }
      }

      // 释放资源
      if (kvClient != null) {
      kvClient.close();
      }
      if (client != null) {
      client.close();
      }
      }

1
2
3

* 在RpcApplication的init中,注册shutdown hook

public static void init(RpcConfig newRpcConfig) {
rpcConfig = newRpcConfig;
log.info(“rpc init, config = {}”, newRpcConfig.toString());
// 注册中心初始化
RegistryConfig registryConfig = rpcConfig.getRegistryConfig();
Registry registry = RegistryFactory.getInstance(registryConfig.getRegistry());
registry.init(registryConfig);
log.info(“registry init, config = {}”, registryConfig);

// 创建并注册 Shutdown Hook,JVM 退出时执行操作
Runtime.getRuntime().addShutdownHook(new Thread(registry::destroy));

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

## 自定义协议

### 需求分析

目前rpc框架,使用Vert.x的HttpServer作为服务提供者的服务器,底层使用的是http协议。

一般情况下,rpc框架注重性能,而HTTP协议中的头部信息,请求响应格式较重,会影响传输。

所以,我们需要自定义一套rpc协议,来实现更灵活的框架。

### 设计方案

分为两个核心部分:

* 自定义网络传输
* 自定义消息结构

#### 网络传输设计

目标:选择一个能够高性能通信的网络协议和传输方式

选择使用TCP协议完成网络传输,有更多的自主设计空间。

#### 消息结构设计

目标:用最少的空间传递需要的信息。

* 如何用最少空间:尽量使用轻量的类型,比如byte字节类型,同时每个数据凑到整个字节。
* 消息内部所需信息:

魔数:作用是安全校验,防止服务器处理非框架发送的无用信息

版本号:保证请求和响应的一致性

序列化方式:告知如何解析数据

类型:标识是请求还是响应

状态:记录响应结果

请求id:唯一标识某个请求

请求体:包含数据

请求体数据长度:保证完整的获取body内容信息

最终的消息结构设计如下图所示:

![image-20241023175713736](手写RPC框架/image-20241023175713736.png)

请求体信息总长17字节。在后续,需要有消息编码器和消息解码器,编码器先new一个空的Buffer缓冲区,然后按照顺序向缓冲区依次写入;解码器则依次读出。

### 开发设计

#### 消息结构

新建protocol包

* 新建歇息消息类,把消息头单独封装为一个内部类。

package com.lkj.yurpc.protocol;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**

  • 协议消息结构
    *

*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProtocolMessage {

/**
 * 消息头
 */
private Header header;

/**
 * 消息体(请求或响应对象)
 */
private T body;

/**
 * 协议消息头
 */
@Data
public static class Header {

    /**
     * 魔数,保证安全性
     */
    private byte magic;

    /**
     * 版本号
     */
    private byte version;

    /**
     * 序列化器
     */
    private byte serializer;

    /**
     * 消息类型(请求 / 响应)
     */
    private byte type;

    /**
     * 状态
     */
    private byte status;

    /**
     * 请求 id
     */
    private long requestId;

    /**
     * 消息体长度
     */
    private int bodyLength;
}

}

1
2
3

* 新建协议常量类

package com.lkj.yurpc.protocol;

/**

  • 协议常量
    *

    */
    public interface ProtocolConstant {

    /**

    • 消息头长度
      */
      int MESSAGE_HEADER_LENGTH = 17;

      /**

    • 协议魔数
      */
      byte PROTOCOL_MAGIC = 0x1;

      /**

    • 协议版本号
      */
      byte PROTOCOL_VERSION = 0x1;
      }
1
2
3

* 新建消息字段的枚举类,例如:协议状态枚举,暂时只定义成功、请求失败、响应失败三种枚举值

package com.lkj.yurpc.protocol;

import lombok.Getter;

/**

  • 协议消息的状态枚举

@Getter
public enum ProtocolMessageStatusEnum {

OK("ok", 20),
BAD_REQUEST("badRequest", 40),
BAD_RESPONSE("badResponse", 50);

private final String text;

private final int value;

ProtocolMessageStatusEnum(String text, int value) {
    this.text = text;
    this.value = value;
}

/**
 * 根据 value 获取枚举
 *
 * @param value
 * @return
 */
public static ProtocolMessageStatusEnum getEnumByValue(int value) {
    for (ProtocolMessageStatusEnum anEnum : ProtocolMessageStatusEnum.values()) {
        if (anEnum.value == value) {
            return anEnum;
        }
    }
    return null;
}

}

1
2
3

* 协议信息类型枚举,包括请求响应心跳等。


package com.lkj.yurpc.protocol;

import lombok.Getter;

/**

  • 协议消息的类型枚举
    *

    */
    @Getter
    public enum ProtocolMessageTypeEnum {

    REQUEST(0),
    RESPONSE(1),
    HEART_BEAT(2),
    OTHERS(3);

    private final int key;

    ProtocolMessageTypeEnum(int key) {

     this.key = key;
    

    }

    /**

    • 根据 key 获取枚举
      *
    • @param key
    • @return
      */
      public static ProtocolMessageTypeEnum getEnumByKey(int key) {
      for (ProtocolMessageTypeEnum anEnum : ProtocolMessageTypeEnum.values()) {
       if (anEnum.key == key) {
           return anEnum;
       }
      
      }
      return null;
      }
      }
      1
      2
      3

      * 协议消息的序列化器枚举

      package com.lkj.yurpc.protocol;

import cn.hutool.core.util.ObjectUtil;
import lombok.Getter;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**

@Getter
public enum ProtocolMessageSerializerEnum {

JDK(0, "jdk"),
JSON(1, "json"),
KRYO(2, "kryo"),
HESSIAN(3, "hessian");

private final int key;

private final String value;

ProtocolMessageSerializerEnum(int key, String value) {
    this.key = key;
    this.value = value;
}

/**
 * 获取值列表
 *
 * @return
 */
public static List<String> getValues() {
    return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}

/**
 * 根据 key 获取枚举
 *
 * @param key
 * @return
 */
public static ProtocolMessageSerializerEnum getEnumByKey(int key) {
    for (ProtocolMessageSerializerEnum anEnum : ProtocolMessageSerializerEnum.values()) {
        if (anEnum.key == key) {
            return anEnum;
        }
    }
    return null;
}


/**
 * 根据 value 获取枚举
 *
 * @param value
 * @return
 */
public static ProtocolMessageSerializerEnum getEnumByValue(String value) {
    if (ObjectUtil.isEmpty(value)) {
        return null;
    }
    for (ProtocolMessageSerializerEnum anEnum : ProtocolMessageSerializerEnum.values()) {
        if (anEnum.value.equals(value)) {
            return anEnum;
        }
    }
    return null;
}

}

1
2
3
4
5
6
7
8
9

#### 网络传输

新建server.tcp包,将所有TCP服务相关的代码放到该包中

* TCP服务器实现

新建VertcTcpServer类


package com.lkj.yurpc.server.tcp;

import io.vertx.core.Vertx;
import io.vertx.core.net.NetServer;
import lombok.extern.slf4j.Slf4j;

/**

  • Vertx TCP 服务器
    *

    */
    @Slf4j
    public class VertxTcpServer {

    public void doStart(int port) {

     // 创建 Vert.x 实例
     Vertx vertx = Vertx.vertx();
    
     // 创建 TCP 服务器
     NetServer server = vertx.createNetServer();
    
     // 处理请求
     server.connectHandler(new TcpServerHandler());
    
     // 启动 TCP 服务器并监听指定端口
     server.listen(port, result -> {
         if (result.succeeded()) {
             log.info("TCP server started on port " + port);
         } else {
             log.info("Failed to start TCP server: " + result.cause());
         }
     });
    

    }

    public static void main(String[] args) {

     new VertxTcpServer().doStart(8888);
    

    }
    }

1
2
3

* TCP客户端实现

package com.lkj.yurpc.server.tcp;

import cn.hutool.core.util.IdUtil;
import com.yupi.yurpc.RpcApplication;
import com.yupi.yurpc.model.RpcRequest;
import com.yupi.yurpc.model.RpcResponse;
import com.yupi.yurpc.model.ServiceMetaInfo;
import com.yupi.yurpc.protocol.*;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**

  • Vertx TCP 请求客户端
    *

    */
    public class VertxTcpClient {

    /**

    • 发送请求
      *
    • @param rpcRequest
    • @param serviceMetaInfo
    • @return
    • @throws InterruptedException
    • @throws ExecutionException
      */
      public static RpcResponse doRequest(RpcRequest rpcRequest, ServiceMetaInfo serviceMetaInfo) throws InterruptedException, ExecutionException {
      // 发送 TCP 请求
      Vertx vertx = Vertx.vertx();
      NetClient netClient = vertx.createNetClient();
      CompletableFuture responseFuture = new CompletableFuture<>();
      netClient.connect(serviceMetaInfo.getServicePort(), serviceMetaInfo.getServiceHost(),

           result -> {
               if (!result.succeeded()) {
                   System.err.println("Failed to connect to TCP server");
                   return;
               }
               NetSocket socket = result.result();
               // 发送数据
               // 构造消息
               ProtocolMessage<RpcRequest> protocolMessage = new ProtocolMessage<>();
               ProtocolMessage.Header header = new ProtocolMessage.Header();
               header.setMagic(ProtocolConstant.PROTOCOL_MAGIC);
               header.setVersion(ProtocolConstant.PROTOCOL_VERSION);
               header.setSerializer((byte) ProtocolMessageSerializerEnum.getEnumByValue(RpcApplication.getRpcConfig().getSerializer()).getKey());
               header.setType((byte) ProtocolMessageTypeEnum.REQUEST.getKey());
               // 生成全局请求 ID
               header.setRequestId(IdUtil.getSnowflakeNextId());
               protocolMessage.setHeader(header);
               protocolMessage.setBody(rpcRequest);
      
               // 编码请求
               try {
                   Buffer encodeBuffer = ProtocolMessageEncoder.encode(protocolMessage);
                   socket.write(encodeBuffer);
               } catch (IOException e) {
                   throw new RuntimeException("协议消息编码错误");
               }
      
               // 接收响应
               TcpBufferHandlerWrapper bufferHandlerWrapper = new TcpBufferHandlerWrapper(
                       buffer -> {
                           try {
                               ProtocolMessage<RpcResponse> rpcResponseProtocolMessage =
                                       (ProtocolMessage<RpcResponse>) ProtocolMessageDecoder.decode(buffer);
                               responseFuture.complete(rpcResponseProtocolMessage.getBody());
                           } catch (IOException e) {
                               throw new RuntimeException("协议消息解码错误");
                           }
                       }
               );
               socket.handler(bufferHandlerWrapper);
      
           });
      

      RpcResponse rpcResponse = responseFuture.get();
      // 记得关闭连接
      netClient.close();
      return rpcResponse;
      }
      }

1
2
3
4
5

#### 编码解码器

* 编码器

package com.lkj.yurpc.protocol;

import com.lkj.yurpc.serializer.Serializer;
import com.lkj.yurpc.serializer.SerializerFactory;
import io.vertx.core.buffer.Buffer;

import java.io.IOException;

/**

  • 协议消息编码器

public class ProtocolMessageEncoder {

/**
 * 编码
 *
 * @param protocolMessage
 * @return
 * @throws IOException
 */
public static Buffer encode(ProtocolMessage<?> protocolMessage) throws IOException {
    if (protocolMessage == null || protocolMessage.getHeader() == null) {
        return Buffer.buffer();
    }
    ProtocolMessage.Header header = protocolMessage.getHeader();
    // 依次向缓冲区写入字节
    Buffer buffer = Buffer.buffer();
    buffer.appendByte(header.getMagic());
    buffer.appendByte(header.getVersion());
    buffer.appendByte(header.getSerializer());
    buffer.appendByte(header.getType());
    buffer.appendByte(header.getStatus());
    buffer.appendLong(header.getRequestId());
    // 获取序列化器
    ProtocolMessageSerializerEnum serializerEnum = ProtocolMessageSerializerEnum.getEnumByKey(header.getSerializer());
    if (serializerEnum == null) {
        throw new RuntimeException("序列化协议不存在");
    }
    Serializer serializer = SerializerFactory.getInstance(serializerEnum.getValue());
    byte[] bodyBytes = serializer.serialize(protocolMessage.getBody());
    // 写入 body 长度和数据
    buffer.appendInt(bodyBytes.length);
    buffer.appendBytes(bodyBytes);
    return buffer;
}

}

1
2
3

* 解码器

package com.lkj.yurpc.protocol;

import com.lkj.yurpc.model.RpcRequest;
import com.lkj.yurpc.model.RpcResponse;
import com.lkj.yurpc.serializer.Serializer;
import com.lkj.yurpc.serializer.SerializerFactory;
import io.vertx.core.buffer.Buffer;

import java.io.IOException;

/**

  • 协议消息解码器

    */
    public class ProtocolMessageDecoder {

    /**

    • 解码
      *
    • @param buffer
    • @return
    • @throws IOException
      */
      public static ProtocolMessage<?> decode(Buffer buffer) throws IOException {
      // 分别从指定位置读出 Buffer
      ProtocolMessage.Header header = new ProtocolMessage.Header();
      byte magic = buffer.getByte(0);
      // 校验魔数
      if (magic != ProtocolConstant.PROTOCOL_MAGIC) {
       throw new RuntimeException("消息 magic 非法");
      
      }
      header.setMagic(magic);
      header.setVersion(buffer.getByte(1));
      header.setSerializer(buffer.getByte(2));
      header.setType(buffer.getByte(3));
      header.setStatus(buffer.getByte(4));
      header.setRequestId(buffer.getLong(5));
      header.setBodyLength(buffer.getInt(13));
      // 解决粘包问题,只读指定长度的数据
      byte[] bodyBytes = buffer.getBytes(17, 17 + header.getBodyLength());
      // 解析消息体
      ProtocolMessageSerializerEnum serializerEnum = ProtocolMessageSerializerEnum.getEnumByKey(header.getSerializer());
      if (serializerEnum == null) {
       throw new RuntimeException("序列化消息的协议不存在");
      
      }
      Serializer serializer = SerializerFactory.getInstance(serializerEnum.getValue());
      ProtocolMessageTypeEnum messageTypeEnum = ProtocolMessageTypeEnum.getEnumByKey(header.getType());
      if (messageTypeEnum == null) {
       throw new RuntimeException("序列化消息的类型不存在");
      
      }
      switch (messageTypeEnum) {
       case REQUEST:
           RpcRequest request = serializer.deserialize(bodyBytes, RpcRequest.class);
           return new ProtocolMessage<>(header, request);
       case RESPONSE:
           RpcResponse response = serializer.deserialize(bodyBytes, RpcResponse.class);
           return new ProtocolMessage<>(header, response);
       case HEART_BEAT:
       case OTHERS:
       default:
           throw new RuntimeException("暂不支持该消息类型");
      
      }
      }

}

1
2
3
4
5

#### 请求处理器

作用是接受请求,然后通过反射调用服务实现类。

package com.lkj.yurpc.server.tcp;

import com.lkj.yurpc.model.RpcRequest;
import com.lkj.yurpc.model.RpcResponse;
import com.lkj.yurpc.protocol.*;
import com.lkj.yurpc.registry.LocalRegistry;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;

import java.io.IOException;
import java.lang.reflect.Method;

/**

  • TCP 请求处理器

    */
    public class TcpServerHandler implements Handler {

    /**

    • 处理请求
      *
    • @param socket the event to handle
      */
      @Override
      public void handle(NetSocket socket) {
      TcpBufferHandlerWrapper bufferHandlerWrapper = new TcpBufferHandlerWrapper(buffer -> {

       // 接受请求,解码
       ProtocolMessage<RpcRequest> protocolMessage;
       try {
           protocolMessage = (ProtocolMessage<RpcRequest>) ProtocolMessageDecoder.decode(buffer);
       } catch (IOException e) {
           throw new RuntimeException("协议消息解码错误");
       }
       RpcRequest rpcRequest = protocolMessage.getBody();
       ProtocolMessage.Header header = protocolMessage.getHeader();
      
       // 处理请求
       // 构造响应结果对象
       RpcResponse rpcResponse = new RpcResponse();
       try {
           // 获取要调用的服务实现类,通过反射调用
           Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName());
           Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
           Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs());
           // 封装返回结果
           rpcResponse.setData(result);
           rpcResponse.setDataType(method.getReturnType());
           rpcResponse.setMessage("ok");
       } catch (Exception e) {
           e.printStackTrace();
           rpcResponse.setMessage(e.getMessage());
           rpcResponse.setException(e);
       }
      
       // 发送响应,编码
       header.setType((byte) ProtocolMessageTypeEnum.RESPONSE.getKey());
       header.setStatus((byte) ProtocolMessageStatusEnum.OK.getValue());
       ProtocolMessage<RpcResponse> responseProtocolMessage = new ProtocolMessage<>(header, rpcResponse);
       try {
           Buffer encode = ProtocolMessageEncoder.encode(responseProtocolMessage);
           socket.write(encode);
       } catch (IOException e) {
           throw new RuntimeException("协议消息编码错误");
       }
      

      });
      socket.handler(bufferHandlerWrapper);
      }

}

1
2
3

#### 请求发送

package com.lkj.yurpc.server.tcp;

import com.lkj.yurpc.protocol.ProtocolConstant;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser;

/**

  • TCP 消息处理器包装
  • 装饰者模式,使用 recordParser 对原有的 buffer 处理能力进行增强

    */
    public class TcpBufferHandlerWrapper implements Handler {

    /**

    • 解析器,用于解决半包、粘包问题
      */
      private final RecordParser recordParser;

      public TcpBufferHandlerWrapper(Handler bufferHandler) {
      recordParser = initRecordParser(bufferHandler);
      }

      @Override
      public void handle(Buffer buffer) {
      recordParser.handle(buffer);
      }

      /**

    • 初始化解析器
      *
    • @param bufferHandler
    • @return
      */
      private RecordParser initRecordParser(Handler bufferHandler) {
      // 构造 parser
      RecordParser parser = RecordParser.newFixed(ProtocolConstant.MESSAGE_HEADER_LENGTH);

      parser.setOutput(new Handler() {

       // 初始化
       int size = -1;
       // 一次完整的读取(头 + 体)
       Buffer resultBuffer = Buffer.buffer();
      
       @Override
       public void handle(Buffer buffer) {
           // 1. 每次循环,首先读取消息头
           if (-1 == size) {
               // 读取消息体长度
               size = buffer.getInt(13);
               parser.fixedSizeMode(size);
               // 写入头信息到结果
               resultBuffer.appendBuffer(buffer);
           } else {
               // 2. 然后读取消息体
               // 写入体信息到结果
               resultBuffer.appendBuffer(buffer);
               // 已拼接为完整 Buffer,执行处理
               bufferHandler.handle(resultBuffer);
               // 重置一轮
               parser.fixedSizeMode(ProtocolConstant.MESSAGE_HEADER_LENGTH);
               size = -1;
               resultBuffer = Buffer.buffer();
           }
       }
      

      });

      return parser;
      }
      }

1
2
3
4
5
6
7

### 粘包半包问题解决

#### 概念

加入客户端连续两次发送信息:

// 第一次
Hello, server!Hello, server!Hello, server!Hello, server!
// 第二次
Hello, server!Hello, server!Hello, server!Hello, server!

1
2
3
4
5

单服务器收到的可能是:

* 每次收到的数据更少了,这种情况叫做半包

// 第一次
Hello, server!Hello, server!
// 第二次
Hello, server!Hello, server!Hello, server!

1
2
3

* 每次收到的数据更多了,这种情况叫做粘包

// 第三次
Hello, server!Hello, server!Hello, server!Hello, server!Hello, server!

1
2
3
4
5
6
7

#### 如何解决

* 解决半包

核心思路:在消息体中设置请求体长度,服务端接收到时,判断每次信息长度是否符合预期,不完整就不读。

if (buffer == null || buffer.length() == 0) {
throw new RuntimeException(“消息 buffer 为空”);
}
if (buffer.getBytes().length < ProtocolConstant.MESSAGE_HEADER_LENGTH) {
throw new RuntimeException(“出现了半包问题”);
}

1
2
3
4
5

* 解决粘包

核心思路也是类似的:每次只读取指定长度的数据,超过长度的留着下一次接收到消息时再读取。

// 解决粘包问题,只读指定长度的数据
byte[] bodyBytes = buffer.getBytes(17, 17 + header.getBodyLength());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

## 负载均衡

### 需求分析

现在 RPC 框架已经可以从注册中心获取到服务提供者的注册信息了,同一个服务可能会有多个服务提供者,但是目前消费者始终读取了第一个服务提供者节点发起调用,不仅会增大单个节点的压力,而且没有利用好其他节点的资源。

我们完全可以从服务提供者节点中,选择一个服务提供者发起请求,而不是每次都请求同一个服务提供者,这个操作就叫做 **负载均衡**。

### 负载均衡

#### 定义

1)何为负载?可以把负载理解为要处理的工作和压力,比如网络请求、事务、数据处理任务等。

2)何为均衡?把工作和压力平均地分配给多个工作者,从而分摊每个工作者的压力,保证大家正常工作。

#### 常见负载均衡算法

* 轮询:按照循坏顺序请求分配给每个服务器
* 随机:随机选择
* 加权轮询:根据服务器的性能或权重分配请求。
* 加权随机
* 最小连接数
* IP Hash

### 开发实现

#### 多种负载器实现

在RPC项目中新建loadbalancer包

* 编写通用接口

package com.lkj.yurpc.loadbalancer;

import com.lkj.yurpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;

/**

  • 负载均衡器(消费端使用)
    *

    */
    public interface LoadBalancer {

    /**

    • 选择服务调用
      *
    • @param requestParams 请求参数
    • @param serviceMetaInfoList 可用服务列表
    • @return
      */
      ServiceMetaInfo select(Map requestParams, List serviceMetaInfoList);
      }
1
2
3

* 轮询负载均衡

package com.lkj.yurpc.loadbalancer;

import com.lkj.yurpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**

  • 轮询负载均衡器

    */
    public class RoundRobinLoadBalancer implements LoadBalancer {

    /**

    • 当前轮询的下标
      */
      private final AtomicInteger currentIndex = new AtomicInteger(0);

      @Override
      public ServiceMetaInfo select(Map requestParams, List serviceMetaInfoList) {
      if (serviceMetaInfoList.isEmpty()) {

       return null;
      

      }
      // 只有一个服务,无需轮询
      int size = serviceMetaInfoList.size();
      if (size == 1) {

       return serviceMetaInfoList.get(0);
      

      }
      // 取模算法轮询
      int index = currentIndex.getAndIncrement() % size;
      return serviceMetaInfoList.get(index);
      }
      }

1
2
3

* 随机负载均衡

package com.lkj.yurpc.loadbalancer;

import com.lkj.yurpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.Random;

/**

  • 随机负载均衡器

    */
    public class RandomLoadBalancer implements LoadBalancer {

    private final Random random = new Random();

    @Override
    public ServiceMetaInfo select(Map requestParams, List serviceMetaInfoList) {

     int size = serviceMetaInfoList.size();
     if (size == 0) {
         return null;
     }
     // 只有 1 个服务,不用随机
     if (size == 1) {
         return serviceMetaInfoList.get(0);
     }
     return serviceMetaInfoList.get(random.nextInt(size));
    

    }
    }

    1
    2
    3

    * 实现一致性 Hash 负载均衡器

    package com.lkj.yurpc.loadbalancer;

import com.lkj.yurpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**

  • 一致性哈希负载均衡器
    *

    */
    public class ConsistentHashLoadBalancer implements LoadBalancer {

    /**

    • 一致性 Hash 环,存放虚拟节点
      */
      private final TreeMap virtualNodes = new TreeMap<>();

      /**

    • 虚拟节点数
      */
      private static final int VIRTUAL_NODE_NUM = 100;

      @Override
      public ServiceMetaInfo select(Map requestParams, List serviceMetaInfoList) {
      if (serviceMetaInfoList.isEmpty()) {

       return null;
      

      }

      // 构建虚拟节点环
      for (ServiceMetaInfo serviceMetaInfo : serviceMetaInfoList) {

       for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
           int hash = getHash(serviceMetaInfo.getServiceAddress() + "#" + i);
           virtualNodes.put(hash, serviceMetaInfo);
       }
      

      }

      // 获取调用请求的 hash 值
      int hash = getHash(requestParams);

      // 选择最接近且大于等于调用请求 hash 值的虚拟节点
      Map.Entry entry = virtualNodes.ceilingEntry(hash);
      if (entry == null) {

       // 如果没有大于等于调用请求 hash 值的虚拟节点,则返回环首部的节点
       entry = virtualNodes.firstEntry();
      

      }
      return entry.getValue();
      }

/**
 * Hash 算法,可自行实现
 *
 * @param key
 * @return
 */
private int getHash(Object key) {
    return key.hashCode();
}

}

1
2
3
4
5
6
7

#### 支持配置和扩展

一个成熟的 RPC 框架可能会支持多个负载均衡器,像序列化器和注册中心一样,我们的需求是,让开发者能够填写配置来指定使用的负载均衡器,并且支持自定义负载均衡器,让框架更易用、更利于扩展。

* 负载均衡器常量

package com.lkj.yurpc.loadbalancer;

/**

  • 负载均衡器键名常量
    *

    */
    public interface LoadBalancerKeys {

    /**

    • 轮询
      */
      String ROUND_ROBIN = “roundRobin”;

      String RANDOM = “random”;

      String CONSISTENT_HASH = “consistentHash”;

}

1
2
3

* 使用工厂模式,支持根据key从SPI获取负载均衡器对象实例

package com.lkj.yurpc.loadbalancer;

import com.lkj.yurpc.spi.SpiLoader;

/**

  • 负载均衡器工厂(工厂模式,用于获取负载均衡器对象)
    *

    */
    public class LoadBalancerFactory {

    static {

     SpiLoader.load(LoadBalancer.class);
    

    }

    /**

    • 默认负载均衡器
      */
      private static final LoadBalancer DEFAULT_LOAD_BALANCER = new RoundRobinLoadBalancer();

      /**

    • 获取实例
      *
    • @param key
    • @return
      */
      public static LoadBalancer getInstance(String key) {
      return SpiLoader.getInstance(LoadBalancer.class, key);
      }

}

1
2
3

* 为RPC全局配置新增

/**

 * 负载均衡器
 */
private String loadBalancer = LoadBalancerKeys.ROUND_ROBIN;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

## 重试机制

### 需求分析

目前,如果使用 RPC 框架的服务消费者调用接口失败,就会直接报错。

调用接口失败可能有很多原因,有时可能是服务提供者返回了错误,但有时可能只是网络不稳定或服务提供者重启等临时性问题。这种情况下,我们可能更希望服务消费者拥有自动重试的能力,提高系统的可用性。

### 设计方案

#### 重试条件

当由于网络等异常情况发生时,触发重试。

#### 重试时间

* 固定重试间隔:在每次重试之间使用固定的时间间隔
* 指数退避重试:在每次失败后,重试的时间间隔会以指数级增加,以避免请求过于密集。
* 随机延迟重试:在每次重试之间使用随机的时间间隔,以避免请求的同时发生。
* 可变延迟重试:这种策略更 “高级” 了,根据先前重试的成功或失败情况,动态调整下一次重试的延迟时间。比如,根据前一次的响应时间调整下一次重试的等待时间。

#### 停止重试

一般来说,重试次数是有上限的,否则随着报错的增多,系统同时发生的重试也会越来越多,造成雪崩。

主流的停止重试策略有:

1)最大尝试次数:一般重试当达到最大次数时不再重试。

2)超时停止:重试达到最大时间的时候,停止重试。

#### 重试工作

需要注意的是,当重试次数超过上限时,往往还要进行其他的操作,比如:

1. 通知告警:让开发者人工介入
2. 降级容错:改为调用其他接口、或者执行其他操作

### 开发实现

* 编写重试策略通用接口

package com.lkj.yurpc.fault.retry;

import com.lkj.yurpc.model.RpcResponse;

import java.util.concurrent.Callable;

/**

  • 重试策略
    *

    */
    public interface RetryStrategy {

    /**

    • 重试
      *
    • @param callable
    • @return
    • @throws Exception
      */
      RpcResponse doRetry(Callable callable) throws Exception;
      }
1
2
3

* 不重试策略实现

package com.lkj.yurpc.fault.retry;

import com.lkj.yurpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;

/**

  • 不重试 - 重试策略
    *

    */
    @Slf4j
    public class NoRetryStrategy implements RetryStrategy {

    /**

    • 重试
      *
    • @param callable
    • @return
    • @throws Exception
      */
      public RpcResponse doRetry(Callable callable) throws Exception {
      return callable.call();
      }

}

1
2
3

* 固定重试间隔策略

package com.lkj.yurpc.fault.retry;

import com.github.rholder.retry.*;
import com.lkj.yurpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**

  • 固定时间间隔 - 重试策略
    *

    */
    @Slf4j
    public class FixedIntervalRetryStrategy implements RetryStrategy {

    /**

    • 重试
      *
    • @param callable
    • @return
    • @throws ExecutionException
    • @throws RetryException
      */
      public RpcResponse doRetry(Callable callable) throws ExecutionException, RetryException {
      Retryer retryer = RetryerBuilder.newBuilder()
           .retryIfExceptionOfType(Exception.class)
           .withWaitStrategy(WaitStrategies.fixedWait(3L, TimeUnit.SECONDS))
           .withStopStrategy(StopStrategies.stopAfterAttempt(3))
           .withRetryListener(new RetryListener() {
               @Override
               public <V> void onRetry(Attempt<V> attempt) {
                   log.info("重试次数 {}", attempt.getAttemptNumber());
               }
           })
           .build();
      
      return retryer.call(callable);
      }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上述代码中,重试策略如下:

- 重试条件:使用 retryIfExceptionOfType 方法指定当出现 Exception 异常时重试。
- 重试等待策略:使用 withWaitStrategy 方法指定策略,选择 fixedWait 固定时间间隔策略。
- 重试停止策略:使用 withStopStrategy 方法指定策略,选择 stopAfterAttempt 超过最大重试次数停止。
- 重试工作:使用 withRetryListener 监听重试,每次重试时,除了再次执行任务外,还能够打印当前的重试次数。

#### 支持配置和扩展重试策略

需求是,让开发者能够填写配置来指定使用的重试策略,并且支持自定义重试策略,让框架更易用、更利于扩展。

要实现这点,开发方式和序列化器、注册中心、负载均衡器都是一样的,都可以使用工厂创建对象、使用 SPI 动态加载自定义的注册中心。

* 常量

package com.lkj.yurpc.fault.retry;

/**

  • 重试策略键名常量
    *

    */
    public interface RetryStrategyKeys {

    /**

    • 不重试
      */
      String NO = “no”;

      /**

    • 固定时间间隔
      */
      String FIXED_INTERVAL = “fixedInterval”;

}

1
2
3

* 使用工厂模式,根据key从SP获取重试策略对象实例

package com.lkj.yurpc.fault.retry;

import com.lkj.yurpc.spi.SpiLoader;

/**

  • 重试策略工厂(用于获取重试器对象)
    *

    */
    public class RetryStrategyFactory {

    static {

     SpiLoader.load(RetryStrategy.class);
    

    }

    /**

    • 默认重试器
      */
      private static final RetryStrategy DEFAULT_RETRY_STRATEGY = new NoRetryStrategy();

      /**

    • 获取实例
      *
    • @param key
    • @return
      */
      public static RetryStrategy getInstance(String key) {
      return SpiLoader.getInstance(RetryStrategy.class, key);
      }

}

1
2
3

* 应用重试功能

// 使用重试机制
RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
RpcResponse rpcResponse = retryStrategy.doRetry(() ->
VertxTcpClient.doRequest(rpcRequest, selectedServiceMetaInfo)
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

## 容错机制

### 需求分析

当系统出现异常时,我们仍要保证可以通过一定策略稳步运行。

### 设计方案

#### 容错机制

容错是指系统在出现异常情况时,可以通过一定的策略保证系统仍然稳定运行,从而提高系统的可靠性和健壮性。1800771338183979009_0.5649165956367674

在分布式系统中,容错机制尤为重要,因为分布式系统中的各个组件都可能存在网络故障、节点故障等各种异常情况。要顾全大局,尽可能消除偶发 / 单点故障对系统带来的整体影响。

打个比方,将分布式系统类比为一家公司,如果公司某个优秀员工请假了,需要 “触发容错”,让另一个普通员工顶上,这本质上是容错机制的一种 **降级** 策略。

##### 容错策略

容错策略有很多种,常用的容错策略主要是以下几个:

1)Fail-Over 故障转移:一次调用失败后,切换一个其他节点再次进行调用,也算是一种重试。1800771338183979009_0.0852483407241389

2)Fail-Back 失败自动恢复:系统的某个功能出现调用失败或错误时,通过其他的方法,恢复该功能的正常。可以理解为降级,比如重试、调用其他服务等。

3)Fail-Safe 静默处理:系统出现部分非重要功能的异常时,直接忽略掉,不做任何处理,就像错误没有发生过一样。

4)Fail-Fast 快速失败:系统出现调用错误时,立刻报错,交给外层调用方处理。

##### 容错实现方式

容错其实是个比较广泛的概念,除了上面几种策略外,很多技术都可以起到容错的作用。

比如:

1)重试:重试本质上也是一种容错的降级策略,系统错误后再试一次。

2)限流:当系统压力过大、已经出现部分错误时,通过限制执行操作(接受请求)的频率或数量,对系统进行保护。

3)降级:系统出现错误后,改为执行其他更稳定可用的操作。也可以叫做 “兜底” 或 “有损服务”,这种方式的本质是:即使牺牲一定的服务质量,也要保证系统的部分功能可用,保证基本的功能需求得到满足。

4)熔断:系统出现故障或异常时,暂时中断对该服务的请求,而是执行其他操作,以避免连锁故障。

5)超时控制:如果请求或操作长时间没处理完成,就进行中断,防止阻塞和资源占用。

注意,在实际项目中,根据对系统可靠性的需求,我们通常会结合多种策略或方法实现容错机制。

### 开发实现

* 通用接口

package com.lkj.yurpc.fault.tolerant;

import com.lkj.yurpc.model.RpcResponse;

import java.util.Map;

/**

  • 容错策略
    *

    */
    public interface TolerantStrategy {

    /**

    • 容错
      *
    • @param context 上下文,用于传递数据
    • @param e 异常
    • @return
      */
      RpcResponse doTolerant(Map context, Exception e);
      }
      1
      2
      3
      4
      5

      * 快速失败容错策略实现

      遇到异常后,将异常再次抛出,交给外层实现

      package com.lkj.yurpc.fault.tolerant;

import com.lkj.yurpc.model.RpcResponse;

import java.util.Map;

/**

  • 快速失败 - 容错策略(立刻通知外层调用方)
    *

    */
    public class FailFastTolerantStrategy implements TolerantStrategy {

    @Override
    public RpcResponse doTolerant(Map context, Exception e) {

     throw new RuntimeException("服务报错", e);
    

    }
    }

1
2
3
4
5

* 静默处理容错策略实现

遇到异常后,记录一条日志,然后正常返回一个响应对象

package com.lkj.yurpc.fault.tolerant;

import com.lkj.yurpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

/**

  • 静默处理异常 - 容错策略
    *

    */
    @Slf4j
    public class FailSafeTolerantStrategy implements TolerantStrategy {

    @Override
    public RpcResponse doTolerant(Map context, Exception e) {

     log.info("静默处理异常", e);
     return new RpcResponse();
    

    }
    }

1
2
3
4
5

#### 配置和扩展容错策略

* 常量

package com.lkj.yurpc.fault.tolerant;

/**

  • 容错策略键名常量
    *

    */
    public interface TolerantStrategyKeys {

    /**

    • 故障恢复
      */
      // String FAIL_BACK = “failBack”;

      /**

    • 快速失败
      */
      String FAIL_FAST = “failFast”;

      /**

    • 故障转移
      */
      // String FAIL_OVER = “failOver”;

      /**

    • 静默处理
      */
      String FAIL_SAFE = “failSafe”;

}

1
2
3

* 使用工厂模式

package com.lkj.yurpc.fault.tolerant;

import com.lkj.yurpc.spi.SpiLoader;

/**

  • 容错策略工厂(工厂模式,用于获取容错策略对象)
    *

    */
    public class TolerantStrategyFactory {

    static {

     SpiLoader.load(TolerantStrategy.class);
    

    }

    /**

    • 默认容错策略
      */
      private static final TolerantStrategy DEFAULT_RETRY_STRATEGY = new FailFastTolerantStrategy();

      /**

    • 获取实例
      *
    • @param key
    • @return
      */
      public static TolerantStrategy getInstance(String key) {
      return SpiLoader.getInstance(TolerantStrategy.class, key);
      }

}

1
2
3

* 应用

// rpc 请求
// 使用重试机制
RpcResponse rpcResponse;
try {
RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
rpcResponse = retryStrategy.doRetry(() ->
VertxTcpClient.doRequest(rpcRequest, selectedServiceMetaInfo)
);
} catch (Exception e) {
// 容错机制
TolerantStrategy tolerantStrategy = TolerantStrategyFactory.getInstance(rpcConfig.getTolerantStrategy());
rpcResponse = tolerantStrategy.doTolerant(null, e);
}
return rpcResponse.getData();

```

智能面试刷题平台项目

项目介绍

于 Next.js 服务端渲染 + Spring Boot + Redis + MySQL + Elasticsearch 的 面试刷题平台。

管理员可以创建题库、题目和题解,并批量关联题目到题库;用户可以注册登录、分词检索题目、在线刷题并查看刷题记录日历等。

采用了一些企业级的新技术,比如使用数据库连接池、热 Key 探测、缓存、高级数据结构来提升性能。通过流量控制、熔断、动态 IP 黑白名单过滤、同端登录冲突检测、分级反爬虫策略来提升系统和内容的安全性。

项目结构设计图:

image-20241005214021499

技术选型

后端

  • Java Spring Boot 框架 + Maven 多模块构建
  • MySQL 数据库 + MyBatis-Plus 框架 + MyBatis X
  • Redis 分布式缓存 + Caffeine 本地缓存
  • Redission 分布式锁 + BitMap + BloomFilter
  • ⭐️ Elasticsearch 搜索引擎
  • ⭐️ Druid 数据库连接池 + 并发编程
  • ⭐️ Sa-Token 权限控制
  • ⭐️ HotKey 热点探测
  • ⭐️ Sentinel 流量控制
  • ⭐️ Nacos 配置中心
  • ⭐️ 多角度项目优化:性能、安全性、可用性

前端

  • React 18 框架
  • ⭐️ Next.js 服务端渲染
  • ⭐️ Redux 状态管理
  • Ant Design 组件库
  • 富文本编辑器组件
  • ⭐️ 前端工程化:ESLint + Prettier + TypeScript
  • ⭐️ OpenAPI 前端代码生成

实习

java

字符串

String 类提供两种查找字符串的方法

1
2
indexOf()和lastIndexOf()
str.indexOf(substr)
1
charAt()返回指定索引的字符
1
2
3
4
5
6
str.substring(int beginIndex)
str.substring(int beginIndex,int endIndex)
str.trim()去除空格
str.replace(char oldChar,char newChar)//字符替换
str.startsWith(String prefix)//判断是否以指定字符串开头
str.endsWith(String suffix)//判断是否以指定字符串结尾

判断字符串是否相等

1
2
str.equals(String s)//区分大小写
str.equalsIgnoreCase(String s) //不区分大小写
1
2
3
compareTo()//按字典顺序比较字符串
toLowerCase()
toUpperCase()

字符串分割

1
2
str.split(String sign )//sign为分隔符
str.split(String sign,int limit)//limit为拆分次数

类和对象

==与equals()

1
==是比较地址,equals比较内容

对象销毁

1
2
3
对象引用超过作用范围
对象被置为null
以上情况会被是我垃圾自动回收

包装类

Integer

1
2
3
4
Integer a=new Integer(7);
Integer a=new Integer("7");
Integer.MAX_VALUE;
Integer.MIN_VALUE;

Web前端

HTML

块元素与行内元素

超链接

image-20240518162207184

div 组织块,无实际内容

快捷

1
.content(内容名称)

image-20240518162421361

span组织行内元素

表单

image-20240518163015042

image-20240518163027103

1
2
placeholder输入内容自动消失
而value会自动填写

input标签type

image-20240518163635394

要实现单选,需要加一个name属性,同时name名称必须相同

image-20240518163810407

image-20240518163857100

1
label中的for元素是与input绑定,与ID对应,所以不适用于单选

image-20240518164221322

form中的action就是规定提交到哪一个url

CSS

CSS

image-20240518164341961

1为直接放入html标签

2为放入头部

3为放入一个专门的css文件夹

image-20240518164626162

优先级

image-20240518164648874

CSS选择器

image-20240518164749712

元素选择器

image-20240518165213273

image-20240518165222108

类选择器

image-20240518165236305

image-20240518165243648

class控制类

ID选择器

image-20240518165440962

image-20240518165453638

创建包含id的快捷方式

1
2
#+id
eg:h4#header

通用选择器

image-20240518165833640

CSS常用属性

复合属性

image-20240518170343414

行内元素,块内元素,行内块元素可以相互转换

利用display

盒子模型

image-20240518173728960

image-20240518174152486

1
borde-width遵循上右下左原则

浮动

image-20240518175601327

清除浮动:overflow

image-20240518175926141

定位

image-20240518181229357

相对定位

image-20240518181710121

JS

简介

image-20240518182023822

JS数据与基本变量

image-20240518184109001

const是常量;var是变量,作用于函数作用域;let是变量,作用于块级作用域。let更加安全灵活。

JS控制语句

JS函数

image-20240518185435220

JS事件

image-20240518190057898

image-20240518190508610

image-20240518191313600

JS DOM

image-20240518192132602

image-20240518192658972

image-20240518192727355

image-20240518192740841

响应式布局

image-20240518214739882

image-20240518214842592

FLEX布局

image-20240518215336502

微信公众号开发

image-20240518220619210

image-20240518220708758

微信web开发者平台

KND项目说明

项目内容

项目计划完成一个网页界面,所实现功能为在原有基础上,当下拉框选择了前面部分的信息后,后面的电缆信息会自动给出,同时后面信息在自动给出的基础上,也可下拉调整。

项目实现思路

对于整体项目实现,主要难度体现在后面电缆信息自动给出部分。我所采取的解决办法为:建立表名为“约束”的表,在对于后面电缆信息的每一栏,采取数据库查询,然后展示查询内容,所采取的查询原理为,首先在表“约束”中查询有无和前面信息都匹配的元素,然后UNION ALL该栏中本该展示的元素,这样即可实现对于匹配的信息优先给出,又可自行选择的效果。

以下为一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
--反馈电缆_X轴
create procedure 反馈电缆_X轴电缆工艺图_query
@系统型号 varchar(30),
@NC软件版本 varchar(30),
@PLC软件版本 varchar(30),
@指令脉冲 varchar(5),
@刀架上拉电阻 varchar(5),
@电源单元 varchar(30),
@配套资料 varchar(5),
@NC面板 varchar(5),
@X轴驱动 varchar(30),
@X轴电机 varchar(30),
@Y轴驱动 varchar(30),
@Y轴电机 varchar(30),
@Z轴驱动 varchar(30),
@Z轴电机 varchar(30),
@主轴驱动1 varchar(30),
@主轴电机1 varchar(30),
@附加面板 varchar(30),
@隔离变压器 varchar(30),
@外置手轮 varchar(30),
@制动电阻 varchar(30),
@IO模块 varchar(30),
@资料 varchar(30)
AS
select 反馈电缆_X轴电缆工艺图 from 约束 where 系统型号=@系统型号 and NC软件版本=@NC软件版本 and PLC软件版本=@PLC软件版本
and 指令脉冲=@指令脉冲 and 刀架上拉电阻=@刀架上拉电阻 and 电源单元= @电源单元 and 配套资料=@配套资料 and
NC面板= @NC面板 and X轴驱动=@X轴驱动 and X轴电机=@X轴电机 and Y轴驱动=@Y轴驱动 and Y轴电机=@Y轴电机 and
Z轴驱动=@Z轴驱动 and Z轴电机=@Z轴电机 and 主轴驱动1=@主轴驱动1 and 主轴电机1=@主轴电机1 and 附加面板=@附加面板 and
隔离变压器=@隔离变压器 and 外置手轮=@外置手轮 and 制动电阻=@制动电阻 and IO模块=@IO模块 and 资料=@资料
UNION ALL
select 电缆工艺图 from 反馈电缆_X轴
GO

以反馈电缆_X轴为例,对于后面需要实现的每一项,都编写了对应的查询函数来实现。

项目具体实现说明

以下是完成工作介绍。

数据库部分

首先是对于所有表项的创建,文件名为:

1
创建.sql

然后进行了一些测试元素的插入,文件名为:

1
插入.sql

对于表约束的创建,文件名为:

1
约束.sql

同时创建了表记录每次插入的一组信息,文件名为:

1
记录.sql

因为前面部分,如系统部分,驱动部分,选择件部分的下拉框显示,在前端绑定即可实现,所以没有编写sql函数。

而对于后面的部分,各种电缆部分,因为是被选择部分,所以需要运用数据库查询语句,文件夹名为查询,其中对应的sql文件即为对应的查询语句。

前端部分

因为技术和时间原因,前端部分目前实现了系统部分,驱动部分与选择件部分的展示和数据库连接。

在KND文件下,文件

1
knd.qspx

即为对应的前端界面,其余为测试部分。

未实现部分

在整体项目中,后面部分的前端展示未实现,但是对应的查询语句已经完成。

计算机设计大赛

微课与教学辅助赛道介绍

微课与教学辅助赛道是中国大学生计算机设计大赛中的一个重要类别,该类别的参赛作品主要是针对教学辅助领域的应用和创新。

参赛作品的形式主要包括以下几种:

微课视频:参赛者可以根据自己的教学经验和所学专业知识,制作适合初学者学习的微课视频。该类作品要求内容丰富、表达清晰、形式新颖,同时需要具有一定的教学实用性和可行性。

教学辅助工具:参赛者可以自主开发基于计算机技术的教学辅助工具,如教学管理系统、在线作业系统、学生评价系统等。该类作品要求具有一定的实用性和创新性,能够为教师和学生提供便捷的教学辅助服务。

教育游戏:参赛者可以开发基于计算机技术的教育游戏,旨在通过游戏化的方式提高学生对知识的兴趣和学习效果。该类作品要求游戏性强、教育性强,能够帮助学生快乐地学习。

教学资源共享平台:参赛者可以开发教学资源共享平台,旨在为教师和学生提供便捷的资源共享服务。该类作品要求平台功能完善、资源丰富,能够满足教学需求。

参赛作品的形式不限于上述几种,只要能够创新应用计算机技术为教学服务,满足比赛要求即可。

1
注:你们做的应该是属于微课视频类,下面着重介绍一下微课视频类。

微课视频

微课视频是中国大学生计算机设计大赛中微课与教学辅助赛道的一个参赛作品形式。微课视频主要是指通过视频方式来呈现教学内容,让学生通过视听学习的方式获取知识。微课视频的表现形式可以有以下几种:

1
2
3
4
5
1.	PPT课件+录音:这种形式主要是将PPT课件和讲解录音结合起来,通过录音对PPT课件进行讲解。这种形式操作简单,制作成本较低,适合教学内容相对简单的课程。
2. 2D动画:这种形式是通过制作2D动画来呈现教学内容,具有生动形象、易于理解的优点。但是该形式的制作难度较高,需要具备一定的动画制作技术和经验。
3. 白板演示:这种形式是通过屏幕录制的方式,将教师在白板上的演示和讲解录制下来,并配以文字说明和动画效果。该形式适用于教学内容较为复杂的课程。
4. 视频课程:这种形式是将教学内容拍摄成视频,并加入讲解和演示,呈现给学生。该形式具有视觉冲击力强、学生易于接受的优点,但是制作成本较高。
5. MOOC课程:这种形式是以互联网为平台,将教学内容制作成网上课程,并配以在线测试和交互式学习。该形式适用于远程教育和大规模在线课程的场景。

除了以上几种形式外,还有一些其他的微课视频表现形式,例如模拟演示、VR课程等,参赛者可以根据自己的创新想法和实际情况选择适合自己的表现形式。

中国大学生计算机设计大赛微课与教学辅助赛道的国家级奖项获得者形式多种多样,没有一种形式是绝对更容易获奖的。根据历届大赛的获奖情况来看,微课赛道的国家级奖项获得者包括了视频课件、虚拟实验、在线答疑等不同类型的作品。不同类型的作品在获奖的比重上并没有明显的差别,获奖的关键还是在作品的设计创新性、实用性、技术难度等方面,同时还需要具有良好的学术论证和实际应用价值。

1
注意:这里设计创新性肯定是要有的,实用性应该都具备,我认为重点还应该体现一些技术难度。

因此,无论是哪种形式的作品,只要具备以上的优秀特点,就有可能获得国家级奖项。参赛者应当注重作品的创新性、实用性、技术难度等方面的表现,以及充分考虑作品的实际应用和市场前景,同时要注意作品的语言表达和呈现效果。

获奖作品介绍

微课类国奖介绍

以下是部分往年微课类赛道中获得国奖一等奖的作品,仅供参考:

  • 《基于视觉的工业机器人在线编程平台设计与实现》

    1
    2
    3
    《基于视觉的工业机器人在线编程平台设计与实现》是2019年中国大学生计算机设计大赛中微课类赛道的一等奖获得者之一。该作品的主要内容是基于机器人视觉技术,设计并实现了一种工业机器人在线编程平台。该平台可以通过摄像头获取机器人工作区域的实时图像,通过人机交互的方式,实现对机器人的在线编程,支持多种编程方式和多种机器人品牌。

    该作品在创新点方面,采用了基于视觉的机器人编程方式,通过人机交互的方式,实现了对机器人的在线编程,避免了传统编程方式的复杂性和难度。同时,该平台支持多种编程方式和多种机器人品牌,提高了平台的适用性和实用性。
  • 《基于自然语言处理的图像生成技术》、《智慧图书馆新一代管理系统》

    1
    2
    3
    4
    《基于自然语言处理的图像生成技术》是2018年中国大学生计算机设计大赛中微课类赛道的一等奖获得者之一。该作品的主要内容是基于自然语言处理技术,实现了一种可以根据输入的文字描述生成对应图像的技术。

    该作品的创新点在于采用了自然语言处理技术,将自然语言描述转换为图像生成的过程中,通过使用卷积神经网络和对抗生成网络等先进技术,提高了生成图像的质量和逼真度。同时,该作品还将生成的图像用于图像检索和图像识别等领域,扩展了应用场景。

    1
    2
    3
    《智慧图书馆新一代管理系统》是2019年中国大学生计算机设计大赛中微课类赛道的一等奖获得者之一。该作品的主要内容是基于云计算和物联网技术,开发了一种可以实现智能化图书馆管理的系统。

    该作品的创新点在于采用了多种现代化技术,如基于RFID的智能借还书系统、人脸识别技术、基于云计算的图书馆管理系统等,实现了对图书借还、馆藏管理、查询和统计等方面的全面管理。同时,该系统还支持多语言版本,能够为外籍学生提供更加便捷的服务。
  • 《基于深度学习的脸部表情识别教学软件设计与实现》、《大数据驱动的中小学网络安全教育平台》

    1
    2
    3
    4
    5
    6
    7
    《基于深度学习的脸部表情识别教学软件设计与实现》是2018年中国大学生计算机设计大赛微课类赛道的一等奖获得者之一。该作品的主要内容是基于深度学习技术,开发了一款可以实现脸部表情识别的教学软件。

    该软件可以通过摄像头实时捕捉人脸图像,并通过深度学习算法对脸部表情进行分类和识别。同时,该软件还支持多种教学场景,如教室授课、在线教学、学生考试等,为教师和学生提供了更加便捷的教学工具。

    该作品的创新点在于采用了最新的深度学习技术,通过大量数据的训练和优化,实现了对脸部表情的高精度识别。同时,该软件还具有良好的用户体验和交互性能,能够满足不同用户的需求。


    1
    2
    3
    4
    《大数据驱动的中小学网络安全教育平台》是2018年中国大学生计算机设计大赛微课类赛道的一等奖作品。该作品旨在利用大数据技术为中小学生提供更为科学、全面的网络安全教育,为培养优秀信息安全人才打下基础。

    该作品的主要创新点包括:通过数据挖掘技术分析用户行为数据,对网络安全风险进行实时预警和智能识别;通过可视化技术呈现教育平台的数据分析结果,方便教师和学生理解和掌握网络安全知识;通过大数据技术构建了多维度的用户画像,实现了个性化网络安全教育。

    这些作品的具体内容和技术实现方式各有不同,但它们都具备了较高的技术难度和实用性,同时还注重了用户体验和教学效果,符合了微课与教学辅助赛道的评审标准,因此获得了国家级奖项的肯定。

以下是部分往年微课类赛道中获得国奖二等奖的作品,仅供参考:

  • 2021年:《基于 AR 技术的虚拟物理实验教学辅助系统》、《一种基于深度学习的电商网站商品推荐系统》

  • 2020年:《基于深度学习的手写汉字智能识别系统》、《基于计算机视觉的道路交通标志智能识别系统》

  • 2019年:《基于虚拟现实技术的大学物理实验教学系统设计与实现》、《基于语音识别技术的高效英语听力学习系统设计与实现》

  • 2018年:《基于深度学习的中药材图像分类与识别系统》、《基于 VR 技术的校园导览与交互式地图系统》

  • 2017年:《基于深度学习的车辆目标检测技术研究》、《基于人脸识别技术的实时点名系统》

这些作品的具体内容和技术实现方式也各有不同,但它们同样具备较高的技术难度和实用性,同时在用户体验、教学效果等方面也表现出色,虽然与一等奖的作品略有差距,但仍然是获得国家级奖项的优秀作品。

微课视频类国奖介绍

白班演示类

《大学物理课程白板教学微课设计》

1
2
3
《大学物理课程白板教学微课设计》是2019年中国大学生计算机设计大赛微课类赛道中获得国家级一等奖的作品之一。

该作品是一套针对大学物理课程的白板教学微课程,采用交互式的白板演示形式,结合动画、图像等多种形式进行讲解。在内容设计上,该作品注重将抽象的物理概念通过生动形象的演示方式呈现出来,让学生更容易理解和掌握。同时,作品还加入了实例演示、思考题等环节,帮助学生更好地巩固和应用所学知识。

《高等数学白板演示课程设计》

1
该作品由山东师范大学的学生设计,采用白板演示的方式,以动态的数学符号、图像、公式等形式进行高等数学知识的讲解。该作品的特色是能够将抽象的数学概念通过形象化的图像和实例进行讲解,使学生更容易理解和掌握数学知识。

《数据结构白板演示微课的设计与实现》

1
该作品采用了多媒体教学方法,以白板演示为主线,辅以配套课件和视频讲解,帮助学生更好地理解数据结构相关的知识点。作品获得了2019年中国大学生计算机设计大赛微课类赛道国家级一等奖。

《Python程序设计白板演示微课的设计与实现》

1
该作品通过清晰明了的白板演示和生动的示例代码,系统地介绍了Python程序设计的基本知识和应用方法。作品获得了2020年中国大学生计算机设计大赛微课类赛道国家级一等奖。

《C++白板演示微课设计与实现》

1
该作品充分利用白板演示的优势,采用多种方式解释C++程序设计的重要概念,为学生提供了一个系统而完整的学习体验。作品获得了2018年中国大学生计算机设计大赛微课类赛道国家级一等奖。

PPT课件+录音

中国大学生计算机设计大赛的微课类赛道一般注重白板演示和视频课程设计,较少使用PPT课件。国奖经历较少。

2D动画

《乒乓球技术的基本技能教学动画》

1
2
3
《乒乓球技术的基本技能教学动画》是一部在中国大学生计算机设计大赛微课类赛道中获得国家级一等奖的作品。该作品是由南京理工大学学生团队制作的,旨在通过2D动画的形式,生动地展示乒乓球基本技能的教学过程,帮助初学者更好地掌握乒乓球技术。

该动画通过细致的画面和逐步分解的动作,将各项乒乓球技能的动作过程一步步呈现出来,同时还配有详细的文字说明和语音讲解,让观众能够更加深入地理解和掌握乒乓球技能的细节。同时,该动画还设计了互动练习环节,让学习者能够在动画中进行实践练习,加深对技能的掌握。

《计算机图形学中的二维变换动画演示》

1
2
3
4
5
《计算机图形学中的二维变换动画演示》是2016年中国大学生计算机设计大赛微课类赛道中获得国家级大奖的作品。

该作品是一部以动画方式展示计算机图形学中二维变换的教学课件,主要介绍了平移、旋转、缩放和剪切四种基本的二维变换,其中每种变换都通过生动的动画和详细的文字说明进行了讲解,能够让初学者快速理解这些概念。

该作品采用了简洁明了的设计风格,使得内容易于理解,同时也配备了考试模式,学生可以通过练习来检验自己的学习效果。作品在教学效果和设计风格上均得到了评委的高度认可。

《二维空间的变换及其应用》

1
2
3
《二维空间的变换及其应用》是一份获得中国大学生计算机设计大赛微课类赛道国家二等奖的作品。

这份作品是一份2D动画,通过生动有趣的方式讲解了二维空间中的旋转、平移、缩放和反演变换,并介绍了这些变换在计算机图形学中的应用,如图形绘制、图像处理等。通过本作品的学习,可以帮助学生更好地理解和掌握计算机图形学中的二维变换。

视频课程

《用Python做科学计算》

1
2
3
《用Python做科学计算》是一份由华南理工大学的学生制作的视频课程,该作品曾在2019年中国大学生计算机设计大赛微课类赛道中获得国家级一等奖。该视频课程主要介绍了Python在科学计算中的应用,包括Numpy库的使用、矩阵计算、插值法等内容。

该视频课程通过生动的示例和详细的讲解,使初学者能够快速掌握Python在科学计算中的应用方法,并且提供了丰富的练习题和实践项目,能够帮助学习者巩固知识和提高能力。课程的制作团队还提供了免费的课程资料和代码库,方便学习者进行自主学习和深入研究。

《疫情下的在线课堂设计》

1
2
3
4
5
《疫情下的在线课堂设计》是一份获得2019-2020年中国大学生计算机设计大赛微课类赛道国家级一等奖的作品。该作品主要针对疫情期间线上教学的需要,设计了一套适用于大规模在线教育的系统,包括多媒体教学内容的制作、互动式学习和实时评测等功能,提供了一套全方位的在线学习解决方案。
该作品的特点主要有:
多媒体教学内容制作:采用Markdown语法,支持多种图形、表格、公式等文本形式,辅以演示文稿,让教师能够方便快捷地制作教学内容。
互动式学习:通过在线讨论、课堂练习等形式,让学生更好地参与学习。
实时评测:利用互动式课堂进行实时答题,并通过算法对答题情况进行实时评测,提供即时的反馈。

《数据可视化》

1
《数据可视化》是一份获得过中国大学生计算机设计大赛微课类赛道国家二等奖的作品。它是由上海交通大学的学生所设计的一份视频课程,旨在通过数据可视化的方式,帮助人们更好地理解和分析数据。该课程包含数据可视化的基本概念、图表的设计原则、数据的准备与整理、使用Python语言进行数据可视化等方面的内容。

《Python数据分析与可视化》

1
这是一门Python编程语言的数据分析和可视化教程,以实例为主线,注重实战。在本课程中,您将学习到如何使用Python处理、清洗、可视化数据,并且学会了如何进行数据的探索性分析、数据预处理、数据建模等多个环节。

MOOC课程

相关信息较少

微课视频类国奖链接

动画类

2020中国计算机设计大赛-微课大类-计算机小类(国一优秀作品)

1
https://www.bilibili.com/video/BV17Q4y1f7o7/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee

我看了一下,他这个是利用万彩做了一个动画,讲解计算机中的一个算法,我认为优秀之处是首先选题切合于计算机,其次动画制作良好。

2022年中国大学生计算机设计大赛-全国一等奖获奖作品-汉语言文学

1
https://www.bilibili.com/video/BV1ad4y1T7Vm/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee

同样,利用动画讲解古诗词。动画制作也同样采用万彩。

2022全国计算机设计大赛微课教学组国家二等奖作品《小黄鸭的奇幻漂流》

1
https://www.bilibili.com/video/BV1eT411T7uC/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee

【计算机设计大赛】微课类别国赛一等奖作品《时光修复-图像的平滑操作》

1
https://www.bilibili.com/video/BV1rB4y1b7We/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee

2022年中国大学生计算机设计大赛国赛优秀作品点评(划重点)

1
注;由于感觉网上大多数微课类可以搜到资源的获奖作品都是动画类,所以去观看了一下2022年中国大学生计算机设计大赛国赛优秀作品点评,其中有一些优秀的作品可以借鉴一下。

链接

1
https://www.bilibili.com/video/BV1SS4y1t7ty/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee

汉语言文化转译空间设计

这是点评的第一个作品,他采用的是PPT讲解与视频展示的方式,视频是自己录制剪辑的,感觉他出色的地方在于思路的新颖,同时会有专业知识的穿插,展现自己的设计难度。不过这个是数媒类的。

巧立方法,巧破球牢

这是第一个点评的微课类作品,优秀作品都有自己的出发点与实用性,这个作品说白了就是利用动画讲解了一个知识点,他们的亮点在于:

1.讲解知识点时代用历史知识

2.虚拟模型,构造场景

3.采用实物教学

4.交互学习

赋得古原草送别

这是第二个点评的微课类作品,这个他的出发点我感觉很好,是专门为听障人士设计,所以是手语教学,我感觉你们也可以借鉴这个思路。

秒懂归并排序

这是第三个点评的微课类作品,这个就比较普通,利用动画讲解知识点,但是他有一个出彩的地方你们可以学习,就是他的背景是引入的时候,还采用的问卷调查。同时他们课程录完以后,也采用了问卷进行满意度调查。

关于主题

以熊猫与物种多样性为题参加微课类赛道,可以考虑以下几点:

1
2
3
4
1.	内容设计:熊猫是中国的国宝之一,是世界上最受欢迎的动物之一。可以以熊猫为主题,介绍熊猫的特点、生态环境、繁殖生态、食性等方面的知识,同时结合熊猫的保护现状和措施,介绍物种多样性的概念、意义和保护方法,从而引导学生关注环境保护和生态文明建设。
2. 视频制作:可以采用2D动画、PPT课件、白板演示等多种制作方式,配合讲解和图表等教学手段,以提高学生的学习效果。同时,注重视频制作的创意和美观度,增加学生的观赏性和兴趣。
3. 教学质量:在制作视频时,需要注重教学内容的科学性和严谨性,同时注重教学方法的合理性和有效性。要注意吸引学生的注意力,增强学生的参与感和互动性,提高教学效果。
4. 技术创新:可以采用新颖的技术手段,如虚拟现实、增强现实等技术,以提高教学效果和视觉效果。

创新点选取:

1
2
3
4
5
1.	视频形式的创新:在制作视频时,可以考虑采用不同的视频形式,如3D动画、虚拟现实等,以吸引学生的兴趣和提高教学效果。
2. 教学方式的创新:在讲解熊猫和物种多样性的相关知识时,可以采用一些新的教学方式,如游戏化教学、情境教学等,以提高学生的学习兴趣和参与度。
3. 数据和案例的创新:可以收集一些熊猫和物种多样性相关的数据和案例,结合实际情况和科学研究,向学生介绍物种多样性的现状和保护措施,从而提高学生对环保和生态建设的关注度。
4. 互动性的创新:可以增加视频的互动性,如加入互动式测试、调查问卷等环节,以提高学生的参与感和兴趣,同时可以帮助教师了解学生的学习情况和反馈意见,从而提高教学效果。调查问卷感觉开始和结束都可以采用。
5.出发点的创新:比如说上文提到的那个作品,为听障人士展出。

作品信息概要表填写

作品简介:

1
在作品简介中,你可以简要介绍你的作品,包括作品的主题、目的、内容和特点等。同时,还可以简要介绍一下你的创新点,让评审委员会能够快速了解你的作品和创新点。

特别说明:

1
特别说明是你对作品的详细解释和阐述,可以包括你的设计思路、实现方法、难点解决、对学生的启发和帮助等方面。此外,你还可以针对评审委员会可能会关注的方面,例如教学效果、学生反馈、科研价值等方面进行阐述,以表达你的深入思考和专业素养。

总之,作品简介和特别说明应该突出你的创新点和教学效果,同时能够让评审委员会更好地了解你的作品和思路。最后,一定要注意书写规范,清晰明了地表达你的思想和观点。

创新描述:

1
2
3
4
1.	突出创新点:你的创新点应该与主题紧密相关,并且突出创新和实用性。在描述创新点时,可以从问题、挑战和解决方案等方面入手,说明你的创新点的价值和实用性。
2. 确定优势:你的创新点应该具有一定的独特性和优势,可以结合现有的教学方法和工具进行对比和说明,从而突出你的创新点的优势和亮点。
3. 描述实现过程:在描述创新点时,需要说明如何实现该创新点。可以从技术方面入手,例如使用的软件、硬件等,也可以从教学实践的角度入手,例如如何将创新点融入到教学实践中等。
4. 表达教学效果:最后,你需要说明你的创新点的教学效果,包括学生学习成果和反馈等方面。可以结合具体的案例和数据进行说明,以证明你的创新点对于学生的学习和提高的促进作用。

总之,创新描述应该能够让评审委员会清晰地了解你的创新点的价值和实用性,同时表达出你的专业素养和教育理念。最后一定要注意语言准确、简洁明了,让评审委员会轻松理解你的想法和创新点。

结束语

目前想到的就是这些啦,有什么其他我再补充。后面几个模块可以重点看看,像优秀作品点评,主题和作品信息概要填写等。

累死喔喽,要加油哦!