初始项目
账号与密码
linux —-root/123321
mysql —-root/123
redis —-123321
Git私服 —-tjxt/123321
Jenkins 持续集成 —-root/123
RabbitMQ —-tjxt/123321
Nacos 控制台 —-nacos/nacos
xxl-job 控制台 —-admin/123456
1. 学习背景
开发场景:你刚刚进入了一家公司,进入了一个微服务项目组,参与一个微服务项目开发。我们会带着大家拉取代码、搭建开发环境、熟悉项目代码、熟悉业务流程、修改项目BUG、开发新功能、完成项目优化。通过整个项目的学习,真正掌握微服务架构技术栈,有能力解决微服务架构的各种问题。
2.天机学堂介绍
天机学堂是一个基于微服务架构的生产级在线教育项目,核心用户不是K12群体,而是面向成年人的非学历职业技能培训平台。
通过天机学堂项目,你能学习到在线教育中核心的学习辅助系统、考试系统,电商类项目的促销优惠系统等等。更能学习到微服务开发中的各种热点问题,以及不同场景对应的解决方案。学完以后你会收获很多的“哇塞”。
2.1项目亮点
- 电商
- 对接多种支付平台
- 优惠券系统设计方案
- 库存超卖解决方案
- 秒杀抢购解决方案
- 超时订单处理方案
- 在线学习
- 视频点播方案
- 学习辅助系统
- 考试评测系统
- 社交互动
- 互动问答系统
- 课程评价系统
- 通用点赞实现方案
- 积分系统实现方案
- 排行榜实现方案
2.2行业图谱
职业教育产业图谱:
职业教育产业链分为三大部分:
- 上游:由配套服务商、平台服务商、师资服务商和内容服务商构成。
- 中游:由学历和非学历的职业教育服务商 构成, 主要提供教育和培训服务。
- 下游:是职业教育需求方, 其中现阶段学历职业教育主要面向 15-22 岁的 C 端学生, 非学历职业培训的受众则更为广泛,基本覆盖了中考毕业以后所有年龄阶层的学生,此外职业技能培训和企业培训公司还向 B 端企业提供服务
- 天机学堂正是属于中游的非学历职业技能培训的一家企业。
2.3系统架构
天机学堂目前是一个B2C类型的教育网站,因此分为两个端:
- 后台管理端
- 用户端(PC网站)
整体架构如下:
2.4技术架构
2.5核心业务
- 老师核心业务
虽然流程并不复杂,但其中包含的业务繁多,例如:
- 课程分类管理:课程分类的增删改查
- 媒资管理:媒资的增删改查、媒资审核
- 题目管理:试题的增删改查、试题批阅、审核
- 课程管理:课程增删改查、课程上下架、课程审核、发布等等
- 学员核心业务
3.项目环境搭建
为了模拟真实的开发场景,我们设定的场景是这样的:天机学堂项目已经完成1.0.0版本60%的功能开发,能够实现项目的课程管理、课程购买等业务流程。现在需要加入课程学习、优惠促销、评价等功能。
相关微服务及1.0.0版本的完成状态如下:
微服务名称 | 功能描述 | 完成状态 |
---|---|---|
tj-parent | 父工程 | √ |
tj-common | 通用工程 | √ |
tj-message | 消息中心 | √ |
tj-gateway | 网关 | √ |
tj-auth | 权限服务 | √ |
tj-user | 用户服务 | √ |
tj-pay | 支付服务 | √ |
tj-course | 课程服务 | √ |
tj-exam | 考试服务 | O |
tj-search | 搜索服务 | √ |
tj-trade | 交易服务 | O |
tj-learning | 学习服务 | X |
tj-promotion | 促销服务 | X |
tj-media | 媒资服务 | √ |
tj-data | 数据服务 | O |
tj-remark | 评论服务 | X |
3.1企业开发模式
在企业开发中,微服务项目非常庞大,往往有十几个,甚至数十个,数百个微服务。而这些微服务也会交给不同的开发组去完成开发。你可能只参与其中的某几个微服务开发,那么问题来了:
- 如果我的微服务需要访问其它微服务怎么办?
- 难道说我需要把所有的微服务都部署到自己的电脑吗?
很明显,这样做是不现实的。第一,不是所有的代码你都有访问的权限;第二,你的电脑可能无法运行这数十、数百的微服务。
因此,企业往往会提供一个通用的公共开发、测试环境,在其中部署很多公共服务,以及其它团队开发好的、开发中的微服务。而我们大多数情况下只在本地运行正在开发的微服务,此时我们就需要一些其它的测试手段:- 单元测试:测试最小的可测试单元,
- 集成测试:验证某些功能接口,是否能与其它微服务正确交互
- 组件测试:验证微服务组件
- 端对端联调:验证整个系统
在天机学堂中,我们也给大家模拟了这样的一个开发环境,其中部署了各种公共服务,而我们只需要在本地开发未完成的几个服务即可:
3.2导入虚拟机
为了模拟企业中的开发环境,我们利用虚拟机搭建了一套开发环境,其中部署了开发常用的组件:
- Git私服(gogs):代码全部提交带了自己的Git私服,模拟企业开发的代码管理,大家也需要自行到私服拉取代码
- jenkins:持续集成,目前已经添加了所有部署脚本和Git钩子,代码推送会自动编译,可以根据需求手动部署
- nacos:服务注册中心、统一配置管理,大多数共享的配置都已经交给nacos处理
- seata:分布式事务管理
- xxl-job:分布式任务系统
- es:索引库
- redis:缓存库
- mysql:数据库
- kibana:es控制台
导入虚拟机说明
静态IP地址为192.168.150.101
子关为192.168.150.0
网关为192.168.150.2
起始IP为192.168.150.101- 验证:在本机的VMnet8查看ip是否为192.168.150.1
- 账号/密码:root/123321
- 测试网络:ping qq.com
3.3配置本机hosts
为了模拟使用域名访问,我们需要在本地配置hosts:1
2
3
4
5
6
7
8
9
10192.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
每个域名对应的服务列表如下:
名称 | 域名 | 账号 | 端口 |
---|---|---|---|
Git私服 | git.tianji.com | tjxt/123321 | 10880 |
Jenkins持续集成 | jenkins.tianji.com | root/123 | 18080 |
RabbitMQ | mq.tianji.com | tjxt/123321 | 15672 |
Nacos控制台 | nacos.tianji.com | nacos/nacos | 8848 |
xxl-job控制台 | xxljob.tianji.com | admin/123456 | 8880 |
ES的Kibana控制台 | es.tianji.com | - | 5601 |
微服务网关 | api.tianji.com | - | 10010 |
用户端入口 | www.tianji.com | - | 18081 |
管理端入口 | manage.tianji.com | - | 18082 |
3.4部署
微服务部署比较麻烦,所以企业中都会采用持续集成的方式,快捷实现开发、部署一条龙服务。
为了模拟真实环境,我们在虚拟机中已经提供了一套持续集成的开发环境,代码一旦自测完成,push到Git私服后即可自动编译部署。
而开发我们负责的微服务时,则需要在本地启动运行部分微服务。
虚拟机部署
项目已经基于Jenkins实现了持续集成,每当我们push代码时,就会触发项目完成自动编译和打包。
我们可以在Git仓库模拟代码push操作:
- 首先,访问http://git.tianji.com(tjxt/123321),找到tianji这个仓库,点击仓库设置按钮
- 然后,点击《管理Web钩子》菜单,进入页面后点击钩子后面的修改按钮
- 进入页面后,向下滚动,点击测试推送按钮
- 然后回到jenkins页面,会发现已经触发了tjxt-dev-build的自动编译
需要运行某个微服务时,我们只需要经过两步:
- 第一步,访问jenkins控制台:http://jenkins.tianji.com (账号:root/123)
- 第二步,点击对应微服务后面的运行按钮
- 第三步,可以在虚拟机中查看是否有容器,和nacos中是否有服务
我们需要分别启动几个开发完成的微服务:
- tj-user
- tj-auth
- tj-gateway
- tj-course
- tj-media
- tj-search
- tj-exam
- tj-data
- tj-trade
此时访问Nacos控制台,可以看到微服务都成功注册了 - 此时访问 http://www.tianji.com (jack/123 rose/123456)即可看到用户端页面:
- 此时访问 http://manage.tianji.com 即可看到管理端页面:
- 如果想要知道微服务具备哪些API接口,可以访问网关中的swagger页面,路径如下:http://api.tianji.com/doc.html
本地部署
对于需要开发功能的微服务,则需要在本地部署,不过首先我们要把代码拉取下来。
查看Git私服的代码:http://git.tianji.com/tjxt/tianji :
- 克隆代码到本地 git clone http://192.168.150.101:10880/tjxt/tianji.git -b lesson-init
- 在idea中打开
- 把分支改为lesson-init
- 删除多余的模块
- 对父工程进行clean和install
- 在本地运行项目时,设置Active Profiles为local
- 在nacos中查看服务是否为192.168.159.1
本机ip一定要设置为192.168.150.1,不能设置为localhost
因为有的服务在虚拟机内,当虚拟机服务要访问本机服务时,如何ip为localhost则指向虚拟机内的地址,不能访问本机地址
4.BUG修复
问题:
都是普通用户,为什么用户jack可以删除自己的订单,而Rose则不可以删除?
4.1熟悉项目
项目结构
我们先来看看项目结构,目前企业微服务开发项目结构有两种模式:
- 项目下的每一个微服务,都创建为一个独立的Project,有独立的Git仓库,尽可能降低耦合
- 项目创建一个Project,项目下的每一个微服务都是一个Module,方便管理
天机学堂采用的正是第二种模式,结构如图:
实体类规范
实体类规范
在天机学堂项目中,所有实体类按照所处领域不同,划分为4种不同类型:
- DTO:数据传输对象,在客户端与服务端间传递数据,例如微服务之间的请求参数和返回值、前端提交的表单
- PO:持久层对象,与数据库表一一对应,作为查询数据库时的返回值
- VO:视图对象,返回给前端用于封装页面展示的数据
- QUERY:查询对象,一般是用于封装复杂查询条件
依赖注入
依赖注入
Spring提供了依赖注入的功能,方便我们管理和使用各种Bean,常见的方式有:
- 字段注入(@Autowired 或 @Resource)
- 构造函数注入
- set方法注入
@Autowired这种模式是不被Spring推荐的,Spring推荐的是基于构造函数注入,像这样
但是,如果需要注入的属性较多,构造函数就会非常臃肿,代码写起来也比较麻烦。
好在Lombok提供了一个注解@RequiredArgsConstructor,可以帮我们生成构造函数,简化代码:
异常处理
在项目运行过程中,或者业务代码流程中,可能会出现各种类型异常,为了加以区分,我们定义了一些自定义异常对应不同场景:
在开发业务的过程中,如果出现对应类型的问题,应该优先使用这些自定义异常。
当微服务抛出这些异常时,需要一个统一的异常处理类,同样在tj-common模块中定义了: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
public class CommonExceptionAdvice {
public Object handleDbException(DbException e) {
log.error("mysql数据库操作异常 -> ", e);
return processResponse(e.getStatus(), e.getCode(), e.getMessage());
}
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());
}
public Object handleFeignException(FeignException e) {
log.error("feign远程调用异常 -> ", e);
return processResponse(e.status(), e.status(), e.contentUTF8());
}
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);
}
public Object handleBindException(BindException e) {
log.error("请求参数绑定异常 ->BindException, {}", e.getMessage());
log.debug("", e);
return processResponse(400, 400, "请求参数格式错误");
}
public Object handleNestedServletException(NestedServletException e) {
log.error("参数异常 -> NestedServletException,{}", e.getMessage());
log.debug("", e);
return processResponse(400, 400, "请求参数异常");
}
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("|"))
);
}
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);
}
}
配置文件
文件 | 说明 |
---|---|
bootstrap.yml | 通用配置属性,包含服务名、端口、日志等等各环境通用信息 |
bootstrap-dev.yml | 线上开发环境配置属性,虚拟机中部署使用 |
bootstrap-local.yml | 本地开发环境配置属性,本地开发、测试、部署使用 |
项目中的很多共性的配置都放到了Nacos配置中心管理:
例如mybatis、mq、redis等,都有对应的shared-xxx.yaml共享配置文件。在微服务中如果用到了相关技术,无需重复配置,只要引用上述共享配置即可:
4.2阅读源码
- BUG重现
理清请求链路
- 由页面得到 DELETE —- http://api.tianji.com/ts/orders/1597502678241378305
- 被nginx分析代理到 —- http://192.168.101:10010/ts/orders/1597502678241378305 (虚拟机nginx配置文件)
- 被gateway路由到 —- http://192.168.150.101:8088/ts/orders/1597502678241378305 (网关配置)
- 最终定位到这个位置
这段代码表名,jack不会进入到if判断里面,rock进入到if判断里面
- 这是因为userId是long类型,比较判断用的是==号,long类型只能比较-128到129之间的数字
- 应改为!userId.equals(order.getUserId())
远程调试
远程调试
- 如果部署的微服务不在本地,我们可以利用IDEA的远程调试功能
- 关闭docker容器里面的tj-trade
- 部署tj-trade-debug
- 启动调试 控制台输出:Connected to the target VM, address: ‘192.168.150.101:5005’, transport: ‘socket’
本地调试
启动后可以在nacos中看到2个实例,下线掉虚拟机中的实例
如果下线报错docker exec -it nacos /bin/bash 进入到nacos容器
cd data
rm -rf protocol/ 删除protocol文件夹
exit 退出容器
docker restart naocos 重启nacos服务我使用本地调试出现服务器内部错误,所以后面使用远程调试
- 如果部署的微服务不在本地,我们可以利用IDEA的远程调试功能
分支管理及部署
- 创建新分支hotfix-delete-order-error
- 推送到git
- 切换到lesson分组,合并hotfixd分支然后推送
- 小老头就会自动构建并部署项目
- docker删除trade的容器及镜像
- 重新构建trade