一、微服务演变过程版本1.0
几年前,小明和小皮一起创业做网上超市。小明负责程序开发,小皮负责其他事宜。当时互联网还不发达,网上超市还是蓝海。只要功能实现了就能随便赚钱。所以他们的需求很简单,只需要一个网站挂在公网,用户能够在这个网站上浏览商品、购买商品;另外还需一个管理后台,可以管理商品、用户、以及订单数据。
我们整理一下功能清单:
由于需求简单,小明左手右手一个慢动作,网站就做好了。管理后台出于安全考虑,不和网站做在一起,小明右手左手慢动作重播,管理网站也做好了。总体架构图如下:
小明挥一挥手,找了家云服务部署上去,网站就上线了。上线后好评如潮,深受各类肥宅喜爱。小明小皮美滋滋地开始躺着收钱。
版本2.0
好景不长,没过几天,各类网上超市紧跟着拔地而起,对小明小皮造成了强烈的冲击。
在竞争的压力下,小明小皮决定开展一些营销手段:
这些活动都需要程序开发的支持。小明拉了同学小红加入团队。小红负责数据分析以及移动端相关开发。小明负责促销活动相关功能的开发。
因为开发任务比较紧迫,小明小红没有好好规划整个系统的架构,随便拍了拍脑袋,决定把促销管理和数据分析放在管理后台里,微信和移动端APP另外搭建。通宵了几天后,新功能和新应用基本完工。这时架构图如下:
这一阶段存在很多不合理的地方:
尽管有着诸多问题,但也不能否认这一阶段的成果:快速地根据业务变化建设了系统。不过紧迫且繁重的任务容易使人陷入局部、短浅的思维方式,从而做出妥协式的决策。在这种架构中,每个人都只关注在自己的一亩三分地,缺乏全局的、长远的设计。长此以往,系统建设将会越来越困难,甚至陷入不断推翻、重建的循环。
版本3.0
幸好小明和小红是有追求有理想的好青年。意识到问题后,小明和小红从琐碎的业务需求中腾出了一部分精力,开始梳理整体架构,针对问题准备着手改造。
要做改造,首先你需要有足够的精力和资源。如果你的需求方(业务人员、项目经理、上司等)很强势地一心追求需求进度,以致于你无法挪出额外的精力和资源的话,那么你可能无法做任何事……
在编程的世界中,最重要的便是抽象能力。微服务改造的过程实际上也是个抽象的过程。小明和小红整理了网上超市的业务逻辑,抽象出公用的业务能力,做成几个公共服务:
各个应用后台只需从这些服务获取所需的数据,从而删去了大量冗余的代码,就剩个轻薄的控制层和前端。这一阶段的架构如下:
这个阶段只是将服务分开了,数据库依然是共用的,所以一些烟囱式系统的缺点仍然存在:
如果一直保持共用数据库的模式,则整个架构会越来越僵化,失去了微服务架构的意义。因此小明和小红一鼓作气,把数据库也拆分了。所有持久化层相互隔离,由各个服务自己负责。另外,为了提高系统的实时性,加入了消息队列机制。架构如下:
完全拆分后各个服务可以采用异构的技术。比如数据分析服务可以使用数据仓库作为持久化层,以便于高效地做一些统计计算;商品服务和促销服务访问频率比较大,因此加入了缓存机制等。
还有一种抽象出公共逻辑的方法是把这些公共逻辑做成公共的框架库。这种方法可以减少服务调用的性能损耗。但是这种方法的管理成本非常高昂,很难保证所有应用版本的一致性。 数据库拆分也有一些问题和挑战:比如说跨库级联的需求,通过服务查询数据颗粒度的粗细问题等。但是这些问题可以通过合理的设计来解决。总体来说,数据库拆分是一个利大于弊的。
微服务架构还有一个技术外的好处,它使整个系统的分工更加明确,责任更加清晰,每个人专心负责为其他人提供更好的服务。在单体应用的时代,公共的业务功能经常没有明确的归属。最后要么各做各的,每个人都重新实现了一遍;要么是随机一个人(一般是能力比较强或者比较热心的人)做到他负责的应用里面。在后者的情况下,这个人在负责自己应用之外,还要额外负责给别人提供这些公共的功能——而这个功能本来是无人负责的,仅仅因为他能力较强/比较热心,就莫名地背锅(这种情况还被美其名曰能者多劳)。结果最后大家都不愿意提供公共的功能。长此以往,团队里的人渐渐变得各自为政,不再关心全局的架构设计。
问题:
如果没有注册中心,URL异常麻烦; 大量配置,远程访问很麻烦; 大量配置,配置没有统一管理; 服务的熔断降级; 链路追踪; 需要一些服务进行支撑;版本4.0
网关可以做,统一的鉴权,权限控制。
二、微服务拆分划分原则(面试:考核业务划分的想法)
我们拆分微服务的时候需要按照某些原则进行拆分.
基于上诉的拆分原则,我们可以针对骡窝窝项目进行微服务的拆分:
假如我们按照业务来划分,根据粒度大小,可能存在以下两种:
3 VS 6,这该怎么办?
如果你的团队只有9个人,那么分成3个是合理的,如果有18个人,那么6个服务是合理的。这里引入团队成员进行协助拆分。
在拆分遇到争议的时候,一般情况下我们增加一项拆分条件,虽然不是充要条件,但至少我们的答案会更加接近真理。
除了业务可能存在争议,其他的划分也会有争议,比如一个独立的服务到底需要多少人员的配置?
为什么说是三个人分配一个服务(当然,成员主要是后端人员)?
那么这个3是不是就是稳定的数量呢?
假设你做的是边开飞机边换引擎的重写工作,那么前期3个人都可能捉襟见肘。但是到了服务后期,你可能1个就够了。
微服务实践项目结构图
基础模块搭建01.parent模块
管理统一依赖;
依赖:
org.springframework.boot
spring-boot-starter-parent
2.1.4.RELEASE
1.8
Greenwich.SR1
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
02.注册中心Euraka
提供服务的注册和发现功能;
依赖:eureka-server
org.springframework.cloud
spring-cloud-starter.NETflix-eureka-server
配置application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/euraka
fetch-registry: false
register-with-eureka: false
03.配置中心Config-Server
依赖
eureka-client:
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
config-server:
org.springframework.cloud
spring-cloud-config-server
配置application.yml
server:
port: 9100
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri:
username:
password:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
04.网关Zuul
依赖
eureka-client:
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
netflix-zuul:
org.springframework.cloud
spring-cloud-starter-netflix-zuul
config-client:
org.springframework.cloud
spring-cloud-config-client
配置
bootstrap.yml:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: zuul-server
cloud:
config:
discovery:
enabled: true
service-id: config-server
label: master
name: zuul-server
远程托管平台的文件:zuul-server.yml:
port: 9000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
zuul:
sensitive-headers:
forceOriginalQueryStringEncoding: true #强制采用原始请求的编码格式,即不对Get请求参数做编解码
ignored-patterns: /*-server/** #忽略匹配这个格式的路径
05.公共模块common
依赖
lombok:
org.projectlombok
lombok
web:
org.springframework.boot
spring-boot-starter-web
06.服务接口父模块provider-api
07.服务父模块provider-server
前端项目
本来由前端人员写好的前端项目;
创建frontend-website组件,和任何项目没有关系,引入依赖:
org.springframework.boot
spring-boot-starter-parent
2.1.4.RELEASE
org.springframework.boot
spring-boot-starter-web
创建启动类。
启动发出注册请求,发现访问的是8080端口,微服务的项目,不再是直接访问到某个Tomcat服务器了。而是需要通过网关:
微服务结构,请求发到 zuul 网关,当zuul接受到请求后,首先会由前置过滤器进行处理,然后在由路由过滤器具体把请求转发到后端应用,然后在执行后置过滤器把执行结果返回到请求方。
修改static/js/vue/common.js中的url访问的端口;
微服务架构发送请求到返回数据的执行流程:
聚合服务website-server结构&依赖关系处理
在provider-server模块中添加依赖,因为下面三个依赖是其模块下所有的server模块都要用到的,所以公共所有的依赖在方法父项目中添加,子项目可以继承到:
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-config-client
配置文件
bootstrap.yml
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: website-server
cloud:
config:
discovery:
enabled: true
service-id: config-server
label: master
name: website-server
码云的website-server.yml
server:
port: 8082
定义路由规则
码云的zuul-server.yml
zuul:
routes:
member-server-route:
path: /website/**
service-id: website-server
加启动类启动测试:
跨域问题:直接在网关位置就可以处理跨域的问题:
跨域原因:域名、端口、ip不一样的时候会产生跨域问题。默认不允许跨域的原因:对被访问的域外资源来说,服务器压力变大了,假设不信任的恶意攻击可能导致访问量过大宕机。所有需要对信任的域名做配置。预检请求会有一个缓存,除了第一次之外其他的都是一次请求。详情见单独的文档;
在zuul-server中设置跨域:
解决跨域的问题:设置的位置,在zuul-server中的启动类中添加设置:
// 解决跨域问题
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// #允许访问的头信息,*表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,*表示全部允许
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
// 允许Get的请求方法
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
测试发现跨域的错误信息已经没有了。接下来写controller来处理我们的请求;
响应类&统一异常:
controller的设计:返回类型需要确定
返回值:以前单体项目返回的是Object;现在是微服务项目,我们希望有一个状态码,成功带上数据,错误带上错误信息:
返回类型应该是什么样的?那么应该怎么定义字段:
所有的返回对象都是Result对象,具体的数据封装在result对象的data中;
在controller中,会调用到各种业务层的业务方法:业务层中必须考虑到执行过程中出现异常的情况,需要回滚:
设计的错误状态码和错误信息是一一对应的,应该是常量(枚举类或者自定义类来封装他们)
自定义异常:来解决业务方法执行过程中出现的异常,即抛出的异常里要包含了刚刚封装了错误状态码和错误信息的对象,在throw的时候返回给调用者,由调用者(controller)来处理。
业务层的业务方法中写操作等,必须要保持一致性(事务),当出现异常的时候需要将异常throw出去。
聚合服务中,需要对有可能出现的各种异常进行统一的处理,AOP思想。出现异常,将封装了状态码和异常信息的result返回给前台,前台才能够接受到具体的错误和错误信息。
实现:异常的处理基本的类是所有的微服务都需要有的,所以放到common中
CodeMsg
* 封装状态码和信息
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class CodeMsg implements Serializable {
private Integer code;
private String msg;
Result
* 返回前台的数据类型
* @param
@Setter
@Getter
public class Result implements Serializable {
public static final int SUCCESS_CODE = 200;//成功码.
public static final String SUCCESS_MESSAGE = "操作成功";//成功信息.
public static final int ERROR_CODE = 500000;//错误码.
public static final String ERROR_MESSAGE = "系统异常";//错误信息.
private int code;
private String msg;
private T data;
public Result(){}
private Result(int code, String msg, T data){
this.code = code;
this.msg = msg;
this.data = data;
public static Result success(T data){
return new Result(SUCCESS_CODE,SUCCESS_MESSAGE,data);
public static Result success(String msg,T data){
return new Result(SUCCESS_CODE,msg,data);
public static Result error(CodeMsg codeMsg){
return new Result(codeMsg.getCode(),codeMsg.getMsg(),null);
public static Result defaultError(){
return new Result(ERROR_CODE,ERROR_MESSAGE,null);
public boolean hasError(){
//状态吗!=200 说明有错误.
return this.code!=SUCCESS_CODE;
BusinessException
* 自定义异常
@Setter
@Getter
public class BusinessException extends RuntimeException {
private CodeMsg codeMsg;
public BusinessException(CodeMsg codeMsg){
this.codeMsg = codeMsg;
CommonExceptionAdvice
* 公共的sdvice,不贴 @ControllerAdvice 注解,因为它是让其他服务来继承的基类
public class CommonExceptionAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result hanlderDefaultException(Exception e){
e.getMessage();
return Result.defaultError();
会员服务:结构&依赖关系处理模块provider-api中添加依赖:
因为该模块下的所有的api都要提供给其他的服务来调用,所以需要在api的父项目中添加这三个依赖:
cn.wolfcode.xloud.luowowo
common
1.0.0
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
模块member-api:暴露会员服务接口
用户的信息使用mongodb存储的,所以要引入mongodb的依赖:
org.springframework.boot
spring-boot-starter-data-mongodb
true
UserInfo
@Setter
@Getter
@Document("userInfo")@ToString
public class UserInfo implements Serializable {
public static final int GENDER_SECRET = 0; //保密
public static final int GENDER_MALE = 1; //男
public static final int GENDER_FEMALE = 2; //女
public static final int STATE_NORMAL = 0; //正常
public static final int STATE_DISABLE = 1; //冻结
@Id
protected String id;
private String nickname; //昵称
private String phone; //手机
private String email; //邮箱
private String password; //密码
private int gender = GENDER_SECRET; //性别
private int level = 0; //用户级别
private String city; //所在城市
private String headImgUrl; //头像
private String info; //个性签名
private int state = STATE_NORMAL; //状态
模块member-server:会员服务,api负责暴露接口,这里负责具体的业务
添加依赖:
member-api依赖、redis依赖、fastjson依赖
cn.wolfcode.xloud.luowowo
menber-api
1.0.0
org.springframework.boot
spring-boot-starter-data-redis
com.alibaba
fastjson
1.2.47
bootstrap.yml
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: member-server
cloud:
config:
discovery:
enabled: true
service-id: config-server
label: master
name: member-server,redis
member-server.yml
server:
port: 8080
spring:
data:
mongodb:
uri: mongodb://192.168.20.130:27017/member
创建微服务对应的数据库,拆分以前单体项目的表:
![image-2020051621441662
在模块menber-server中准备repository接口,来访问mongodb数据库的接口;
@Repository
public interface UserInfoRepository extends MongoRepository {
* 通过号码查询用户信息
* @param phone
* @return
UserInfo findByPhone(String phone);
* 通过号码和莫密码查询用户信息
* @param username
* @param password
* @return
UserInfo getByPhoneAndPassword(String username, String password);
服务之间调用:
聚合服务需要调用会员服务中的方法。需要在menber-api中用feign负载均衡将menber-server的业务方法暴露出来一个接口;
// 类似controller
@FeignClient(name = "member-server") //负载访问的服务
public interface MemberFeignApi {
@RequestMapping("/checkPhone") //映射方法
Result checkPhone(@RequestParam("phone") String phone); //映射参数
返回类型是
Result
的原因:
因为,基础服务也有可能出现异常,直接返会封装了错误状态码和错误信息的Result 回去,聚合服务可以直接通过result中boolean值来区分对异常的处理情况;
在member-server中,需要对接口做具体的实现了:
需要在server中对异常做统一的处理:
每个服务的异常有不相同的,比如注册的错和登录的错就不一样,怎么体现各个服务之间的异常差异呢?可能会有其他的异常比如 连接数据库超时的异常。自己实现具体的CoreMsg:
* member-server服务中返回的异常结果:封装code和msg
public class MemberServerCodeMsg extends CodeMsg {
//用父类的构造器来完成初始化操作
public MemberServerCodeMsg(Integer code, String msg){
super(code, msg);
public static final MemberServerCodeMsg DEFAULT_ERROR =
new MemberServerCodeMsg(500100, "会员服务繁忙!");
public static final MemberServerCodeMsg PHONE_EXIST_ERROR =
new MemberServerCodeMsg(500101, "手机号码已存在!");
异常的advice增强类,继承增强基类:
* 需要在该增强类中处理member-server中特有的异常
@ControllerAdvice
public class MemberServerExceptionAdvice extends CommonExceptionAdvice {
* 处理普通的Exception
* @param ex
* @return
@ExceptionHandler(Exception.class)
@ResponseBody
public Result handlerDefault(Exception ex){
ex.printStackTrace();
return Result.error(MemberServerCodeMsg.DEFAULT_ERROR); //返回自己的默认异常
那具体抛出哪一个server中的异常就看在呢一个server中出现异常了;
熔断降级:
既然可能在某个服务中会出现异常,那我们就不能让出现异常的时候使服务出现宕机而影响其它服务的正常运行。所以应该要想到对服务做熔断降级的处理;
* 对服务可能出现异常做熔断降级处理
@Component
public class UserInfoFeignHystrix implements UserInfoFeignApi{
@Override
public Result checkPhone(String phone) {
//熔断降级之后,应该做的有些处理
return null;
回退类和方法完成之后,在api接口的注解上加上:
注意:feign默认是没有开启Hystrix的,需要添加配置进行开启:直接放到码云中的website-server的配置文件中
# hystrix默认是关闭的,需要手动开启一下,否则一直是超时的
feign:
hystrix:
enabled: true
到此,member-server的模块就完成了,当服务出现熔断会调用到降级方法;
注入服务接口对象,完成远程调用:
注入需要引入menber-api的依赖:
返回的数据应该是什么样的,怎么解决出现异常的情况:
实现自己服务的返回CodeMsg:
public class WebsiteServerCodeMsg extends CodeMsg {
public WebsiteServerCodeMsg(Integer code, String msg) {
super(code, msg);
public static final WebsiteServerCodeMsg MEMBER_SERVER_ERROR =
new WebsiteServerCodeMsg(500201, "会员服务繁忙!");
public static final WebsiteServerCodeMsg DEFAULT_ERROR =
new WebsiteServerCodeMsg(500200, "聚合服务繁忙!");
controller
@RestController
@RequestMapping("/userRegister")
public class UserRegisterController {
@Autowired
private UserInfoFeignApi userInfoFeignApi;
@RequestMapping("/checkPhone")
public Result checkPhone(String phone){
//检查号码
Result result = userInfoFeignApi.checkPhone(phone);
//result返回的情况有3种,需要区分开
if (result == null){
//返回null。说明member-server服务宕机了,对应的CodeMsg应该重新设计
return Result.error(WebsiteServerCodeMsg.MEMBER_SERVER_ERROR);
//到这里,表示远程调用成功,远程的服务返回的是一个result,直接返回即可
//返回参数
return result;
还需要在启动类上加注解:@EnableFeignClients,spring才能扫描到API接口,才能创建代理对象,代理对象才能发起服务器之间的远程调用。
website-server对异常的统一处理:
* 对website-server的异常统一处理
@ControllerAdvice
public class WebsiteServerExceptionAdvice extends CommonExceptionAdvice{
@ExceptionHandler(Exception.class)
@ResponseBody
public Result handlerDefaultException(Exception ex){
ex.printStackTrace();
return Result.error(WebsiteServerCodeMsg.DEFAULT_ERROR);
启动测试:
两处错误:
最终:
微服务架构逐渐成熟,如何做到相对独立,成了当前重点考虑的问题。
我将分割了以下基础模块:
user-service 用户认证服务
shopping-cart-service 购物车服务
info-version-service 信息版本服务
user-edit-service 用户编辑服务
order-service 订单服务
seller-info-service 店铺信息
discount-Service 折扣 促销服务
inventory-service 库存服务
account-service 账户会员服务
payment-service 支付组件
promotion-service 促销服务Base
Search-service 搜索服务
concern-Service 互动服务
tracks-service 足迹
Cms Search-service CMS搜索服务
Seller-service 商户认证服务
product-Mgr-service 商品管理服务
Order-mgr-service 订单服务
Catalog-mgr-service 产品目录
Seller-edit-service 商户编辑服务
sendout-service 发货服务/推送
Grant-service 权限服务
employee-service 雇员认证服务
employee-mgr-service 雇员服务
Promotion-mgr-Service 促销管理
static-service 静态化服务
product-Mgr-service 商品管理服务
Order-mgr-service 订单服务
Catalog-mgr-service 产品目录
dict-service 字典管理服务
Cms-service CMS服务
Seller-edit-service 商户编辑服务