账号与密码

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项目亮点

  1. 电商
    • 对接多种支付平台
    • 优惠券系统设计方案
    • 库存超卖解决方案
    • 秒杀抢购解决方案
    • 超时订单处理方案
  2. 在线学习
    • 视频点播方案
    • 学习辅助系统
    • 考试评测系统
  3. 社交互动
    • 互动问答系统
    • 课程评价系统
    • 通用点赞实现方案
    • 积分系统实现方案
    • 排行榜实现方案

2.2行业图谱

职业教育产业图谱:
code
职业教育产业链分为三大部分:

  • 上游:由配套服务商、平台服务商、师资服务商和内容服务商构成。
  • 中游:由学历和非学历的职业教育服务商 构成, 主要提供教育和培训服务。
  • 下游:是职业教育需求方, 其中现阶段学历职业教育主要面向 15-22 岁的 C 端学生, 非学历职业培训的受众则更为广泛,基本覆盖了中考毕业以后所有年龄阶层的学生,此外职业技能培训和企业培训公司还向 B 端企业提供服务
    code
  • 天机学堂正是属于中游的非学历职业技能培训的一家企业。

2.3系统架构

天机学堂目前是一个B2C类型的教育网站,因此分为两个端:

  • 后台管理端
  • 用户端(PC网站)
    整体架构如下:
    code

2.4技术架构

code

2.5核心业务

  1. 老师核心业务
    code
    虽然流程并不复杂,但其中包含的业务繁多,例如:
  • 课程分类管理:课程分类的增删改查
  • 媒资管理:媒资的增删改查、媒资审核
  • 题目管理:试题的增删改查、试题批阅、审核
  • 课程管理:课程增删改查、课程上下架、课程审核、发布等等
  1. 学员核心业务
    code

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企业开发模式

在企业开发中,微服务项目非常庞大,往往有十几个,甚至数十个,数百个微服务。而这些微服务也会交给不同的开发组去完成开发。你可能只参与其中的某几个微服务开发,那么问题来了:

  • 如果我的微服务需要访问其它微服务怎么办?
  • 难道说我需要把所有的微服务都部署到自己的电脑吗?

    很明显,这样做是不现实的。第一,不是所有的代码你都有访问的权限;第二,你的电脑可能无法运行这数十、数百的微服务。
    因此,企业往往会提供一个通用的公共开发、测试环境,在其中部署很多公共服务,以及其它团队开发好的、开发中的微服务。而我们大多数情况下只在本地运行正在开发的微服务,此时我们就需要一些其它的测试手段:

  • 单元测试:测试最小的可测试单元,
  • 集成测试:验证某些功能接口,是否能与其它微服务正确交互
  • 组件测试:验证微服务组件
  • 端对端联调:验证整个系统

在天机学堂中,我们也给大家模拟了这样的一个开发环境,其中部署了各种公共服务,而我们只需要在本地开发未完成的几个服务即可:
code

3.2导入虚拟机

为了模拟企业中的开发环境,我们利用虚拟机搭建了一套开发环境,其中部署了开发常用的组件:

  • Git私服(gogs):代码全部提交带了自己的Git私服,模拟企业开发的代码管理,大家也需要自行到私服拉取代码
  • jenkins:持续集成,目前已经添加了所有部署脚本和Git钩子,代码推送会自动编译,可以根据需求手动部署
  • nacos:服务注册中心、统一配置管理,大多数共享的配置都已经交给nacos处理
  • seata:分布式事务管理
  • xxl-job:分布式任务系统
  • es:索引库
  • redis:缓存库
  • mysql:数据库
  • kibana:es控制台
    code
    导入虚拟机说明

    静态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
10
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

每个域名对应的服务列表如下:

名称 域名 账号 端口
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私服后即可自动编译部署。
code
而开发我们负责的微服务时,则需要在本地启动运行部分微服务。

虚拟机部署

项目已经基于Jenkins实现了持续集成,每当我们push代码时,就会触发项目完成自动编译和打包。
我们可以在Git仓库模拟代码push操作:

需要运行某个微服务时,我们只需要经过两步:

  • 第一步,访问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控制台,可以看到微服务都成功注册了
    code
  • 此时访问 http://www.tianji.com (jack/123 rose/123456)即可看到用户端页面:
    code
  • 此时访问 http://manage.tianji.com 即可看到管理端页面:
    code
  • 如果想要知道微服务具备哪些API接口,可以访问网关中的swagger页面,路径如下:http://api.tianji.com/doc.html
