1.学习背景
各位同学大家好,经过前面的学习我们已经掌握了《微服务架构》的核心技术栈。相信大家也体会到了微服务架构相对于项目一的单体架构要复杂很多,你的脑袋里也会有很多的问号:
2.天机学堂介绍
天机学堂是一个基于微服务架构的生产级在线教育项目,核心用户不是K12群体,而是面向成年人的非学历职业技能培训平台。相比之前的项目课程,其业务完整度、真实度、复杂度都非常的高,与企业真实项目非常接近。
通过天机学堂项目,你能学习到在线教育中核心的学习辅助系统、考试系统,电商类项目的促销优惠系统等等。更能学习到微服务开发中的各种热点问题,以及不同场景对应的解决方案。学完以后你会收获很多的“哇塞”。
2.1.行业背景
2021年7月,国务院颁布《关于进一步减轻义务教育阶段学生作业负担和校外培训负担的意见》,简称“双减”政策。在该政策影响下,多年来占据我国教育培训行业半壁江山的课外辅导培训遭到毁灭性打击。相对的,职业教育培训的市场规模持续增长:
项目学习地址 www.cx1314.cn
3.1.企业开发模式
在企业开发中,微服务项目非常庞大,往往有十几个,甚至数十个,数百个微服务。而这些微服务也会交给不同的开发组去完成开发。你可能只参与其中的某几个微服务开发,那么问题来了:
如果我的微服务需要访问其它微服务怎么办?
难道说我需要把所有的微服务都部署到自己的电脑吗?
很明显,这样做是不现实的。第一,不是所有的代码你都有访问的权限;第二,你的电脑可能无法运行这数十、数百的微服务。
因此,企业往往会提供一个通用的公共开发、测试环境,在其中部署很多公共服务,以及其它团队开发好的、开发中的微服务。
而我们大多数情况下只在本地运行正在开发的微服务,此时我们就需要一些其它的测试手段:
单元测试
单元测试一般是在项目的test目录下自己编写的测试,可以针对具体到每一个方法的测试。
集成测试
接口开发完成后,可能需要调用其它微服务接口,此时可以调用开发环境中的其它微服务,测试接口功能是否正常工作。
组件测试
将自己团队开发的微服务部署到开发环境,作为一个微服务组件,与开发环境中的其它微服务联调,测试整个微服务是否正常工作。
端对端测试
在测试环境部署前端、后端微服务群,直接进行前后端的联调测试。
当然,实际中我们可以把集成测试与组件测试合并,开发完成后直接与开发环境的其它微服务联调,测试服务工作状态。
[图片]
在天机学堂中,我们也给大家模拟了这样的一个开发环境,其中部署了各种公共服务,而我们只需要在本地开发未完成的几个服务即可:
[图片]
3.2.导入虚拟机
为了模拟企业中的开发环境,我们利用虚拟机搭建了一套开发环境,其中部署了开发常用的组件:
导入方式有两种:
注意:导入虚拟机后所有软件即可使用,无需重复安装,VMware一定要按照文档中设置IP,不要私自修改。一定要关闭windows防火墙。
3.3.配置本机hosts
为了模拟使用域名访问,我们需要在本地配置hosts:
192.168.150.101 git.tianji.com
192.168.150.101 jenkins.tianji.com
192.168.150.101 mq.tianji.com
192.168.150.101 nacos.tianji.com
192.168.150.101 xxljob.tianji.com
192.168.150.101 es.tianji.com
192.168.150.101 api.tianji.com
192.168.150.101 www.tianji.com
192.168.150.101 manage.tianji.com
192.168.150.101 cpolar.tianji.com
当我们访问上述域名时,请求实际是发送到了虚拟机,而虚拟机中的Nginx会对这些域名做反向代理,这样我们就能请求到对应的组件了:
[图片]
18082
同样,我们访问用户端或者管理端页面时,也会被Nginx反向代理:
[图片]
当我们访问www.tianji.com时,请求会被代理到虚拟机中的 /usr/local/src/tj-portal目录中的静态资源
当页面访问api.tianji.com时,请求会被代理到虚拟机中的网关服务。
3.4.部署
微服务部署比较麻烦,所以企业中都会采用持续集成的方式,快捷实现开发、部署一条龙服务。
为了模拟真实环境,我们在虚拟机中已经提供了一套持续集成的开发环境,代码一旦自测完成,push到Git私服后即可自动编译部署。
[图片]
而开发我们负责的微服务时,则需要在本地启动运行部分微服务。
3.3.1.虚拟机部署
项目已经基于Jenkins实现了持续集成,每当我们push代码时,就会触发项目完成自动编译和打包。
我们可以在Git仓库模拟代码push操作:
需要运行某个微服务时,我们只需要经过两步:
3.3.2.本地部署
对于需要开发功能的微服务,则需要在本地部署,不过首先我们要把代码拉取下来。
查看Git私服的代码:http://git.tianji.com/tjxt/tianji :
[图片]
利用命令将代码克隆到你的IDEA工作空间中:
git clone http://192.168.150.101:10880/tjxt/tianji.git -b lesson-init
[图片]
注意,开发时需要使用dev分支,因此我们需要创建新的分支:
cd tianji
git checkout -b dev
为了方便我们教学,目前所有微服务代码都聚合在了一个Project中,如图:
[图片]
在默认情况下,微服务启用的是dev配置,如果要在本地运行,需要设置profile为local:
[图片]
可以在本地启动ExamApplication,然后我们去Nacos控制台查看exam-service,可以看到有两个实例,分别是虚拟机IP和宿主机IP:
[图片]
4.修复BUG
在刚刚进入项目组后,一般不会布置开发任务,而是先熟悉项目代码。为了帮助大家熟悉整个项目,我们预留了一个BUG,让大家在修复BUG的过程中熟悉项目代码。
一般修复BUG的过程是这样的:
4.1.熟悉项目
熟悉项目的第一步是熟悉项目的结构、用到的技术、编码的一些规范等。
4.1.1.项目结构
我们先来看看项目结构,目前企业微服务开发项目结构有两种模式:
方案一更适合于大型项目,架构更为复杂,管理和维护成本都比较高;
方案二更适合中小型项目,架构更为简单,管理和维护成本都比较低;
天机学堂采用的正是第二种模式,结构如图:
暂时无法在飞书文档外展示此内容
对应到我们项目中每个模块及功能如下:
[图片]
当我们要创建新的微服务时,也必须以tjxt为父工程,创建一个子module. 例如交易微服务:
[图片]
微服务module中如果有对外暴露的Feign接口,需要定义到tj-api模块中:
[图片]
4.1.2.实体类规范
在天机学堂项目中,所有实体类按照所处领域不同,划分为4种不同类型:
4.1.3.依赖注入
Spring提供了依赖注入的功能,方便我们管理和使用各种Bean,常见的方式有:
在以往代码中,我们经常利用Spring提供的@Autowired注解来实现依赖注入:
[图片]
不过,这种模式是不被Spring推荐的,Spring推荐的是基于构造函数注入,像这样:
[图片]
但是,如果需要注入的属性较多,构造函数就会非常臃肿,代码写起来也比较麻烦。
好在Lombok提供了一个注解@RequiredArgsConstructor,可以帮我们生成构造函数,简化代码:
[图片]
这样一来,不管需要注入的字段再多,我们也只需要一个注解搞定:
[图片]
4.1.4.异常处理
在项目运行过程中,或者业务代码流程中,可能会出现各种类型异常,为了加以区分,我们定义了一些自定义异常对应不同场景:
[图片]
在开发业务的过程中,如果出现对应类型的问题,应该优先使用这些自定义异常。
当微服务抛出这些异常时,需要一个统一的异常处理类,同样在tj-common模块中定义了:
@RestControllerAdvice
@Slf4j
public class CommonExceptionAdvice {
@ExceptionHandler(DbException.class)
public Object handleDbException(DbException e) {
log.error("mysql数据库操作异常 -> ", e);
return processResponse(e.getStatus(), e.getCode(), e.getMessage());
}
@ExceptionHandler(CommonException.class)
public Object handleBadRequestException(CommonException e) {
log.error("自定义异常 -> {} , 状态码:{}, 异常原因:{} ",e.getClass().getName(), e.getStatus(), e.getMessage());
log.debug("", e);
return processResponse(e.getStatus(), e.getCode(), e.getMessage());
}
@ExceptionHandler(FeignException.class)
public Object handleFeignException(FeignException e) {
log.error("feign远程调用异常 -> ", e);
return processResponse(e.status(), e.status(), e.contentUTF8());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getAllErrors()
.stream().map(ObjectError::getDefaultMessage)
.collect(Collectors.joining("|"));
log.error("请求参数校验异常 -> {}", msg);
log.debug("", e);
return processResponse(400, 400, msg);
}
@ExceptionHandler(BindException.class)
public Object handleBindException(BindException e) {
log.error("请求参数绑定异常 ->BindException, {}", e.getMessage());
log.debug("", e);
return processResponse(400, 400, "请求参数格式错误");
}
@ExceptionHandler(NestedServletException.class)
public Object handleNestedServletException(NestedServletException e) {
log.error("参数异常 -> NestedServletException,{}", e.getMessage());
log.debug("", e);
return processResponse(400, 400, "请求参数异常");
}
@ExceptionHandler(ConstraintViolationException.class)
public Object handViolationException(ConstraintViolationException e) {
log.error("请求参数异常 -> ConstraintViolationException, {}", e.getMessage());
return processResponse( HttpStatus.OK.value(), HttpStatus.BAD_REQUEST.value(),
e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).distinct().collect(Collectors.joining("|"))
);
}
@ExceptionHandler(Exception.class)
public Object handleRuntimeException(Exception e) {
log.error("其他异常 uri : {} -> ", WebUtils.getRequest().getRequestURI(), e);
return processResponse(500, 500, "服务器内部异常");
}
private Object processResponse(int status, int code, String msg){
// 1.标记响应异常已处理(避免重复处理)
WebUtils.setResponseHeader(Constant.BODY_PROCESSED_MARK_HEADER, "true");
// 2.如果是网关请求,http状态码修改为200返回,前端基于业务状态码code来判断状态
// 如果是微服务请求,http状态码基于异常原样返回,微服务自己做fallback处理
return WebUtils.isGatewayRequest() ?
R.error(code, msg).requestId(MDC.get(Constant.REQUEST_ID_HEADER))
: ResponseEntity.status(status).body(msg);
}
}
4.1.5.配置文件
SpringBoot的配置文件支持多环境配置,在天机学堂中也基于不同环境有不同配置文件:
[图片]
说明:
文件
说明
bootstrap.yml
通用配置属性,包含服务名、端口、日志等等各环境通用信息
bootstrap-dev.yml
线上开发环境配置属性,虚拟机中部署使用
bootstrap-local.yml
本地开发环境配置属性,本地开发、测试、部署使用
项目中的很多共性的配置都放到了Nacos配置中心管理:
[图片]
例如mybatis、mq、redis等,都有对应的shared-xxx.yaml共享配置文件。在微服务中如果用到了相关技术,无需重复配置,只要引用上述共享配置即可:
[图片]
4.1.5.1.bootstrap.yml
我们来看看bootstrap.yml文件的基本内容:
[图片]
接下来,我们就分别看看每一个共享的配置文件内容。
4.1.5.2.shared-spring.yml
spring:
jackson:
default-property-inclusion: non_null # 忽略json处理时的空值字段
main:
allow-bean-definition-overriding: true # 允许同名Bean重复定义
mvc:
pathmatch:
# 解决异常:swagger Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerException
# 因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher
matching-strategy: ant_path_matcher
4.1.5.3.shared-mybatis.yaml
mybatis-plus:
configuration: # 默认的枚举处理器
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
field-strategy: 0
db-config:
logic-delete-field: deleted # mybatis逻辑删除字段
id-type: assign_id # 默认的id策略是雪花算法id
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动
url: jdbc:mysql:// t j . j d b c . h o s t : 192.168.150.101 : {tj.jdbc.host:192.168.150.101}: tj.jdbc.host:192.168.150.101:{tj.jdbc.port:3306}/${tj.jdbc.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
username: ${tj.jdbc.username:root}
password: ${tj.jdbc.password:123}
注意到这里把mybatis的datasource都配置了,不过由于jdbc连接时的数据库ip、端口,数据库名、用户名、密码是不确定的,这里做了参数映射:
参数名
描述
默认值
tj.jdbc.host
主机名
192.168.150.101,也就是虚拟机ip
tj.jdbc.port
数据库端口
3306
tj.jdbc.database
数据库database名称
无
tj.jdbc.username
数据库用户名
root
tj.jdbc.password
数据库密码
123
除了tj.jdbc.database外,其它参数都有默认值,在没有配置的情况下会按照默认值来配置,也可以按照参数名来自定义这些参数值。其中tj.jdbc.database是必须自定义的值,例如在交易服务中:
tj:
jdbc:
database: tj_trade
4.1.5.4.shared-mq.yaml
spring:
rabbitmq:
host: ${tj.mq.host:192.168.150.101} # mq的IP
port: ${tj.mq.port:5672}
virtual-host: ${tj.mq.vhost:/tjxt}
username: ${tj.mq.username:tjxt}
password: ${tj.mq.password:123321}
listener:
simple:
retry:
enabled: ${tj.mq.listener.retry.enable:true} # 开启消费者失败重试
initial-interval: ${tj.mq.listener.retry.interval:1000ms} # 初始的失败等待时长为1秒
multiplier: ${tj.mq.listener.retry.multiplier:1} # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: ${tj.mq.listener.retry.max-attempts:3} # 最大重试次数
stateless: ${tj.mq.listener.retry.stateless:true} # true无状态;false有状态。如果业务中包含事务,这里改为false
这里配置了mq的基本配置,例如地址、端口等,默认就是tjxt的地址,不需要修改。另外还配置类消费者的失败重试机制,如有需要可以按需修改。
4.1.5.5.shared-redis.yaml
spring:
redis:
host: ${tj.redis.host:192.168.150.101}
password: ${tj.redis.password:123321}
lettuce:
pool:
max-active: ${tj.redis.pool.max-active:8}
max-idle: ${tj.redis.pool.max-idle:8}
min-idle: ${tj.redis.pool.min-idle:1}
max-wait: ${tj.redis.pool.max-wait:300}
注意配置了Redis的基本地址和连接池配置,省去了我们大部分的工作
4.1.5.6.shared-feign.yaml
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
这里配置了默认的Feign日志级别以及连接池配置,一般不需要修改。
4.1.5.7.shared-xxljob.yaml
tj:
xxl-job:
access-token: tianji
admin:
address: http://192.168.150.101:8880/xxl-job-admin
executor:
appname: s p r i n g . a p p l i c a t i o n . n a m e l o g − r e t e n t i o n − d a y s : 10 l o g P a t h : j o b / {spring.application.name} log-retention-days: 10 logPath: job/ spring.application.namelog−retention−days:10logPath:job/{spring.application.name}
这里配置了xxl-job组件的地址等信息,一般不需要修改。
4.2.阅读源码
阅读源码也不是闷头乱找,而是有一定的技巧。一般阅读源码的流程如下:
[图片]
4.2.1.BUG重现
首先,我们来看还原一下BUG现场。
我们用杰克用户登录(jack/123),删除一个订单,发现删除成功:
[图片]
我们切换到萝丝用户登录(rose/123456),再次删除一个订单:
[图片]
发现删除失败,这是什么情况??
4.2.2.理清请求链路
如果是我们自己写的代码,肯定很容易找到业务入口、整个业务线路。但现在我们是接手他人项目,所以只能通过其它途径来梳理业务:
此处由于我们没有人可以交流,只能通过查看前端请求来分析了。经过查看,页面删除订单的请求如下:
[图片]
按照之前我们的环境部署方案,api.tianji.com这个域名会被解析到192.168.150.101这个地址,然后被Nginx反向代理到网关微服务。
而网关则会根据请求路径和路由规则,把请求再路由到具体微服务。这里请求路径以/ts开头,对应的微服务是trade-service,也就是交易微服务。
这样,整个请求链路就比较清楚了:
暂时无法在飞书文档外展示此内容
找到了具体的微服务,接下来,我们就进入微服务,查看对应源码,找出问题即可。
请求到达交易服务后的路径是 /orders/{id},对应的controller是:
[图片]
跟入service代码:
[图片]
这样就找到了BUG发生的代码块了,现在只需要通过DEBUG调试来发现问题产生的原因就可以了。
4.3.远程调试
由于交易服务属于开发环境已经部署的服务,我们无法在本地调试,这在今后的开发中会经常碰到。遇到这样的情况我们就需要利用IDEA提供的远程调试功能。
4.3.1.本地配置
首先,我们需要对本地启动项做一些配置:
[图片]
然后添加一个新的启动项:
[图片]
在新建的Configuration中填写信息:
[图片]
此时,就可以在启动项中看到我们配置的远程调试项目了:
[图片]
4.3.2.远程调试的部署脚本
仅仅本地配置还不够,我们还需要在虚拟机中部署时,添加一段配置到部署脚本中,这段配置IDEA已经提供给我们了:
[图片]
我们需要在启动时加上这段参数,像这样:
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 xx.jar
不过我们的项目都是基于Jenkins来部署的,因此需要修改Jenkins部署脚本。部署脚本我也已经帮大家配置好了,我们直接运行即可:
[图片]
部署完成后,可以看到tj-trade多暴露了一个5005端口,就是远程调试的端口了:
[图片]
4.3.3.开始调试
现在,我们就可以在需要的地方打上断点,然后DEBUG运行了:
[图片]
访问页面请求,就可以进入DEBUG断点了。
经过断点,可以发现断点所属用户判断出现问题的原因了:
[图片]
我们在判断用户id时使用了!=来判断,由于id是Long 类型,因此判断的是id对应的地址而不是值,所以萝丝用户的userId虽然都是129,但地址不同,判断自然不成立。
但问题来了,为什么杰克用户就可以删除成功呢?
再次以杰克发起请求,进入断点:
[图片]
可以发现杰克的id是2,两个userId的地址是一样的!!
为什么userId为2的时候判断相等可以成立,而userId是129的时候判断相等不成立呢?
这是因为userId是Long类型包装类。包装类为了提高性能,减少内存占用,采用了享元模式,提前将-128~127之间的Long包装类提前创建出来,共享使用。
因此只要大小范围在者之间的数字,只要值相同,使用的都是享元模式中提供的同一个对象。杰克的id是2,恰好在范围内;而萝丝的id是129,刚好超过了这个范围。这就导致了杰克可以删除自己订单,而萝丝无法删除的现象。
这就说明,我们此处判断userId是否相等的方式是错误的,不能基于!=来判断,而是应该比较值,使用equals。
4.4.修复BUG
既然找到了BUG产生的原因,接下来就可以来修复BUG了。
4.4.1.分支管理
一般我们不建议大家直接在Dev分支直接修改代码。在企业中都有一套分支管理机制,称为GitFlow,大概如图所示:
[图片]
说明:
在咱们项目中,master分支用来给大家提供完整版本代码了,而lesson-init分支作为初始化分支。因此一般不使用master分支,而是把lesson-init当做master分支来用。开发用的dev分支就等于GitFlow中的Develop分支。
因此,这里建议大家在dev分支基础上创建一个Hotfix分支,用以修改BUG,可以通过命令来创建该分支:
git checkout -b hotfix-delete-order-error
[图片]
4.4.2.修复BUG
接下来,就可以修复BUG了,其实非常简单,不要使用!=判断,而是改用equals即可:
[图片]
接下来,提交代码:
[图片]
然后切换会Dev分支,并将hotfix-delete-order-error分支合并到dev分支,然后删除:
[图片]
4.5.测试部署
一般的测试步骤是这样的:
[图片]
由于我们这里的修改比较简单,这里就不做单元测试了。
4.5.1.接口测试
我们首先基于swagger做本地接口测试,在本地启动tj-trade项目,然后访问swagger页面:
http://localhost:8088/doc.html ,找到删除订单接口:
[图片]
由于删除订单时需要对登录用户做校验,因此需要先设置用户id的全局参数:
[图片]
[图片]
微服务获取用户是基于请求头来传递的,因此我们设置全局参数时添加一个user-info的请求头参数即可。
然后刷新页面,来再次找到删除订单接口,进行调试,发现当用户id不对时,删除会失败:
[图片]
当用户id正确时,删除成功:
[图片]
4.5.2.组件测试
接下来让我们的服务与网关联调,再次测试。
不过问题来了,现在我们在本地启动了交易服务,而虚拟机中也启动了交易服务:
[图片]
当我们请求网关时,如何保证请求一定进入本地启动的服务呢?
这里有两种办法:
权重设置:
[图片]
接下来,通过浏览器访问前端页面,然后点击删除订单测试即可。
4.5.3.部署联调
最后,测试没有问题,我们就可以将代码部署到开发环境去了。
我们在Jenkins中配置了web钩子,代码推送后自动触发构建。不过需要注意的是,默认情况下我们推送的代码不管是哪个分支都会触发构建,而且构建默认是基于lesson-init分支,需要重新配置。
更多极品IT资源 www.cx1314.cn
我们找到Jenkins控制台中的tjxt-dev-build任务:
[图片]
修改其中的配置。
第一个是哪些分支变化以后触发构建:
[图片]
第二个是构建时基于哪个分支构建:
[图片]
然后选择提交dev分支,并push到远端仓库:
[图片]
[图片]
然后到控制台,重新构建tj-trade服务:
[图片]
将本地服务停止,修改nacos中的虚拟机中的tj-trade实例权重为1:
[图片]
再次测试即可。
5.作业
天机学堂的产品原型地址如下:
阅读其中有关《个人中心-我的课程》有关功能原型,如图:
[图片]
思考下面几个问题: