微服务项目总结
1. 微服务
1.1 单体架构
单体架构(monolithic structure):整个项目中所有功能模块都在一个工程中开发;项目部署时需要对所有模块一起编译、打包;项目的架构设计、开发模式都非常简单。
所有的功能集中在一个项目中开发,打包一个包部署
1.2 微服务
微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。
1.3 SpringCloud
1. JWT
1.1 JWT的生成
- 从表单实体中拿到输入的用户的名字和密码
- 使用 MyBatis Plus 的 LambdaQueryWrapper 查询数据库中是否存在一个用户名为
username
的用户 - 校验用户状态
- 校验密码
- 生成TOKEN
- 将DTO封装成VO返回
public UserLoginVO login(LoginFormDTO loginDTO) {// 1.数据校验String username = loginDTO.getUsername();String password = loginDTO.getPassword();// 2.根据用户名或手机号查询User user = lambdaQuery().eq(User::getUsername, username).one();Assert.notNull(user, "用户名错误");// 3.校验是否禁用if (user.getStatus() == UserStatus.FROZEN) {throw new ForbiddenException("用户被冻结");}// 4.校验密码if (!passwordEncoder.matches(password, user.getPassword())) {throw new BadRequestException("用户名或密码错误");}// 5.生成TOKENString token = jwtTool.createToken(user.getId(), jwtProperties.getTokenTTL());// 6.封装VO返回UserLoginVO vo = new UserLoginVO();vo.setUserId(user.getId());vo.setUsername(user.getUsername());vo.setBalance(user.getBalance());vo.setToken(token);return vo;}
JWT:是一个字符串,包含三部分
- Header:算法和令牌类型
- Payload:荷载,包含声明,如用户信息、过期时间等
- Signature:签名,用于验证消息在传输过程中没有被更改
1.2 拦截器
在前端的每次请求,会携带JWT,在服务端需要获取 Authorization
请求头中的 JWT Token,解析并校验JWT,通过拦截器来实现
通过拦截器拿到用户信息,将用户信息存到ThreadLocal中
项目包含
- 前置方法
- Controller 方法执行前调用。
- 用于身法校验、权限校验
- 最终方法
- 整个请求处理完成后执行(无论是否发生异常)。
- 用于资源清理(如关闭数据库连接、清除 ThreadLocal),避免内存泄漏
public class LoginInterceptor implements HandlerInterceptor {private final JwtTool jwtTool;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的 tokenString token = request.getHeader("authorization");// 2.校验tokenLong userId = jwtTool.parseToken(token);// 3.存入上下文UserContext.setUser(userId);// 4.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 清理用户UserContext.removeUser();}
}
1.3 可能问到的问题
如何实现用户Token的存放的?为什么要用 ThreadLocal?
UserContext内部使用ThreadLocal来保存当前线程的用户信息,这样做的目的是为了实现线程格里,保证每个请求的用户信息不会不想干扰
拦截器的执行顺序和生命周期
前置方法应该在Controller 方法执行前调用,用于身份验证、权限校验。
结束方法在整个请求处理完成后执行(无论是否发生异常),用于资源清理(如关闭数据库连接、清除 ThreadLocal),避免内存泄漏
校验token抛出异常的原因有哪些?
Token 已过期,签名不匹配(签名被篡改),Token 格式错误(不是三段结构),Token 被加入黑名单(需配合 Redis 黑名单机制)
Token 过期后如何让用户继续使用而不用重新登录?
可以引入 Refresh Token:
- 登录时返回两个 Token:Access Token(短时效) + Refresh Token(长时效)
- Access Token 失效后,客户端携带 Refresh Token 向服务端换取新的 Access Token
- Refresh Token 也需要校验合法性,并可加入黑名单
所有接口都需要经过这个拦截器吗?
不应该拦截所有接口,比如 /login
接口就不需要 Token。
可以在拦截器中增加白名单判断
浏览器跨域请求时,Authorization Header 会不会被浏览器拦截?
在跨域请求时:
- 浏览器会先发送一个
OPTIONS
预检请求(preflight) - 服务器必须正确配置 CORS,允许
Authorization
请求头
2 远程调用
服务做了拆分,数据有了隔离后,当业务需要别的服务的数据时候,可以通过远程调用拿到别的服务的数据。
但是这个远程调用有个缺陷就是会把请求路径的IP给写死了
例如Cart-service请求Item-service的数据:
Spring提供了一个RestTemplete工具类,可以方便的实现Http请求的发送
- 注入RestTemplate到Spring容器
@Beanpublic RestTemplate restTemplate(){return new RestTemplate();}
- 发起远程调用
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange("http://localhost:8081/items?ids={ids}", // 请求路径HttpMethod.GET, // 请求方式null, // 请求实体,可以为空new ParameterizedTypeReference<List<ItemDTO>>() {}, // 返回值类型Map.of("ids", CollUtil.join(itemIds, ",")) // 请求参数);// 2.2 解析响应// 查询响应是否成功,如果失败则直接结束if(!response.getStatusCode().is2xxSuccessful()){return;}List<ItemDTO> items = response.getBody();
远程调用的关键点就在于四个:
- 请求方式
- 请求路径
- 请求参数
- 返回值类型
3. 服务治理
3.1 注册中心原理
服务治理中的三个角色:
- 服务提供者:提供接口供其它微服务访问,比如
item-service
- 服务消费者:调用其它微服务提供的接口,比如
cart-service
- 注册中心:记录并监控微服务各实例状态,推送服务变更信息
消费者如何知道提供者的地址?
服务提供者会在启动时注册自己的信息到注册中心,消费者可以从注册中心订阅和拉取服务信息
消费者如何得知服务状态变更?
服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务删除,并通知订阅了该服务的消费者
当提供者有多个实例时,消费者该选择哪一个?
负载均衡算法
3.2 Nacos注册中心
Nacos帮助我们取管理所有的服务,并监控所有微服务的状态
进入root目录,执行:
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
访问:http://192.168.100.128:8848/nacos/
3.3 服务注册
-
引入nacos discovery依赖
-
配置nacos地址
3.4 服务发现
-
引入nacos discovery依赖
-
配置nacos地址
-
服务发现
- 根据服务名称获取服务的实例列表
- 手写负载均衡,从服务列表中获取一个实例
- 利用RestTemplate发起http请求,请求另外一个微服务提供相关服务
// 2.1 根据服务的名称获获取服务的实例列表List<ServiceInstance> instances = discoveryClient.getInstances("item-service");if(CollUtil.isEmpty(instances)){return;}// 2.2 手写负载均衡(这里使用简单的随机负载均衡),从服务列表中挑选一个实例ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));// 2.3利用RestTemplate来发起http请求,请求item.service提供相关服务,获取实例的IP和端口:instance.getUri()ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(instance.getUri()+"/items?ids={ids}",HttpMethod.GET,null,new ParameterizedTypeReference<List<ItemDTO>>() {},Map.of("ids", CollUtil.join(itemIds, ",")));
4. OpenFeign
nacos使用太复杂,使用OpenFeign来进一步优化
4.1 快速入门
-
**引入依赖,**包括OpenFeign和负载均衡组件SpingCloudLoadBalancer
一个做调用一个做负载均衡
<!--openFeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--负载均衡器--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
- 通过@EnableFeignClients注解,启用OpenFeign功能
@EnableFeignClients
- 编写FeignClient
@FeignClient("item-service") // 服务提供者的名字
public interface ItemClient {@GetMapping("/items")List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
- 使用FeignClient,实现远程调用
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
4.2 连接池
连接池指的是一组预先创建的 HTTP 连接,这些连接可以被重复使用,而不是每次请求都创建一个新的连接。
在 Feign 中,不同的 HTTP 客户端实现对连接池的支持有所不同:
HttpURLConnection:这是 Java 的默认 HTTP 客户端实现,不支持连接池。每次请求都会创建一个新的连接,请求完成后连接会被关闭。这种方式在高并发场景下性能较差。
Apache HttpClient:这是一个功能强大的 HTTP 客户端库,支持连接池。通过配置连接池,可以复用连接,提高性能。
OKHttp:这是另一个流行的 HTTP 客户端库,也支持连接池。OKHttp 的连接池实现高效且易于配置,适合在高并发场景下使用
Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
- HttpURLConnection:默认实现,不支持连接池
- Apache HttpClient :支持连接池
- OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.
最好使用连接池,降低创建和修改连接的开销
OpenFeign整合OKHttp的步骤:
-
引入依赖
-
src/main/resources/application.yaml 开启连接池功能
feign:okhttp:enabled: true # 开启OKHttp功能
4.3 最佳实践
将与业务无关的接口单独抽取成一个微服务
4.4 日志
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
1-4 微服务中远程调用与服务治理可能问到的问题
1. 你是如何在微服务之间进行远程调用的?请描述一下你使用过哪些远程调用的方式?
-
RestTemplate 是 Spring 提供的用于发送 HTTP 请求的工具类。可以通过
exchange
方法发起 GET/POST 等请求,并处理返回值类型(如ParameterizedTypeReference
)。缺点:请求路径中 IP 和端口是硬编码的,不灵活,不利于服务治理。 -
后续优化中,使用到了Nacos来进行服务的注册和发现,使用手动负载均衡进行业务的分流;
-
后续优化中,使用OpenFeign替换RestTemplate 进行优化,通过声明式客户端,简化远程调用;
-
继续优化中,为了避免频繁创建连接,使用带有连接池的客户端来代替默认的客户端连接。比如,我们使用OK Http.
-
最终,为了保证微服务之间的低耦合性,我们抽取 FeignClient 到公共模块,实现了接口的复用,在 FeignClient 我们不是调用方法,而是发送请求给指定的微服务以要求服务。