本地部署

对于需要开发功能的微服务,则需要在本地部署,不过首先我们要把代码拉取下来。
查看Git私服的代码:http://git.tianji.com/tjxt/tianji

  1. 克隆代码到本地 git clone http://192.168.150.101:10880/tjxt/tianji.git -b lesson-init
  2. 在idea中打开
  3. 把分支改为lesson-init
  4. 删除多余的模块
  5. 对父工程进行clean和install
  6. 在本地运行项目时,设置Active Profiles为local
  7. 在nacos中查看服务是否为192.168.159.1

    本机ip一定要设置为192.168.150.1,不能设置为localhost
    因为有的服务在虚拟机内,当虚拟机服务要访问本机服务时,如何ip为localhost则指向虚拟机内的地址,不能访问本机地址

4.BUG修复

问题:
都是普通用户,为什么用户jack可以删除自己的订单,而Rose则不可以删除?

4.1熟悉项目

项目结构

我们先来看看项目结构,目前企业微服务开发项目结构有两种模式:

  1. 项目下的每一个微服务,都创建为一个独立的Project,有独立的Git仓库,尽可能降低耦合
  2. 项目创建一个Project,项目下的每一个微服务都是一个Module,方便管理
    天机学堂采用的正是第二种模式,结构如图:
    code
实体类规范

实体类规范
在天机学堂项目中,所有实体类按照所处领域不同,划分为4种不同类型:

  • DTO:数据传输对象,在客户端与服务端间传递数据,例如微服务之间的请求参数和返回值、前端提交的表单
  • PO:持久层对象,与数据库表一一对应,作为查询数据库时的返回值
  • VO:视图对象,返回给前端用于封装页面展示的数据
  • QUERY:查询对象,一般是用于封装复杂查询条件
依赖注入

依赖注入
Spring提供了依赖注入的功能,方便我们管理和使用各种Bean,常见的方式有:

  • 字段注入(@Autowired 或 @Resource)
  • 构造函数注入
  • set方法注入

    @Autowired这种模式是不被Spring推荐的,Spring推荐的是基于构造函数注入,像这样
    code
    但是,如果需要注入的属性较多,构造函数就会非常臃肿,代码写起来也比较麻烦。
    好在Lombok提供了一个注解@RequiredArgsConstructor,可以帮我们生成构造函数,简化代码:
    code

异常处理

在项目运行过程中,或者业务代码流程中,可能会出现各种类型异常,为了加以区分,我们定义了一些自定义异常对应不同场景:
code
在开发业务的过程中,如果出现对应类型的问题,应该优先使用这些自定义异常。
当微服务抛出这些异常时,需要一个统一的异常处理类,同样在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
@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);
}
}

配置文件
文件说明
bootstrap.yml通用配置属性,包含服务名、端口、日志等等各环境通用信息
bootstrap-dev.yml线上开发环境配置属性,虚拟机中部署使用
bootstrap-local.yml本地开发环境配置属性,本地开发、测试、部署使用

项目中的很多共性的配置都放到了Nacos配置中心管理:
例如mybatis、mq、redis等,都有对应的shared-xxx.yaml共享配置文件。在微服务中如果用到了相关技术,无需重复配置,只要引用上述共享配置即可:
code

4.2阅读源码

  1. BUG重现
  2. 理清请求链路

  3. 远程调试

    远程调试
    • 如果部署的微服务不在本地,我们可以利用IDEA的远程调试功能
      code
    • 关闭docker容器里面的tj-trade
    • 部署tj-trade-debug
    • 启动调试 控制台输出:Connected to the target VM, address: ‘192.168.150.101:5005’, transport: ‘socket’
    本地调试

    code
    启动后可以在nacos中看到2个实例,下线掉虚拟机中的实例
    如果下线报错

    docker exec -it nacos /bin/bash 进入到nacos容器
    cd data
    rm -rf protocol/ 删除protocol文件夹
    exit 退出容器
    docker restart naocos 重启nacos服务

    我使用本地调试出现服务器内部错误,所以后面使用远程调试

  4. 分支管理及部署
    code

    1. 创建新分支hotfix-delete-order-error
    2. 推送到git
    3. 切换到lesson分组,合并hotfixd分支然后推送
    4. 小老头就会自动构建并部署项目
    5. docker删除trade的容器及镜像
    6. 重新构建trade