ElasticStack-1-前言

1.为什么使用ElasticStack

我们知道的一般大数据分析工具有如下:

我们为什么还需要使用ElasticStack呢?
原因:

  1. 1.使用门槛低、开发周期短、上线快
  2. 2.性能好、查询快、实时展示结果
    对T+1说不,以前我们查询数据结果,都是时隔一段时间或者一天再分析,现在我们需要”现在、立刻马上要数据”
  3. 3.扩容方便、快速支撑增长迅猛的数据
    刚开始也许只是GB数据、随着业务量变大、TB、PB级别的数据也不是问题。

    2.简介

    2.1 ElasticStack组成

    传统的ELK其实就是:Elasticsearch、Logstash、Kibana,自从加入了Beats后,进行升级,变成了Elastic Stack。

    2.2 各个模块功能

  4. 1.Elasticsearch主要是做数据处理的

  5. 2.Beats和 Logstash是做数据收集与处理,相当于早期的ETL
  6. 3.Kibna数据搜索与可视化分析
2.3 ElasticStack功能

一定要内外兼修、不仅仅会使用API还得了解原理。

  1. 1.ElasticStack是一个完备的数据分析工具集合
  2. 2.学习安排:
  3. 3.Elasticsearch篇章讲解举例

  4. 4.实践项目

ipcc-1-ipcc整体架构

1.ipcc总体架构模块

2.系统模块图

2.1 acd模块

  1. 1.acd模块
    acd在平台中的位置

  2. 2.配置坐席呼出
    https://blog.csdn.net/weixin_34242819/article/details/90492301
    对于登录坐席工号进行呼出的情况,我们有如下步骤:1.坐席签入后能上线成功 2.坐席能够正确路由到运营商的sip电话进行呼叫

2.1.1 坐席签入后能上线成功
坐席上线

1.在cti_work表中找到分机,如:100003。
2.配置100003到cti_work_queue中去
3.在cti_outcaller中配置上线的主叫

3.在cti_telno中配置分机路由:100003分机呼出使用内线字段、长度在1-6(因为:100003是6位数)、

4.查看/cc/bin/log下面的日志:

发现话机没有注册,打开一看:是注册的服务错了

5.如果出问题,查看Acdgate模块下Tomcat日志,在查看/cc/log/acd_acd01.log下的日志
acd_acd01.log里面r:开头的。

机器人锁定坐席转人工

我们在排查之前可以关掉hmp,设置:hmpcheck=0

1.在确保坐席能够上线情况下,先配置转人工的机器人场景:1013502011

2.在批次场景里面创建人批次场景,然后创建对应批次,进行外呼
3.查看acd_acd01_xxxx.log下的日志:锁定坐席成功与否?—>TRptque::getandlockagent

4.外呼后查看日志:httpg_httpg01_xxxx.log

查看机器人转人工文档说明书知道:”att_status”:”0”不在线,”att_status”:”1”在线
5.如果和ai交互时候,传递的参数不对的话,我们查看acd模块:
TRptque::getandlockagent res=[-1]表示没有获取到空闲坐席
6.如果转人工时候,坐席挂不断,说明是坐席表里面的相同workno有多条。

6.正常交互如下:
acd:2019-09-15 09:48:03.453 TRptque::getandlockagent res=[0], workno=[100002] quueueid=[52] usid = [90046126300563]
httpg:ai获取空闲坐席:key:[{“userid”:”900461263”,”inaction”:0,”inparams”:{“call_id”:”900461263”,”call_sor_id”:”15188317019”,”call_dst_id”:”051289578445”,”att_status”:”1”}}].
ai转人工传递queueid:ok:0,resultbuf[{“ret”:0,”userid”:”900461263”,”outaction”:”11”,”outparams”:{“call_dst_id”:”051289578445”,”queue_id”:”52”,”call_id”:”900461263”,”inter_idx”:”8”,”call_sor_id”:”15188317019”}}].

2.1.2 正确路由到运营商的sip电话进行呼叫

1.acd.cfg配置(配置是不是开启did,did是优先级最高的,如果开启,呼叫客户的主叫号码将会是did号码-DID的中文意思是直接拨入,是运营商提出的概念,通过这个号码,运营商可以呼叫到voip系统内,freeswitch上实现外乎呼叫的方式为配置一个网关,指定运营商的接入号和密码,有呼叫这个号码时,运营商会把这个号码送给freeswitch。)

2.acd.cfg配置(设置使用的主叫号码:usefromcaller,如果想要使用cti_work里面的,需要配置为1)

3.配置坐席呼出的主叫显示:cti_outcaller

4.使用web页面上的软电话条进行呼叫,然后查看呼叫的结果里面是不是我们配置的主叫

从上面我们可以知道,坐席呼出时候是两通电话。

2.2.fsg模块

FSG模块作为:AG(CTI)和FS进行交互模块,其作用就是一个接口调用服务。
对应的配置文件是:/cc/cfg/fsg.cfg
对应的日志文件是:/cc/log/fsg**.log和/cc/bin/log/log*.log
其日志记录了ipcc内部和外部中继网络交互的日志详情。

2.2.1.fsg模块修改日志

有时候,我们为了排查和外部对接的错误信息时候,可以通过两种方式:
1.修改fsg的日志级别为6
2.通过tcpdump -w xx.cap抓包
以下为修改fsg的日志级别为6的设置

2.2.1.1.删除原来的fsg相关日志文件
  1. 删除/cc/bin/log文件目录下的文件
  2. 删除/cc/log/fsg*.log文件
    2.2.1.2.修改日志级别配置文件fsg.cfg
  3. 修改fsg.cfg里面SET下的日志级别文件为6

    acd在平台起到用户转座席排队,座席分配,座席操作控制等和座席呼叫相关的功能。
    2.2.1.3.以写入文件的方式启动fsg.sh
    进入/cc/bin下输入
1
fsg.sh restart > /cc/bin/log/xxx.log

2.3 ag模块

2.3.1 概述

AG在整个YC平台中起着集中管理的作用。AG模块就是我们常说的CTI模块。
AG起的作用有以下几点:
1、 管理多个适配节点的接入。
2、 统一对多个节点的电路进行全局编号。
3、 对电路进行分组管理。
4、 呼入电话路由相应的IVR流程进行处理。
5、 呼出电话路由到相应的电路组。
6、 管理呼叫电话分配媒体资源(播音,录音,会议,传真等媒体资源)

2.3.2 呼叫问题排查(AG问题排查)

理解T、HT、R、HR是理解ag模块的关键

  1. 1.按照流程来:T是收到 R是发送 HR是收到发送端的消息 HT是给T端返回信息
  2. 2.对于AG来说:T端是ACD、Dyflow等;R端是fsg
  3. 3.对于FSG来说: T端是ag R是fsapi
  4. 4.ACD等来说:T端是自己 R端是ag

我们查相关日志时候,先从ag模块(cti模块)查询起,然后一次找到T、R。
分析:看ag的日志—>这种交互ag的日志是两边都有的–>从ag里面开始分析—>平台挂机0、200是用户挂的—>从ag开始看确定消息在什么地方不对了 然后再分析fsg测 或者是ivr测的日志

2.日志文件模块

2.1.查看和机器人交互日志

和机器人交互日志都是通过ivr流程中http传数据,所以在httpg日志,需要看就提详细信息可以看dyflow日志

2.2.查看任务加载,工作日,时间段,呼叫情况
  1. 1.任务即是notify,但是任务在呼叫前任务加载,工作日,时间段都是在dbg_dbg01模块
  2. 2.查看任务呼叫情况:callnotify_callnotify01.log
  3. 3.查看任务调用api模块:callnotify_callnotifyapi.log
  4. 4.查看任务呼叫失败情况,没有数据插入到cti_cdr、ocm_result:查看callnotify_callnotifyfailsql.log

3.ipcc对接其他server

参考:https://blog.csdn.net/gredn/article/details/40581871

3.1 修改freeswitch配置

从平台架构我们知道,我们现在需要和外部第三方sipServer对接,首先需要打通各自的网络,然后就是配置相关的网管

  1. 1.进入freeswitch目录下freeswitch/conf/sip_profiles/external其中有外部网管模块。 拷贝存在的一个网关配置
  2. 2.修改gw3.xml中对接的外部sipServer地址和端口:
  3. 3.重启freeswitch
    进入freeswitch的bin目录停止fs:./freeswitch -stop
    进入freeswitch的bin目录启动fs:./freeswitch -nc -nonat
3.2 修改号码网管路由
  1. 1.修改cti_route表
    打开数据库表cti_route配置主叫外显号码和网关对应的关系
  2. 2.重启fsg
    进入/cc/bin目录下:执行:./fsg.sh restart
3.3 使用抓包工具抓包

对接完之后,如果电话不通,那么就需要使用wireshake进行网络抓包,分析包

4.ipcc对每个号码并发进行控制

在/cc/cfg/notify.cfg下的callnotify添加:examentline=1表明开启了号码并发设置

5.ipcc配置空号检测

5.1 安装空号检测
  1. 1.首先正常安装并启用空号检测da2(安装数据库、程序)
  2. 2.如果空号检测部署在一台服务器上,我们呼叫平台部署在多台服务器上,我们如何配置呢?
  3. 3.查看/usr/local/freeswitch/conf下面文件:da2.json
  4. 4.重启freeswitch:进入freeswitch的bin目录:执行:./freeswitch -stop 再执行:./freeswitch -nc -nonat
  5. 5.打开freeswitch客户端:
    1
    ./fs_cli -P 8031 -p testtest

  1. 6.在fs控制台客户端输入:da2 show

    出现上面问题说明成功,如果一直不成功检查对应数据库表
5.2 添加新的授权

部署新的话务平台节点时候需要添加新的授权,修改两个表:user,bill;

6.ipcc中cdr关于服务

  1. 1.服务水平只对呼入而言的 呼出不用管 calltype=100 and dir=1
  2. 2.starttime connecttime和endtime 是整个电话的开始 接起和结束时间
  3. 3.workstarttime 是电话进入acd的时间 workconnect是坐席接起电话的时间 workendtime是坐席服务结束的时间 acmtime 在呼入的时候是坐席开始振铃时间(注意 在呼出的时候是用户振铃时间)
  4. 4.acmtime是服务水平的统计标志
  5. 5.用户呼叫系统的全部电话(呼入我们目前都要进入ivr ):calltype<300 and dir = 1
  6. 6.坐席呼出用户的电话 calltype=100 || calltype=113||calltype=114 and dir=0
  7. 7.自动外呼呼出的电话 notifyid is not null
  8. 8.里面 connecttime workconnecttime字段很重要 判断是否接通的依据
  9. 9.建议:现在数据除了cdr表外 通话数据 坐席数据 其实已经通过ice推送给ccms了、ccms能不能做一些文件存储 然后通过一些外部工具进行数据分析、毕竟cdr是生产上的 不能频繁访问、 ZeroC ICE提供一种打补丁方式,名为IcePatch2。基本功能就是通过ICE通讯将服务器上的最近软件部署到各个客户端中,这间接的实现了文件传输功能,

7.ipcc中任务呼叫失败原因

8.ipcc中任务脚本通知

通过shell脚本的iniwrite和ice不能并存,如果用ice需要把iniwrite改成iniwrite1或者其他

9.ipcc中配置

ipcc各种配置请参考对应的doc文档。

2.空号检测模块搭建及录音库文件更新

注意:金服空号检测服务器由于私有云不能访问到外网,所以将空号检测license所在服务部署到了金服金融云。(金融云是连通公有云和私有云的桥梁)

空号检测服务daserver是根据局端返回的录音文件:然后通过asr识别来返回信令结果:dropcause

2.1服务搭建

  1. 1.解压da.tar.gz数据库
  2. 2.安装mysql
    centos6
    -yum install mysql-server
    -service mysqld start
    -chkconfig mysqld on

centos7
-yum install mariadb-server
-systemctl start mariadb.service
-systemctl enable mariadb.service

  1. 3.创建数据库da
    mysqladmin -uroot -p create da

  2. 4.导入数据到da
    mysql -uroot -p da < da.sql
    生成的数据库和表如下:

  3. 5.执行machinecode获取机器码,发给cdevelop@qq.com申请授权码,写入 license.txt .
  4. 6.根据安装文档word版本,配置数据库和JSON

  5. 7.启动daserver
    临时启动: ./daserver
    后台启动: nohup ./daserver &

注意:启动出现 ./daserver: relocation error: ./libssl.so.10: symbol private_ossl_minimum_dh_bits, version libcrypto.so.10 not defined in file libcrypto.so.10 with link time reference
一般是系统已经有了其他版本的ssl,删除 本目录的 libssl.so.10 文件

2.2更新录音文件库

电话在拨打过程中出现了运营商返回的错误信令:dropcause为480
通过查询da模块(空号检测模块)录制的录音知道:”您拨打的号码是非本地号码,请在拨打号码前加0” 我们知道空号检测模块的录音库中没有识别到此空号录音,所以我们需要更新录音库(使用da提供的win下的工具samplemgr,连接da所在license的库数据库da),有时候我们是无法直接连接license所在的da数据,而只能连接测试数据库,所以我们需要在测试环境下操作,然后把对应的数据替换到生产库。

2.2.1更新da语音库
  1. 1.打开samplemgr,连接对应的数据库

  2. 2.启动服务,输入key(key是da数据库中)


    启动成功日志:

  3. 3.添加录音库测试

    文件要求 8000hz 16位 单声道,文件名要是数字,比如 电话号码.wav 这样子。

    后续步骤按照samplemgr里面的文档:添加样本例子.docx

2.2.2 测试环境da数据导入到生产环境

注意:1.由于da表结构中字段类型是:blob所以我们不能使用SQLyog或者Navicat工具直接将表数据导出,经测试直接导出的数据通过source导入到生产环境时候会由于乱码导入报错。而只能直接在测试环境所在服务是使用mysqldump指令导出数据。

1.测试环境da数据导出

  1. 1.进入测试环境da数据库数据(以导出mfcc数据为例)
    mysqldump -u -p > /path/to/*.sql
    我们直接输入:mysqldump -u root -p123456 da mfcc>mfcc.sql 报错如下:

    说明我们导出表数据时候不能直接输入密码。我们更换成如下:
    2.生产环境da数据导入
  2. 1.连接生产环境数据库(注:如果连接不上时候加上-h 127.0.01参数)
  3. 2.数据导入
3.启动da的license所在服务

从数据导入之后使用./daserver启用之后,大概到了下午4点11分左右,局端又返回许多480结果,然后查看da的录音

听录音是没有识别到,于是进入freeswitch里面的log下的da.log打开后发现下午4点11分左右就不开始识别了:

查看daserver进程发现进程挂了,所以使用下面后太启动方式。

  1. 1.进入daserver所在目录下执行后台启动指令:
    nohup ./daserver &

10.定时备份日志

  1. 1.编写clearlog.sh

    1
    2
    3
    4
    5
    6
    7
    cd /cc/log/bak
    find /cc/bin/log -mtime +1 -type f -name "log*.log" -exec mv {} /cc/log/bak/. \;
    tar zcvf fslog$(date +"%Y%m%d%H%M")tar.gz log*.log
    rm -f log*.log
    find /cc/log -mtime +1 -type f -name "*.log" -exec mv {} /cc/log/bak/. \;
    tar zcvf log$(date +"%Y%m%d%H%M").log.tar.gz *.log
    rm -f *.log
  2. 2.添加到系统定时任务中去
    a.在root下执行 crontab -e
    b.将0 0 * /cc/bin/cleanlog.sh > /dev/null 2>&1 放进去 保存退出。

  3. 3.运维查询每天日志的最小、最大拨打时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SELECT MIN(starttime),MAX(starttime),SUBSTRING(starttime,1,10) FROM cti_cdr WHERE entid = 1308 GROUP BY SUBSTRING(starttime,1,10) ORDER BY starttime ASC;
```
#### 11.停止掉空号检测
1.进入:/cc/freeswitch/conf;不是删除da2.json
2.注释掉:load mod_da2
进入:/cc/freeswitch/conf/autoload_configs找到文件:modules.conf.xml
然后注释掉:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/10/114.png)
3.fsg.cfg里面:注释掉:识别da2的注释掉
![](https://raw.githubusercontent.com/startshineye/img/master/2019/10/116.png)
#### 12.fs中注释掉影响通话质量的元素
1.conf/vars.xml里面的以下几行注释掉:

```

SpringCloud-8-Zuul综合使用

项目整体架构图如下:

8.1 Zuul:Pre和Post过滤器

  1. 1.从以上架构图知道:所有的请求都会到Zuul,然后到ServicreA、然后到ServiceB、再到ServiceC、现在我们对整体服务做一个权限校验。假如没有zuul服务、那么ServicreA、ServicreB、ServicreC都得校验一次做的太多受不了。所以权限校验我们放在Zuul里面统一处理。
  2. 2.接下来我们演示如何对请求进行统一的校验,我们现在做一个所有经过Zuul的请求都要有一个token,并且内容不能为空,如果不带token参数的话,权限校验不通过。我们以:http://localhost:8084/myProduct/product/list?token=12121为例子.。我们只有在连接后面添加:token=?才允许校验通过。
    8.1.1 Zuul统一校验
    我们在api-gateway项目下面创建filter包,并新建一个TokenFilter类
  3. 1.新建TokenFilter做PRE过滤
    a.这里面我们做的是参数校验:所以filterType是:PRE_TYPE。
    b.我们在run()方法里面实现具体的逻辑。
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
@Component
public class TokenFilter extends ZuulFilter{
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
/**
* filter的顺序:对应的值越低优先级越高
* 我们把此Filter放在PRE_DECORATION_FILTER_ORDER前面
*/
return PRE_DECORATION_FILTER_ORDER-1;
}
@Override
public boolean shouldFilter() {
/**
* 开启过滤
*/
return true;
}
@Override
public Object run() throws ZuulException {
//1.获取当前上下文
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//这里从url参数里面获取,也可以从cookie,header里获取
String token = request.getParameter("token");
if(StringUtils.isEmpty(token)){
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}

测试:http://localhost:8084/myProduct/product/list

携带token测试:http://localhost:8084/myProduct/product/list?token=112
正常返回

当然我们的判断逻辑可能不止token为空,也可以拿到token值之后从数据库读。

  1. 2.新建Post过滤器
    我们也可以定义postfilter,在请求到结果之后,对请求的结果进行处理加工。 这里我们往请求返回的header里面写一些东西,新建AddResponseHeaderFilter类。
    a.filterType->POST_TYPE
    b.自定义post filter时候,我们选择使用:SEND_RESPONSE_FILTER_ORDER之前的filter
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
@Component
public class AddResponseHeaderFilter extends ZuulFilter{
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
/**
*我们自定义post filter时候我们filter的顺序放到SEND_RESPONSE_FILTER_ORDER之前。
**/
return SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response = requestContext.getResponse();
/**
*请求成功之后,我们往header里面添加一些信息。
**/
response.setHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}

测试:

8.2 Zuul:限流

8.2.1 限流基本理论
  1. 1.Zuul充当的是api网关的角色、每个请求都会经过它、在他上面做api限流保护、防止攻击。比如我们的api是发短信的、我们需要限制客户端请求速度、从而在一定程序上抵制短信大攻击、降低损失。
  2. 2.Zuul的限流是放在前置过滤器去做的、更具体来说、时机是在请求被转发之前调用。如果前置过滤器有多个操作、限流放到最靠前那个。比如:Zuul前置过滤器里面有限流、鉴权。那么限流应该早于鉴权。

  3. 3.限流方案很多,这里以一种来说:令牌桶流。

    a.中间桶(token bucket):是存放令牌的、所以叫做令牌桶。
    b.最上面会以固定的速率往令牌桶中添加令牌、如果已经放满了就丢掉。
    c.外部请求过来、会从令牌桶中获取令牌、拿到令牌之后才可以继续往前走。如果拿不到令牌直接被拒绝。其实是和买房一样。

8.2.2 限流代码实现

限流也是一个Filter,我们以RateFilter
a.filterType–>PRE_TYPE
b.filterOrder–>优先级最高:优先级为最高:组件自带的优先级最高位-3 我们这边比他还高(SERVLET_DETECTION_FILTER_ORDER-1)

1.新建RateFilter类


8.3 Zuul:鉴权和添加用户服务

8.3.1 Zuul权限校验

我们使用Zuul的权限校验实现下面3个功能。

  1. 1./order/create 只能买家访问(创建订单)
  2. 2./order/finish 只能卖家访问(完结订单)
  3. 3./product/list 都可以访问(商品列表)

  4. 1.新建鉴权Filter(AuthFilter)
    新建鉴权Filter、用来区分买家和卖家

  5. 2.实现逻辑
    怎样区分买家和卖家呢?有些人想到cookie、既然想到了cookie那么必须买家和卖家登录了之后才能获取到信息。那么登录功能写到哪里了,就需要有一个用户服务。

8.3.2 添加用户服务
  1. 1.API接口登录区分
  1. 2.数据库
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
CREATE TABLE `user_info` (
`id` varchar(32) NOT NULL,
`username` varchar(32) DEFAULT '',
`password` varchar(32) DEFAULT '',
`openid` varchar(64) DEFAULT '' COMMENT '微信openid',
`role` tinyint(1) NOT NULL COMMENT '1买家2卖家',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
);
```
userInfo中买家和卖家区分是根据:role字段。
3. 3.创建应用
选择的依赖组件:
1.配置:Cloud Config-->Config Cient
2.注册中心:Cloud Discovery-->Eureka Discovery
3.数据库:SQL-->Mysql/JPA
4.缓存:NoSQL-->redis
4. 4.修改版本依赖
5. 5.统一配置中心远端服务器创建user-dev.yml文件
我们只添加数据库连接信息和redis连接信息,其他信息在bootstrap.yml文件里面
6. 6.在config服务中访问:http://localhost:8083/user-dev.yml可以查看到对应文件
7. 7.因为user服务后面肯定会对外提供接口的、所以我们将其改造成多模块
![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/7.png)
8. 8.添加dataobject

@Entity
public class UserInfo {
@Id
private String id;
private String username;
private String password;
private String openid;
private Integer role;
public String getId() {
return id;
}
}

1
2
3
4
9. 9.service和serviceImpl

public interface UserService {
/**

  • 通过openid来查询用户信息
  • @param openid
  • @return
    */
    UserInfo findByOpenid(String openid);
    }
    @Service
    public class UserServiceImpl implements UserService {
    @Autowired
    private UserInfoRepository repository;
    @Override
    public UserInfo findByOpenid(String openid) {
    return repository.findByOpenid(openid);
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #### 8.3 Zuul:鉴权和添加用户服务
    1. 1.拷贝ResultVOUtil、ResultEnum到user
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/8.png)
    2. 2.书写买家登录:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/9.png)
    3. 3.测试:
    a.正确输入:![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/10.png)
    b.错误输入:![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/11.png)
    c.数据库角色修成成2:![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/12.png)
    4. 4.往redis里面书写数据
    买家登录时候先去redis里面找,如果有说明已经登录了,没有的话才设置cookie
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/13.png)
    上面代码会出现一个问题,就是每次页面刷新,都会在redis里面写数据,导致很多脏数据产生,所以需要在卖家最前面添加判断。
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/14.png)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/15.png)
    ### 8.4 订单完结接口开发
    我们在api-gateway的AuthFilter里面进行过滤校验时候:

    /**

  • 1./order/create 只能买家访问
  • 2./order/finish 只能卖家访问
  • 3./product/list 都可以访问
    */
    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
    72
    73
    分为以上3步,我们还差在Order服务中订单完结接口没有开发:"/order/finish";我们现在实现此接。
    ### 8.5 权限校验完成
    #### 8.5.1 权限基本操作
    1. 1.先访问user(卖家登录:http://localhost:8085/login/seller?openid=xyz)
    结果如下:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/16.png)
    我们请求正常,并且也获取到cookie了,此时我们把cookie删掉.再次请求api-gateway(此时请求api-gateway的时候注释掉:TokenFilter中run认证)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/17.png)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/18.png)
    以上返回的是成功,但是cookies中没有值,之前提过Zuul敏感头设置:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/19.png)
    假如我们对所有服务都提供敏感头,都需要传递cookie,则:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/20.png)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/21.png)
    此时我们能够确保通过api-gateway是可以正常登录的。
    2. 2.接下来我们书写AuthFilter中业务逻辑。
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/22.png)
    下面我们通过接口去调用:
    3. 3.我们通过postman创建订单(切记一定要加上order服务名)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/23.png)
    我们改掉:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/24.png)
    我们再次请求时候回返回401:权限不足->所以我们需要先登录
    注意:在postman中,每一个窗口是互相隔离的,我们在一个窗口中登录,在另一个窗口中也是不能请求过的。
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/26.png)
    所以我们自己需要写入一个cookie到postman中去
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/25.png)
    4. 4.上面我们使用if语句来判断不同授权用户,其实这种写法以后相当不好维护,如果用户很多,耦合进来判断,有一天假如对卖家放开不限制,那么只能删掉,如何改造呢?我们一般对卖家和买家各自做一个filter:
    a.拦截统一在shouldFilter方法中
    b.拦截后处理统一在run方法中
    AuthBuyerFilter:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/27.png)
    AuthSellerFilter也是类似。
    #### 8.5.2 权限补充
    1. 1.大多数情况下,我们会把权限校验的uri存放到数据库中去,那是不是api-gateway直接去连接数据库进行判断呢?
    这里牵涉到一个边界的问题,api-gateway要做的是网关,如果去连接user服务的数据库,显然是不合适的,那能不能去调用user服务呢?调用user服务是合理的,应该去调用,但是每次鉴权的时候去调用user服务,user服务那边又去调用数据库的话,这样对数据库压力很大,我们应该使用redis,api-gateway直接去redis里面获取用户权限,那么redis里面的数据怎样过来呢?可以像之前"异步扣库存的方式->用户信息一变动,就发一个信息过来,网关这边监听消息,并记录到redis"
    2. 2.微服务架构下对所有服务都得鉴权,每个服务都需要明确当前用户的权限,在Zuul的前置过滤器里面实现相关逻辑是一个值得考虑的方案,同时在微服务框架中,多个服务的无状态化一般会考虑两种技术方案:
    a.分布式session(将用户认证信息存储在共享储存中,且通常用用户会话作为key来实现简单的分布式hash映射,当用户访问微服务时,用户数据可以从共享存储中获取,用户登录状态是不透明的,同时也是一个高可用且可扩展的解决方案)
    b.OAuth2与Spring Security结合
    3. 3.我们这有一个细节:用户模块的utils代码一直在copy,相同的我们就拷贝过来,我们少了一个基础服务(如果公司是一个大型服务改造的话,基础服务比较容易,一目连然拆出来,但是是一个从头开发的项目,没有多大把握,类似我们现在,建议是将每个微服务的公用组件放到公用模块里面去,比如我们现在的user,order,product服务都有common模块,有了一定积累后,很自然的将这块代码剥离出来,作为公共组件,下沉,成为公共的一个服务)
    4. 4.另外一点,在SpringCloud架构体系中的所有微服务都是通过Zuul对外提供统一访问入口,这个时候如果公司有两套系统,一套传统项目,一套微服务架构项目,让这两套项目在线上同时运行,Zuul会非常关键。
    ### 8.6 跨域
    #### 8.6.1 跨域问题
    1. 1.我们一般的大型项目都是前后端分离的,前端通过ajax发送请求,浏览器的ajax是有同源策略的,如果违反了同源策略就会有跨域问题,Zuul作为微服务的api网关,在它上面处理跨域也是一种选择。
    2. 2.跨域其实可以看成是Spring的跨域,Spring跨域常用的一种方法是在:被调用的类或方法上增加@CrossOrigin注解来声明自己支持跨域访问,这种方式的缺点其实很明显,作用域在类或者方法上,微服务里面的应用就有很多个,一个应用里面的类和方法更加多
    3. 3.解决跨域的另一种方法是:在Zuul里增加CorsFilter过滤器,此方案是添加到网关上,对内部应用代码没有任何改造。
    #### 8.6.2 跨域改造
    ##### 1.单个接口跨域
    1. 1.项目中跨域:我们在product服务里面的ProductController的list方法里面添加跨域注解:
    allowCredentials = "true"->true允许cookie跨域

@CrossOrigin(allowCredentials = “true”)
```

2.统一跨域设置
  1. 1.在api-gateway上添加配置文件:

SpringCloud-7-服务网管

1.服务网关和Zuul

1.1 为什么需要服务网关

假如没有网关服务,当前系统起了多个服务,比如:订单、广告、商品、支付、用户。等等…

那么客户端怎么调用呢?和每个服务一步步打交道、显然不现实。需要一个接收request请求的统一入口。充当这个角色的是服务网关。一旦有了服务网关,所有请求都通过他,所以我们需要了解他发挥什么作用,具备什么要素?

1.2 服务网关的作用

1.稳定性、高可用

保证24小时可用;网络瘫痪、系统全挂、不能提供外面的服务。

2.性能、并发性

所有的请求都经过网关、所以网关的压力是巨大的、所以网关的性能也必须高。

3.安全性

要确保使用服务的安全、防止外部随意访问、比如金融行业、会进行通讯数据的加密措施。

4.扩展性

各种请求都经过网关服务、所以网关上大有文章可做、可进行扩展。
理论上、网关是处理各种非业务功能的绝佳场所。诸如:协议转发、防刷、流量管控、日志监控等。

1.3 常用的网关方案

网关并非是微服务出来后的新鲜事物。业界成熟的网关服务有:

一代的Zuul在性能上确实没有优势、不能和nginx比较。当然,二代Zuul有较大的提升。

原始的点餐项目是如下:

  1. 1.nginx在前,tomcat在后、nginx做了负载均衡和反向代理。现在我们可以让nginx发挥他负载均衡和反向代理的优势后面的tomcat可以换成Zuul
  2. 2.项目改造过程中、合理利用原来的资源、发挥新加入事物的优势、因地制宜解决问题。

    1.4 Zuul的特点

    Zuul相对nginx来说有所不足、但是作为SpringCloud完整微服务生态体系的前置网关服务是一个很不错的选择。有一种说法路由加过滤器等于Zuul


Zuul的组织架构图如下所示:
可以看到,过滤器之间是没有直接通信的,他们是通过Request Context进行数据通信的。

Zuul的一次Http的生命周期:

  1. 1.Origin Server就是我们的业务服务。
  2. 2.请求先过来之后会到pre filters这种类型的过滤器,比如我们的参数校验就可以在这里面来做。
  3. 3.routing filters作用就是将http请求转发到Origin Server中去:如果重写http请求可以在这里面做。
  4. 4.post filters 这个时候你已经拿到了返回的结果,这个时候如果你想对结果进行处理和加工,可以在这里面来做
  5. 5.error Filters如果在前两个filter发生异常时候,就可以在error filter中进行全局异常处理。

2.Zuul的使用

  1. 1.新建项目api-gateway

  2. 2.选择依赖
  3. 3.删除不需要的文件
  4. 4.修改版本一致
  5. 5.修改配置文件(从统一配置文件中获取配置信息)

2.1 路由转发

我们现在实现Zuul的路由转发功能,我们在api-gateway项目的启动类上添加:@EnableZuulProxy。我们现在要实现product服务下请求里面的/product/list

  1. 1.启动我们的product服务(8081端口),然后访问:

  2. 2.通过Zuul服务(8084端口)路由转发请求:

    只要注册到Eureka上的服务都可以通过:ip:port/服务名/url

  1. 3.自定义Zuul路由转发地址
    以上我们是通过默认的路由转发、默认的地址是:ip:port/服务名/url。但是有时候我们不想把服务名暴露出来 而是想用自己定义的路由地址。此时就需要配置Zuul了。

  2. 4.查询Zuul的所有routes,我们可以通过Zuul对应的服务api-gateway启动地址:

  1. 5.我们如果想要拦截某些请求

2.3 Cookie和动态路由

我们在开发搭建web项目的时候,经常要使用cookie,需要把cookie返回到后端。在这里使用了Zuul组件、默认情况下Cookie是无法传递过去的。如下:


我们从Zuul的配置中可以设置cookie的开启和关闭

2.3.2 动态路由

我们能不能改了路由配置之后自动生效了,就是动态路由。如果需要达到动态路由效果。我们只需要将api-gateway的配置放到config配置中心中去。我们如何做到config统一配置中配置改变了我们的api-gateway服务也动态改变了代码。我们添加Zuul的配置类。

2.4 路由和高可用小结

2.4.1 典型应用场景
  1. 1.Zuul的前置过滤器(Pre)
    a.限流 b.鉴权 c.参数校验调整 d.请求转发

  2. 2.Zuul的后置过滤器(Post)
    人过留名
    a.统计 b.日志

2.4.2 高可用

所有的请求都需要经过Zuul、所以生产环境中、我们需要部署多台Zuul,以避免单点故障。Zuul的高可用是必须的。

  1. 1.多个Zuul节点注册到Eureka Server(将多个节点注册到Eureka Server就可以实现高可用)
  2. 2.Nginx和Zuul “混搭”(使用nginx对外暴露一个url,nginx把请求转发到多个zuul服务上、这样可以做到彼此的取长补短)。

SpringCloud-6-消息和异步

1.异步和消息

1.简介

  1. 1.之前我们使用的方式是同步方式。
  2. 2.异步优点:a.客户端请求不会阻塞进程、服务端的相应可以是非及时的。
  3. 3.http常见是支持同步、但是http也是支持异步的。

2.异步的常见形态

  1. 1.通知
  2. 2.请求/异步响应:客户端发送请求到服务端,服务端异步响应,客户端不会阻塞,服务端默认不会立马响应。
  3. 3.消息

    3.MQ应用场景

    MQ是分布式应用系统最常用的组件。典型应用场景有如下:

  4. 1.异步处理:用户注册之后,通过短信服务、积分服务去做他们相应的操作,这样就能提高用户体验。

  5. 2.流量消峰:一般用于秒杀场景,秒杀过程中一般由于流量较大,会导致应用会挂掉、要解决这个问题。把请求放在消息队列中,如果超过消息队列的长度将抛弃此请求,返回错误信息。
  6. 3.日志处理:这个典型的组件就是kafka,kafka最初的设计就是用于日志处理,大数据里面用的特别多。通过日志采集、定时写入kafka队列,然后kafka队列定时接收 储存 和转发。
  7. 应用解耦:比如用户下单后,订单服务需要通知商品服务、之前是订单服务通过调用商品服务的接口,这样订单服务和商品服务是耦合的。使用mq,用户下单后,订单服务完成持续化处理、将消息写入消息队列、返回用户订单下单成功、商品服务来订阅这个消息采用拉或者推形式获取下单信息、商品服务获取到下单信息后进行商品的扣库存等操作。这样达到应解耦。

2.RabbitMQ的基本使用

我们Order服务使用RabbitMQ实现订单和商品服务解耦。

  1. 1.添加热rabbitmq依赖-在order服务中server子模块添加以下依赖:
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 2.添加配置到github远程仓库

  2. 3.创建MQ消息接收方

  3. 4.创建mq发送方进行测试
    我们在测试test文件目录下创建测试类:

测试出现了上面问题,原因是因为没有myQueue队列

此时正常启动。

这个时候一切正常。

  1. 5.以上是我们自己在RabbitMQ中创建的队列。我们是否可以自动创建队列,然后再调用队列接口。
1
@RabbitListener(queuesToDeclare = @Queue("myQueue1")) //自动创建队列myQueue1
  1. 6.自动创建并且和队列绑定
1
@RabbitListener(bindings =@QueueBinding(value =@Queue("myQueue"),exchange =@Exchange("myExchange")))

2.2 什么情况下需要用到exchange

我们现在是一个小小点餐系统,但是如果后面,我们什么都卖,又卖水果,又卖数码,同时对这两种商品下单,商品变多了,人也多了,订单服务是单独的人来维护,数码供应商、水果供应商都是由单独的人来维护。订单服务要根据不同的商品类型发出不同的MQ消息。相对应的、数码供应商只关注数码订单、水果及其他订单不关注。此时就牵涉到消息的分组。 演示如下:
1.我们接收方模拟两个接收服务(a.数码服务 b.水果服务)
exchange因为都是订单,我们我们叫做myOrder
想要分组归类,我们用key

2.我们发送方 (类似订单服务)如下:

3.SpringCloudStream的使用

可以参数博客:https://www.cnblogs.com/zhixiang-org-cn/p/10093367.html

3.1.基本介绍

  1. 1.应用模型:应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中Binder 交互,通过我们配置来绑定,而 Spring Cloud Stream 的 Binder 负责与中间件交互。所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
  2. 2.抽象绑定器(The Binder Abstraction):Spring Cloud Stream实现Kafkat和RabbitMQ的Binder实现,也包括了一个TestSupportBinder,用于测试。你也可以写根据API去写自己的Binder.Spring Cloud Stream 同样使用了Spring boot的自动配置,并且抽象的Binder使Spring Cloud Stream的应用获得更好的灵活性,比如:我们可以在application.yml或application.properties中指定参数进行配置使用Kafka或者RabbitMQ,而无需修改我们的代码。

  3. 3.Binder(SpringCloudStream)是应用程序(Application)和消息中间件(Middleware)之间的粘合剂,使用 SpringCloudStream最大的好处莫过于对消息中间件的进一步封装。可以做到代码层面对消息中间件的无感知,设置于动态的切换中间件,但是也有局限:目前SpringCloudStream仅支持2种Binder:一种是RabbitMQ、另一种是Kafka

3.2.使用SpringCloudStream

为了详细了解SpringCloudStream的话,可以详细了解下:https://www.cnblogs.com/leeSmall/p/8900518.html这篇博文。

1.引入依赖

在order-server.xml中添加如下依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
2.添加mq的配置

之前我们已经配置过了,这里可以省略

3.使用stream发送和接收消息
  1. 1.定义接口:提供输入输出接口
1
2
3
4
5
6
public interface StreamClient {
@Input("myMessage")
SubscribableChannel input();
@Input("myMessage")
MessageChannel output();
}
  1. 2.添加stream接收端
1
2
3
4
5
6
7
8
9
@Component
@EnableBinding(StreamClient.class)
@Slf4j
public class StreamReceiver {
@StreamListener("myMessage")
public void process(Object message){
log.info("StreamReceiver:{}",message);
}
}
  1. 3.添加stream发送端
1
2
3
4
5
6
7
8
9
10
@RestController
public class SendMessageController {
@Autowired
private StreamClient streamClient;
@GetMapping("/sendMessage")
public void process(){
String message = "now " + new Date();
streamClient.output().send(MessageBuilder.withPayload(message).build());
}
}
  1. 4.测试

    有时候我们后端启用2个order服务,然后重启后 客户端发送消息,发现2个都接收到了,那么我们如何多集群情况下,只有一个实例接收消息呢?
    stream里面有个分组,我们配置一下就可以了 那就是使用分组。
3.使用stream传递对象

之前我们在用例中stream传递的是String、实际工作中stream更重要的是传递对象。
生产者:发送OrderDetail对象

消息接受者:

下面我们看一下:mq里面接收的是什么格式数据呢?
我们在上面看不到消息了,原因是消息已经被消费完了,所以我们需要把接收端,消费消息的停掉。让mq端有消息的累计。

上面我们可以看到消息是:OrderDTO对象,如果我们需要在MQ中获取的消息是json格式:则在stream的配置中添加:content-type: application/json

有时候我们的消费者:StreamReceiver消费完消息之后,我们需要给发送者一个通知,传统的做法是在业务逻辑里面后面做处理。
现在我们加上注解即可:

总结:使用Stream可以降低对消息中间件的复杂度,让开发者更多的关注业务开发。

4.商品和订单服务中使用MQ

4.1 引言

我们结合点餐业务来使用mq

  1. 1.之前我们按照上面图提示到:一旦有库存的变化(商品)、都会发布一个消息、订单拿到这个消息之后,会把库存的消息记录到自己的服务里面(这里我们把他记录到redis里面)
  2. 2.导致库存的变化有很多种:a.第一次商品上线的时候,会填写库存,这个时候库存就变化了。b.还有就是货物快卖完的时候需要补货。相当于加库存。我们以扣库存为例,使用消息队列进行通信。

4.2 product服务接入到配置中心

  1. 1.添加配置中心的依赖
  2. 2.修改appliaction.yml文件为:bootstrap.yml文件,并修改其中的配置

  3. 3.在github上建立对应的product-dev.yml文件,并将共有的信息拷贝到里面去

  4. 4.在config服务中看是否可以访问到此配置文件。
  5. 5.启动时候正常启动:说明我们已经把product服务接入到配置中心了。

4.3 product服务在扣库存时候发送队列消息

  1. 1.添加springBoot里面的amqp依赖

  2. 2.在统一配置中心中添加mq的连接信息

4.4 product服务在扣库存地方操作消息队列

我们在扣库存的service中添加扣库存(decreaseStock)后消息通知:

然后我们使用postman测试:

数据库里面的库存从33变成32,数据库扣除成功了

但是我们登录mq的管理后端却发现没有对应的amqpTemplate的productInfo队列,

为什么没有这个队列呢?原因是因为我们压根就没有创建队列,只有在接收方添加注解:@RabbitListener注解才会自动创建,我们 上面只有发送方,所以我们先手动创建下:然后点几次发送,后面发现我们在消息队列中有了相应的消息:

以上就完成了消息的发送,现在消息已经从商品服务发送到消息队列里面了,接下来我们要在订单服务接收消息。
在Order服务的message包下面添加接收消息:

我们先删除之前的productInfo队列,然后重启服务会发现会自动创建队列

4.5 Order服务获取到扣库存信息存储到redis

我们order服务获取到了product服务发送的信息,现在需要把这些信息存储到redis中
redis可以使用docker安装、也可以使用自己免安装的版本

  1. 1.项目中引入依赖(order-server中pom.xml)
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 2.配置redis里面的参数,在统一配置中心添加
1
2
3
4
spring:
redis:
host: localhost
port: 6379
  1. 3.书写存储到redis(使用StringRedisTemplate)

  2. 4.postman测试

4.6 残留的问题

  1. 1.从上面我们知道:扣库存是遍历了商品,然后操作数据库,再然后就是发送mq消息,假如decreaseStockInputList长度大于1,我们第一件商品扣库存之后、发送消息到mq,但是第二件商品报了异常,由于有事务,数据库会回滚,但是mq里面的消息不会回滚,所有有脏数据。
  2. 2.我们修改下,我们在数据库扣完库存之后再发mq消息
    Product服务修改:
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
@Override
public void decreaseStock(List<DecreaseStockInput> decreaseStockInputList) {
//1.获取到扣库存列表
List<ProductInfo> productInfoList = decreaseStockProcess(decreaseStockInputList);
//2.转换成ProductInfoOutput列表
List<ProductInfoOutput> productInfoOutputList = productInfoList.stream().map(e -> {
ProductInfoOutput output = new ProductInfoOutput();
BeanUtils.copyProperties(e, output);
return output;
}).collect(Collectors.toList());
//3.发送mq消息
amqpTemplate.convertAndSend("productInfo", JsonUtil.toJson(productInfoOutputList));
}
@Transactional(rollbackFor = Exception.class)
public List<ProductInfo> decreaseStockProcess(List<DecreaseStockInput> decreaseStockInputList) {
/**
* 遍历:查看是否存在
*/
List<ProductInfo> productInfoList = new ArrayList<>();
for (DecreaseStockInput cartDTO:decreaseStockInputList){
Optional<ProductInfo> productInfoOptional = repository.findById(cartDTO.getProductId());
//商品不存在
if(!productInfoOptional.isPresent()){
throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST);
}
//商品存在-库存错误
ProductInfo productInfo = productInfoOptional.get();
int result = productInfo.getProductStock() - cartDTO.getProductQuantity();
if(result<0){
throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROE);
}
//保存
productInfo.setProductStock(result);
repository.save(productInfo);
productInfoList.add(productInfo);
}
return productInfoList;
}

Order服务修改:
JsonUtil添加如下方法:

5.异步和库存分析

项目改造成异步后,数据一致性等问题是经常遇到的,很多时候在单体服务中,依靠本地事务,我们很容易保证数据的一致性。但是一旦切换到分布式异步情况下就很可能出现数据不一致的情况。比如这里的发消息,数据库回滚数据是自动的。消息多发了要怎么办?这个就需要重新仔细考虑,稍不留神机会出错。下面我们以下面通路进一步看下更多考虑的点。
参考一下链接:https://cloud.tencent.com/developer/article/1344252

SpringCloud-5-统一配置中心

1.统一配置中心概述

1.1 为什么需要统一配置中心

之前我们写的order微服务和product微服务其实是不足的,有以下几个原因:

  1. 1.不方便维护:(每个人负责单独的模块,有一个人把配置修改后,push到远程,然后另一个人pull后修改代码,测试相同功能,此时配置文件已经改的面目全非了。)
  2. 2.配置内容安全与权限:一个公司线上的配置不会对开发者进行开发公开的,特别熟数据库账号密码,按理说只有运维才知道。我们需要把数据库信息隔离,不放到数据库密码里面去。
  3. 3.更新配置项目需重启:每次更新项目都要重启,线上有可能修改一些配置,比如更改项目发送短信接口次数,我们怎样做到不重启。

1.2 统一配置中心

我们到时候会专门做一个项目:config-server单独的微服务,他也有server端和client端。server端,这些配置我们为了方便管理都把他放到git上,这样子版本控制起来比较方便,他会从远端git把配置给拉取下来,以下图的箭头表示配置信息流的流动方向,最开始我们把他放到远端git,远端git的话,有gitlab,github,config-server把配置拉取下来之后,会把其配置放置到本地git上,注意config<->本地git之前是双向流动的,他既会把远端git放到本地git去,也可以假如远端git不能使用了,被墙了,那么他会把本地的git拿出来,后面给product和order服务使用(两个服务都会集成config-server的客户端)

2.统一配置中心server端(config-server)

新建一个项目:

  1. 1.Spring Initializr
  2. 2.填写Project Metadata
  3. 3.添加依赖(本身是个微服务,需要注册到Eureka上去,所以添加Cloud Discovery中的Eureka Discovery和本身是一个配置类:作为config的server端 添加Cloud Config依赖)

  4. 4.创建完毕之后,删除不必要的文件

  5. 5.更改pom使各个微服务版本号统一
  6. 6.在启动类中添加注解:将其注册到Eureka中去
  7. 7.修改其配置文件application.yml文件
  8. 8.启用Eureka服务,再启用config服务,一下出现config服务,说明启用成功。
  9. 9.要成功config-server端,需要添加对应注解

    此时启动报错
1
Caused by: java.lang.IllegalStateException: You need to configure a uri for the git repository

我们在远程github仓库创建配置文件:

在远程仓库的config-repo下添加order服务的配置信息:

输入一下信息回车:

配置信息的username和password没有配置因为config-repo是公开仓库

然后重启项目完之后,校验我们的配置文件是否被config-server从github仓库中拉取下来了。然后我们在浏览器输入:

为什么需要多写一个a呢?:http://localhost:8083/order-a.yml,http://localhost:8083/order-b.yml也是可以访问的,但是http://localhost:8083/order.yml不可以访问。
我们查看config-server启动时候的日志如下:

上面是支持properties,yml,json格式见转换的,如下:


具体的访问配置文件格式说明:

查看config-server把远程的文件:order.yml拉取到了本地的哪个文件:如下:

当然我们自己也可以自定义这个配置文件路径:浏览器访问:http://localhost:8083/order-a.yml然后再配置的文件下:

如下:

2.统一配置中心client端

我们以order服务为例集成config-client,按理来说我们把order的全部配置已经放到git上了,不应该放在order的appliaction.yml文件中了,那我们要如何使用了?老套路来了。
1.引入依赖 2.启动类上不需要加注解 3.添加配置

2.1 引入依赖


org.springframework.cloud
spring-cloud-config-client

2.2 启动类上不需要加注解

不加注解

2.3 添加配置

我们需要找到github上的通过config-server拉取到本地的一个配置,那么就需要服务名(配置文件中应用名)加开启环境(dev)找到此配置。

启动应用:报错:

思路:我们的期望启动流程是:1.通过配置文件的service-id:CONFIG找到,config-server,然后config-server把这些配置拉取到本地 2.order服务拿到配置之后才能进行数据库的初始化

现在我们把所有信息都写在applaction.yml文件里面,SpringBoot项目不知道哪个在前,哪个在后面,他直接找数据库相关的,那么就启动失败了。

我们希望config-client的配置先加载,先启动。SpringBoot提供了bootstrap.yml(启动引导的文件),我们把application..yml改成bootstrap.yml

测试是否加载了order-dev.yml里面的配置信息:
我们看能不能拿去到env: dev信息

浏览器访问:http://localhost:8082/env/dev
结果如下:

2.4 统一配置中心的高可用

关于统一配置中心的高可用,我们可以以后在服务器上部署多个,多个服务注册在EurekaServer时候,我们通过Order服务连接CONFIG自动可以实现负载均衡,下面我们以开启多个启动端口为例

  1. 1.config-server配置多个启动类


    然后启用ConfigApplication、ConfigApplication(1)、ConfigApplication(2)三个应用。在EurekaServer端可以看到3个服务已经启动:

  2. 2.我们多次启动Order服务,可以看到其连接的地址多次变动。
    第一次启动出现:

2019-05-25 16:38:58.471 INFO 11424 — [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://JSKJ-PC:9002/

第二次启动出现:

2019-05-25 16:40:01.977 INFO 10640 — [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://JSKJ-PC:8083/

可以看出他是获取的不同配置。

2.5 统一配置中心和EurekaServer的注意点

Eureka注册中心默认的端口是8761,如果我们将8761改成8762,我们再来看一下情况。

  1. 1.修改eureka注册中心的端口为:8762,重启
  2. 2.修改config服务注册中心地址端口为:8762,重启
  3. 3.修改config服务对应的远程配置文件的信息:打开http://localhost:8083/order-dev.yml发现配置中心的远程配置已经修改并加载到本地

  4. 4.启动Order服务,加载Order服务的配置

    我们发现我们的Oder服务已经连接不上config服务
    这是什么原因呢?我们理一下流程:Order服务首先要做的其实首先是访问eureka,只有通过访问eureka,才能找到eureka下注册的config服务
    现在是根本获取不到,获取不到的时候默认就会访问:http://localhost:8888,这是由于eureka地址没有配置对。但是我们不是在order-dev.yml中已经配置了这个地址吗?还是一个顺序的原因,之前8761之所以可以,是因为:8761是默认的一个地址。访问不到微服务就会默认访问8761注册中心,所以是没有问题的。所以当eureka不是默认端口:8761的是时候,我们把eureka的配置信息放到github远程配置上是不合理的。所以我们需要把这个配置拿出来放到Order服务里面的配置文件里面来,Order服务首先加载本地的bootstrap.yml,然后找到注册中心,从注册中心中找到config服务。
    做法:远程去掉eureka注册配置,将其保存到order服务的bootstrap.yml中。

  5. 5.在启动Order服务前,我们看一下config下访问的远程配置文件order-dev.yml文件是否已经已经加载到本地

    发现配置信息并没有加载正确到本地,这个是为什么呢?我们先清空config的控制台,然后再浏览器端访问那个配置:http://localhost:8083/order-dev.yml。发现

    说明:会拿到两份配置,并会合并,我们之前的order.yml文件里面配置了eureka注册信息。这个时候是不是应该把order.yml删除了,其实不是的,既然,不管加载哪个配置都需要加载order.yml配置文件,那麽我们就应该把通用的配置信息加载到order.yml文件。这里我们先把order.yml文件里面信息先注释掉。
    然后重新访问就可以了。

2.6 统一配置中心延伸

我们想用统一配置中心的原因是:更新配置项目需重启,但是我们现在也没有达到。我们修改github上面的配置时候,我们还是没有变化。那么如何做到统一配置的的动态刷新呢?下节我们再学习。

3.SpringCloud Bus自动刷新配置

3.1 引言

Bus就是想上就上的公共汽车。他还有信息通路,总线,这里就是总线的意思。
以下是之前统一配置的架构图:

我们修改远程git配置,order服务、product服务不会更新修改的配置,原因是我们修改远端git配置后没有及时通知order、product微服务。消息的传递需要载体-消息队列。消息的通知SpringCloud中推荐使用消息队列。要操作消息和操作数据库一样,肯定要使用组件、引入依赖。SpringCloud Bus就是用来操作消息队列的。config-server和order通过消息队列传递信息。config-server使用SpringCloud Bus之后会对外提供一个http接口叫做/bus-refresh,访问这个接口,config-server就会把远程git的配置信息发送到mq里面,那谁来访问这个接口呢?自然是远端git访问这个接口最合适。git服务器基本会提供WebHooks功能,你只要把web服务器配置上就好了。

3.2 实战

1.config服务配置
  1. 1.修改config中pom.xml中SpringBoot版本
    由于其他版本有bug,我们引入:2.0.0.BUILD-SNAPSHOT
  2. 2.引入SpringCloud Bus组件

org.springframework.cloud
spring-cloud-starter-bus-amqp
  1. 3.启动config服务
    提前安装好RabbitMQ。
    安装请参考:
    可以参考:http://www.chinacion.cn/article/1700.html
    https://www.cnblogs.com/cvol/p/8818823.html
    启动服务后我们登录RabbitMQ发现了以下队列。
2.order服务配置
  1. 1.引入Bus依赖

org.springframework.cloud
spring-cloud-starter-bus-amqp
  1. 2.启动服务
    启动服务我们发现了其他一个mq执行了
3.测试修改github上配置看config服务是否不重启可以获取到

我们修改github上的order-dev.yml文件的env值为:dev11 然后刷新http://localhost:8083/order-dev.yml获取到的env为:dev111 但是order服务获取到的值:http://localhost:8082/env/dev中的值还是dev。说明order服务动态获取不到远程修改的配置。

现在config服务和order服务都已经跟消息队列RabbitMQ打通了,还差的是访问config的http接口:/bus-refresh

我们访问:发现报404错误,我们重启config发现重启日志中确实没有/bus-refresh的接口。

解决,我们需要配置一下,让 /bus-refresh接口暴露出来。在config服务的application.yml文件中添加:

重启后我们发现有相关日志打出:

我们使用postman请求一下:看对应的队列是否已经收到消息:

然后再消息队列中查看下消息是否有:

然后再刷新:http://localhost:8082/env/dev
还是dev 为什么没有变成:dev111呢?
其实通过查看本地日志 发现order服务已经加载了此配置。

这个时候我们通过注解@RequestScope实现
再次请求:http://localhost:8082/env/dev 发现已经变化了。

4.集成WebHooks实现动态更新

我们每次修改了github上的配置信息后,都需要在Order服务中通过http调用此接口才能让Order服务获取到对应的信息。我们现在需要把它做成自动实现。我们可以通过webhooks实现,

4.1 github上绑定webhooks

在github上绑定对应的请求:
注意:github上配置url地址不是:http://yyv7tg.natappfree.cc/actuator/bus-refresh
config组件专门提供了一个用于webhooks的路由,叫做monitor

我们需要把config-server对应的本地服务映射到公网
我们使用ngrok 也可以使用natapp.cn:使用说明:https://blog.csdn.net/weixin_38959210/article/details/80401208

我们在github上的webhooks更新地址:

修改远程的配置:env: dev11111
访问项目后:

说明成功。

SpringCloud-4-应用通信

1.HTTP vs RPC

  1. 应用(微服务)间通讯方式主要有两种:HTTP和RPC
  2. 两种方式的主角:RPC–Dubbo HTTP–SpringCloud
  3. Dubbo定位始终是一个RPC远程调用框架,而SpringCloud是微服务下的一站式解决方式
  4. SpringCloud微服务下服务调用使用的是:HTTP Restful,HTTP Restful本身轻量、适应性强、可以很容易跨语言跨平台。
  5. SpringCloud中服务间两种restful调用方式:1. RestTemplate 2.Feign

2.RestTemplate的三种使用方式

  1. 1.RestTemplate是一个HTTP客户端,类似于HttpClient,功能差不多,但是用法上更加简单。

    2.1 RestTemplate例子

  2. 1.使用 “订单服务->商品服务”为例
  3. 2.订单服务调用商品服务,我们把商品服务当做server端,订单服务当做client端
  4. 3.为了不影响之前的代码逻辑,我们新建单独的包为例子。
  5. 4.product端新建ServerController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author yxm
* @date 2019/4/20 0:16:16
*/
@RestController
public class ServerController {
@GetMapping("/msg")
public String msg(){
return "this is product server mmsg";
}
}
```
5. 5.order服务中新建ClientController
RestTemplate第一种方式

/**

  • @author yxm
  • @date 2019/4/20 0:18:18
    */
    @RestController
    @Slf4j
    public class ClientController {

    @GetMapping(“/getProductMsg”)
    public String getProductMsg(){

    //1.RestTemplate第一种方式 
    RestTemplate restTemplate = new RestTemplate();
    String response = restTemplate.getForObject("http://localhost:8080/msg", String.class);
    log.info("response{}",response);
    return response;
    

    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    由于product服务已经启用了8080端口,我们用-D指令在order服务中用-Dserver.port=8081,不建议直接写到配置文件。
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/84.png)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/85.png)
    以上是第一种调用方式。缺点:1.url为固定写死的,上线时候部署多台服务器,有时候连部署到哪台服务器都不知道。2.对方可能启动了多个实例,如果写死到一台实现不了负载均衡,其他服务器就不能访问了。
    6. 6.RestTemplate第二种方式

@RestController
@Slf4j
public class ClientController {

@Autowired
private LoadBalancerClient loadBalancerClient;

@GetMapping("/getProductMsg")
public String getProductMsg(){

    //2.第二种方式:通过LoadBalancerClient获取服务名,ip和port
    /**
     * SpringCloud提供了LoadBalancerClient,将其注入到Spring中
     */
    RestTemplate restTemplate = new RestTemplate();
    ServiceInstance product = loadBalancerClient.choose("PRODUCT");
    String url = String.format("http://%s:%s", product.getHost(), product.getPort());
    String response = restTemplate.getForObject(url, String.class);
    log.info("response{}",response);
    return response;
}

}

1
2
3
4
5
6
第二种方式每次都需要那么写(创建对象,获取ip,获取port,在调用获取),还是比较麻烦
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/85.png)
7. 7.第三种方式

@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

@RestController
@Slf4j
public class ClientController {
@Autowired
private RestTemplate restTemplate;

@GetMapping("/getProductMsg")
public String getProductMsg(){
    //3.第三种方式:通过RestTemplate的配置加LoadBalancerClient注解
    String response = restTemplate.getForObject("http://PRODUCT/msg", String.class);
    log.info("response{}",response);
    return response;
}

}

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
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/85.png)
#### 3.负载均衡器Ribbion
1. 1.前面我们在说Eureka时候,谈到了服务端发现和客户端发现。Eureka属于客户端发现的方式,他的负载均衡是软负载,也就是客户端会向服务器(例如:EurekaServer)拉取已经注册的可用服务信息,然后根据负载均衡策略直接命中哪台服务器、发送请求。这整个过程都是在客户端完成的,并不需要服务端参数。SpringCloud中客户端负载均衡就是Ribbion 。他是基于NetflexRibbion实现的。通过SpringCloud的封装可以轻松的实现面向服务的restful模板请求自动转化成客户端负载均衡服务调用。
一下组件都使用到了Ribbon:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/9.png)
2. 2.SpringCloud在结合了Ribbon的负载均衡实现中,封装增加了HttpClient和OkHttp两种请求端实现,默认使用了Ribbon对Eureka服务发现的负载均衡client。
3. 3.在上一节小结中,我们介绍了RestTemplate的三种实现方式。其中通过添加@LoadBalanced注解或者 直接写代码时候使用LoadBalancerClient,其实用到的就是Ribbon的组件
4. 4.添加@LoadBalanced注解后,Ribbon会通过LoadBalancerClient自动的帮助你基于某种规则,比如:随机简单的轮训去连接目标服务,从而很容易使用Ribbon实现自定义的负载均衡算法。
5. 5.Ribbon实现负载均衡核心有3点:1.服务发现(也就是发现依赖服务的列表,也就是依据服务的名字,把该服务下的实例全都找出来) 2.服务选择规则(依据规则策略:如何从多个服务中选择一个有效的服务) 3.服务监听(监测失效服务做到高效剔除)
6. 6.主要组件是:ServerList、IRule、ServerListFilter。总体流程是:首先通过ServerList获取所有的可用服务列表。然后通过ServerListFilter过滤掉一部分地址,最后剩下的地址中通过IRule选择一个实例作为最终目标结果。
#### 4 追踪源码自定义负载均衡策略
#### 5 Feign的使用
##### 5.1 Feign的基本使用
1. 1.本节使用Feign实现应用间通信。
2. 2.现在我们使用了Feign就不用使用restTemplate了,我们现在在ClientController中将restTemplate相关删除,并将RestTemplateConfig删除
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/10.png)
3. 3.添加依赖:


org.springframework.cloud
spring-cloud-starter-feign

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
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/11.png)
4. 4.启动类中添加注解
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/12.png)
5. 5.定义好feign要调用的Server端接口
建立client包,定义一个ProductClient接口(里面的方法就是调用product服务的方法),此client代表order服务是一个相对于product的客户端服务。
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/13.png)
product中ServerController里面调用的方法:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/14.png)
6. 6.此接口定义完之后,在Controller里面定义,然后接口请求:http://localhost:8081/getProductMsg
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/15.png)
##### 5.2 Feign的总结
1. 声明式REST客户端(伪RPC)
2. 采用了基于接口的注解(定义一个接口,然后在其上加注解)
3. 内部使用了Ribbon作为负载均衡
#### 6 Feign获取商品列表
在Oder服务的OrderServiceImpl中的create里面1、2、3是我们还没有做的,这节我们需要通过Feign获取商品列表
##### 6.1 Product服务中添加获取商品列表
###### 1.Product-->dao
ProductInfoRepository:

public interface ProductInfoRepository extends JpaRepository{
List findByProductStatus(Integer productStatus);
List findByProductIdIn(List productIdList);
}

1
2
3
4
###### 2.Product-->service
ProductService:

public interface ProductService {
/**

 * 查询所有在架商品列表
 */
List<ProductInfo> findUpAll();
/**
 * 查询商品列表
 */
List<ProductInfo> findList(List<String> productIdList);

}

1
2
3
ProductServiceImpl:

@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductInfoRepository repository;
@Override
public List findUpAll() {
return repository.findByProductStatus(ProductStatusEnum.UP.getCode());
}
@Override
public List findList(List productIdList) {
return repository.findByProductIdIn(productIdList);
}
}

1
2
3
###### 3.Product-->controller
ProductController:

@RestController
@RequestMapping(“/product”)
public class ProductController {
@Autowired
private ProductService productService;
@Autowired
private ProductCategoryService productCategoryService;
/**

 * 获取商品列表(给订单服务使用)
 * @param productIdList
 * @return
 */
@GetMapping("/listForOrder")
public List<ProductInfo> listForOrder(@RequestBody List<String> productIdList){
    return productService.findList(productIdList);
}

}

1
2
3
4
5
6
##### 6.2 Order服务中调用商品列表
在client的ProductClient添加调用product的方法
###### 1.Order-->ProductClient

@FeignClient(name = “product”)
public interface ProductClient {
@GetMapping(“/listForOrder”)
List listForOrder(@RequestBody List productIdList);
}

1
2
###### 2.Order-->ClientController

@RestController
@Slf4j
public class ClientController {
@Autowired
private ProductClient productClient;
@GetMapping(“/getProductMsg”)
public String getProductMsg(){
String msg = productClient.productMsg();
log.info(“getProductMsg()”,msg);
return msg;
}
@GetMapping(“/getProductList”)
public String getProductList(){
List productInfos = productClient.listForOrder(Arrays.asList(“157875196366160022”));
return “ok”;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
###### 3.Order-->请求测试
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/16.png)
#### 7.扣库存(Feign)
我们已经获取了商品列表,现在开始在Product微服务实现扣库存
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/17.png)
从创建订单接口我们知道,前端会传过来商品的信息和购买数量:我们在DTO中构造一个对象:CartDTO用来传送上面的对象
1. 1.Product-->CartDTO对象
新建DTO包,新建CartDTO类:

public class CartDTO {
/**

  • 商品id
    /
    private String productId;
    /*
    • 商品数量
      */
      private Integer productQuantity;
      public CartDTO(){
      }
      public CartDTO(String productId,Integer productQuantity){
      this.productId = productId;
      this.productQuantity = productQuantity;
      }
      }
      1
      2
      3
      2. 2.Product-->Exception自定义异常

public class ProductException extends RuntimeException {
private Integer code;
public ProductException(Integer code,String message){
super(message);
this.code =code;
}
public ProductException(ResultEnum resultEnum){
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
}

1
2
3
4
3. 3.Product-->返回枚举

public enum ResultEnum {
PRODUCT_NOT_EXIST(1,”商品不存在”),
PRODUCT_STOCK_ERROE(2,”商品库存错误” );
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}

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
4. 4.Product-->Service里面新建方法
```
@Override
@Transactional(rollbackFor = Exception.class)
public void decreaseStock(List<CartDTO> cartDTOList) {
/**
* 遍历:查看是否存在
*/
for (CartDTO cartDTO:cartDTOList){
Optional<ProductInfo> productInfoOptional = repository.findById(cartDTO.getProductId());
//商品不存在
if(!productInfoOptional.isPresent()){
throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST);
}
//商品存在-库存错误
ProductInfo productInfo = productInfoOptional.get();
int result = productInfo.getProductStock() - cartDTO.getProductQuantity();
if(result<0){
throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROE);
}
//保存
productInfo.setProductStock(result);
repository.save(productInfo);
}
}

  1. 5.Product–>controller
1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/decreaseStock")
public void decreaseStock(@RequestBody List<CartDTO> cartDTOList){
productService.decreaseStock(cartDTOList);
}
}
  1. 6.在Order服务中client里面的ProductClient添加:减库存方法
1
2
3
4
5
6
7
@FeignClient(name = "product")
public interface ProductClient {
@GetMapping("/msg")
String productMsg();
@PostMapping("/product/decreaseStock")
void decreaseStock(@RequestBody List<CartDTO> cartDTOList);
}
  1. 7.Order服务中:ClientController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@Slf4j
public class ClientController {
@Autowired
private ProductClient productClient;
@GetMapping("/getProductMsg")
public String getProductMsg(){
String msg = productClient.productMsg();
log.info("getProductMsg()",msg);
return msg;
}
@GetMapping("/getProductList")
public String getProductList(){
List<ProductInfo> productInfos = productClient.listForOrder(Arrays.asList("157875196366160022"));
return productInfos.toString();
}
@GetMapping("/productDecreaseStock")
public String productDecreaseStock(){
productClient.decreaseStock(Arrays.asList(new CartDTO("157875196366160022",1)));
return "ok";
}
}
  1. 8.测试

    库存减少了

8.整合接口打通下单流程(Feign)

之前我们已经完善了,商品的查询,扣库存,那么我们这一节就要完成整套业务的打通。我们之前在Order服务中,创建订单时候,里面还有需要实现的如下几步:

  1. 1.现在我们补充完毕:
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
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMasterRepository orderMasterRepository;
@Autowired
private OrderDetailRepository orderDetailRepository;
@Autowired
private ProductClient productClient;
@Override
public OrderDTO create(OrderDTO orderDTO) {
String orderId = KeyUtil.genUniqueKey();
//1.TODO 查询商品信息(调用商品服务)
List<String> productIdList = orderDTO.getOrderDetailList().stream().map(OrderDetail::getProductId).collect(Collectors.toList());
List<ProductInfo> productInfoList = productClient.listForOrder(productIdList);
//2.TODO 计算订单总价
//定义总价
BigDecimal orderAmout = new BigDecimal(BigInteger.ZERO);
for(OrderDetail orderDetail:orderDTO.getOrderDetailList()){
//总价=(单价*数量)+总价
Integer productQuantity = orderDetail.getProductQuantity();
for(ProductInfo productInfo:productInfoList){
if(productInfo.getProductId().equals(orderDetail.getProductId())){
orderAmout = productInfo.getProductPrice().multiply(new BigDecimal(productQuantity)).add(orderAmout);
//订单详情赋值
BeanUtils.copyProperties(productInfo,orderDetail);
orderDetail.setOrderId(orderId);
orderDetail.setDetailId(KeyUtil.genUniqueKey());
//订单详情入库
orderDetailRepository.save(orderDetail);
}
}
}
//3.TODO 扣除库存(调用商品服务)
List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream()
.map(e -> new CartDTO(e.getProductId(), e.getProductQuantity()))
.collect(Collectors.toList());
productClient.decreaseStock(cartDTOList);
//4. 订单入库
OrderMaster orderMaster = new OrderMaster();
//先设置主键 会copy到orderMaster
orderDTO.setOrderId(orderId);
BeanUtils.copyProperties(orderDTO,orderMaster);
orderMaster.setOrderAmount(orderAmout);
orderMaster.setOrderStatus(OrderStatusEnum.NEW.getCode());
orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
orderMasterRepository.save(orderMaster);
return orderDTO;
}
}
  1. 2.测试

9.项目改造成多模块

多模块改造参考:https://blog.csdn.net/yangshangwei/article/details/88809468

9.1 项目缺陷

虽然我们之前已经完成了下单并扣除库存,虽然通信是完成了,但是有些地方做的不好。有如下问题:
1.在Product微服务中,获取商品列表(给订单服务使用)

返回的ProductInfo是数据库实体:

基本上,我们不会把自己数据库映射的表对象暴露给外部的。
2.在订单服务和商品服务之间都有共有的对象:CartDTO,ProductInfo.重复定义。对象属于哪一个服务就在那一个服务中定义。

3.在Order(订单)服务里,定义了ProductClient接口使用Feign主键调用,我们在项目中可能一个服务就是很多人来做,各个服务费透明的,所以我们在Oder服务中不能把Product的服务代码请求路径等写到Order服务中.

9.2 Product多模块拆分
9.2.1模块之间职责

针对解决以上情况,我们把项目分成3个模块:product-server、prodect-client、product-common

  1. 1.product-server:存放所有业务逻辑:包括,controller、service
  2. 2.prodect-client:对外暴露的接口,商品模块对外暴露的接口:商品列表和扣库存。
  3. 3.product-common:公用的对象:既会被内部模块调用,也会被外部模块调用。
    9.2.2模块之间依赖关系
  4. 1.product-common是公用的对象,所以product-server会依赖product-common,返回的商品对象prodect-client也会依赖product-common

  5. 2.product商品多模块划分可以参考:https://blog.csdn.net/qq_29479041/article/details/84230669

  6. 3.在本地打包
    1
    mvn -Dmaven.test.skip=true -U clean install

使用以上命令,会将对应模块打包并安装到本地maven仓库

9.3 Order多模块拆分
  1. 1.以此类推,Order服务也分为3个模块,但是order-client目前是没有代码:其目前不需要给外提供服务 order-common目前也没有代码:目前没有需求
  2. 2.在订单服务需要注意的是:订单服务调用商品服务,那么需要在订单服务里面的启动类中添加扫描到商品服务的路径:@EnableFeignClients(basePackages = “com.yxm.product.client”)
  3. 3.最外层pom文件引入product-client的jar包
  4. 4.是管理jar包版本,不会下载对应依赖
9.5 测试

启动注册中心,通过product-server和order-server中的main函数启动俩微服务

下单

1
2
select * from order_master a where a.order_id = '1558247336963513928'
select * from order_detail a where a.order_id = '1558247336963513928'

源码:github地址:https://github.com/startshineye/SpringCloud_Shell/tree/develop_multimodulw

10.同步或者异步

  1. 1.当前订单服务和商品服务,两个服务之间的通讯机制是同步的。订单会调用商品服务的扣库存接口。微服务中除了同步,有很多时候会集成到异步的场景下,通过队列和订阅主题,实现消息的 发布和订阅,一个微服务可以是消息的发布者,通过异步的方式发送到队列和订阅主题下,作为消费者的微服务,可以从队列或者主题中获取消息。通过消息中间件。把服务间的直接调用解耦。
  2. 比如以下图中:用户登录的时候,用户服务需要调用短信服务发短信。要给用户加积分,需要调用积分服务。还可能有其他服务,如果都采用同步的机制,服务间耦合过大,用户登录成功,需要向多个服务同步响应后才会成功,就会造成不好的用户体验。这个时候,我们通过消息队列可以实现很好的异步调用。
  3. 3.再比如:订单服务在口库存前会调用查询商品服务,之后再调用减库存的接口来扣库存。我们对其改造:商品服务在更改库存的时候、发布库存变化的消息、订单服务来订阅这个消息、可以获取到商品的部分信息,比如:可购买的商品个数、商品id。订单服务在下单的时候不必同步的去查询数据确定商品的库存信息 而是查询自己服务中数据 然后在扣库存时候订单服务发布一个扣库存的消息、商品服务订阅这个消息。拿到消息后,减少本库存消息的库存量。
  4. 4.消息中间件:目前常见的消息队列为:RabbitMQ、Kafka、ActiveMQ,我们现在使用RabbitMQ

11.RabbitMQ的安装

进入下载网址:https://www.rabbitmq.com/download.html
我们使用docker安装RabbitMQ

我们使用带有管理界面的mq:3.8.0-beta.4-management

https://blog.csdn.net/antma/article/details/81334932
https://www.jianshu.com/p/f3e49b495d74

SpringCloud-3-服务拆分

1.微服务拆分的起点

1.1如何拆分微服务?

现在微服务概念炒得很热,关于如何拆分微服务,有以下几点。

  1. 1.先明白起点和终点
    起点:既有架构的形态。
    终点:好的架构不是设计出来的,而是进化而来的。
  2. 典型架构

1.2 适合上微服务么?

业务形态不适合的
1.系统中包含很多很多强事务场景的(因为微服务是分布式的,分布式强事务CAP最多也就能达到最终一致性)
2.业务相对稳定、迭代周期长
3.访问压力不大、可用性要求不高(中小型企业的OA)

2.康威定律和微服务

2.1 康威定律

除了业务形态不适合的,其实还有其他条件,很可能导致不适合迁移到微服务中去,首先我们看一下微服务理论基础:康威定律。

一句话概括就是:沟通的问题会影响系统设计
所以微服务都是强调小团队开发,大的系统拆分成微服务时候,大的团队随机也会拆分成小的团队。

2.2 微服务和团队结构
  1. 微服务特点
  2. 传统vs微服务

3.点餐业务服务拆分分析

点餐系统分为:买家端 和 卖家端

3.1 服务拆分


上面:服务两种方式拆分:
第一种(按照终端):买家端(手机端)的ui单独为一个服务放到nginx里面,卖家端(PC端)的ui单独为一个服务,两个服务同时向后端的通用服务请求数据
第二种(按照业务):将订单ui,商品ui,支付ui都放到一个边缘服务。

以上两种都不对,如果只是自己运营的的点餐应用:团队只有一个,并且业务变化也不大,没有微服务化的必要。如果是一个快速发展的IT公司点餐部门,业务快速发展,需求不断提出,所以需要拆分成微服务。

所以说:起点和团队结构,沟通方式都会决定微服务的设计。

3.2 服务拆分方法论

下图出自<<可扩展的艺术>>书籍

  1. X轴 水平复制:通过应用程序扩展,通过负载均衡运行多个父本一样的应用程序
  2. Z轴 数据分区:将服务按数据分区、每个服务器负责一个数据子集、每个服务器运行的代码是一样的
  3. Y轴 功能解耦:不同职责模块分成不同服务

通过以上模型,知道服务拆分的两个关键职责:功能和数据

3.2.1 如何拆”功能”
  1. 1.单一职责(每个服务负责业务功能的单独一部分)、松耦合(服务之间耦合度低,修改一个服务不用导致另一个服务修改)、高内聚(服务内部相关行为都聚集在一个服务内,而不是分散在不同服务中心)。
  2. 2.关注点分离:-按职责 -按通用性 -按粒度级别(微服务并不是越小越好,要合适)
3.2.2 如何拆”数据”

拆分功能和拆分数据是有先后顺序

  1. 1.先考虑业务功能、再考虑业务功能对应的数据
  2. 2.无状态服务(状态:如果一个数据要被多个服务共享才能完成一个请求,那么这个数据就称为状态,进而依赖这个状态数据的服务称为有状态服务)

    如上所示把业务数据存放到有状态服务的数据库,缓存中实现前端微服务和后端微服务质检无状态服务,数据间没有太大耦合性。

如何拆数据:
1.每个微服务都有单独数据存储(微服务共有数据库,有可能其中一个数据库出现问题会影响其他微服务,一个服务要获取另一个服务的数据,不能直接连接库去请求,而是调用另一个服务接口去获取数据,服务之间有隔离)
2.依据服务特点选择不同结构的数据库类型(如果:数据基于搜索的,那么es合适,如果是非机构化数据,那么nosql的mongodb合适)
3.难点在确定边界

3.2.3 点餐业务服务拆分分析


1.不要期望服务拆分一次就正确,微服务是不断演进的

4.商品服务

4.1 商品服务api和sql介绍

商品列表

1
GET /product/list

参数

1

返回

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
{
"code": 0,
"msg": "成功",
"data": [
{
"name": "热榜",
"type": 1,
"foods": [
{
"id": "123456",
"name": "皮蛋粥",
"price": 1.2,
"description": "好吃的皮蛋粥",
"icon": "http://xxx.com",
}
]
},
{
"name": "好吃的",
"type": 2,
"foods": [
{
"id": "123457",
"name": "慕斯蛋糕",
"price": 10.9,
"description": "美味爽口",
"icon": "http://xxx.com",
}
]
}
]
}
```
sql

– 类目
CREATE TABLE product_category (
category_id INT NOT NULL AUTO_INCREMENT,
category_name VARCHAR(64) NOT NULL COMMENT ‘类目名字’,
category_type INT NOT NULL COMMENT ‘类目编号’,
create_time TIMESTAMP NOT NULL COMMENT ‘创建时间’,
update_time TIMESTAMP NOT NULL COMMENT ‘修改时间’,
PRIMARY KEY (category_id),
UNIQUE KEY uqe_category_type (category_type)
);
– 商品
CREATE TABLE product_info (
product_id VARCHAR(32) NOT NULL,
product_name VARCHAR(64) NOT NULL COMMENT ‘商品名称’,
product_price DECIMAL(8,2) NOT NULL COMMENT ‘单价’,
product_stock INT NOT NULL COMMENT ‘库存’,
product_description VARCHAR(64) COMMENT ‘描述’,
product_icon VARCHAR(512) COMMENT ‘小图’,
product_status TINYINT(3) DEFAULT ‘0’ COMMENT ‘商品状态,0正常1下架’,
category_type INT NOT NULL COMMENT ‘类目编号’,
create_time TIMESTAMP NOT NULL COMMENT ‘创建时间’,
update_time TIMESTAMP NOT NULL COMMENT ‘修改时间’,
PRIMARY KEY (product_id)
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
##### 4.2 商品服务编码
###### 4.2.1 创建工程
1. 创建一个Eureka客户端服务,并修改pom.xml文件 保持版本一致性。
2. 修改项目名称并注册到Eureka注册中心
3. 添加注解:@EnableDiscoveryClient
###### 4.2.2 启动项目,查看Eureka Server是否有注册的服务
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/73.png)
###### 4.2.3 书写业务
1. 1.添加mysql驱动和jpa依赖
2. application.yml中配置数据源
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/74.png)
#### 5.订单服务
##### 5.1 业务逻辑分析
### 创建订单

POST /order/create

1
2
参数

name: “张三”
phone: “18868822111”
address: “总部”
openid: “ew3euwhd7sjw9diwkq” //用户的微信openid
items: [{
productId: “1423113435324”,
productQuantity: 2 //购买数量
}]

1
2
3
返回

{
“code”: 0,
“msg”: “成功”,
“data”: {
“orderId”: “147283992738221”
}
}

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
72
73
74
订单服务业务逻辑思路:
1.参数校验
2.查询商品信息(此时信息有可能不在订单系统中,可能在商品微服务中,则调用商品服务查询)
3.计算总价
4.扣库存(调用商品服务)
5.订单入库
##### 5.2 编码实践
###### 5.2.1 创建应用 注册到注册中心
略(按照:client客户端创建)
###### 5.2.2 dao
订单服务业务逻辑思路:
1.参数校验
2.查询商品信息(此时信息有可能不在订单系统中,可能在商品微服务中,则调用商品服务查询)
3.计算总价
4.扣库存(调用商品服务)
5.订单入库
###### 5.2.3 service
1. 由于传递的参数包含:买家信息和订单信息,所以创建订单的service需要做一个数据转换 然后insert到对应买家和订单表里面,创建dto(Data Transfer Object)包,创建OrderDTO(买家和订单详情是一对多的关系,所以里面OrderDTO里是一个买家信息加许多商品信息)
2. OrderService里面创建订单的参数和返回为:OrderDTO
3. OrderServiceImpl里面实现创建订单:分为以下四步:1.查询商品 2.计算总价 3.扣除库存 4.订单入库。由于前3步都需要调用其他商品服务,所以我们写为:TODO
4. 创建OrderMaster时候,我们需要设置买家状态:orderStatus,payStatus我们用枚举实现如下:
```
public enum OrderStatusEnum {
NEW(0, "新订单"),
FINISHED(1, "完结"),
CANCEL(2, "取消"),
;
private Integer code;
private String message;
OrderStatusEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
public enum PayStatusEnum {
WAIT(0, "等待支付"),
SUCCESS(1, "支付成功"),
;
private Integer code;
private String message;
PayStatusEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
```
5. OrderId是主键,我们用简单的KeyUtil生成工具生成

public class KeyUtil {
/**

 * 生成唯一的主键
 * 格式: 时间+随机数
 */
public static synchronized  String  genUniqueKey(){
    Random random = new Random();
    Integer number = random.nextInt(900000)+100000;
    return System.currentTimeMillis()+String.valueOf(number);
}

}

1
2
3
4
5
6
7
8
6. service逻辑如下:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/82.png)
###### 5.2.4 controller
1. controller里面需要实现的业务逻辑大部分都是在service中,此处只是多了个参数校验,所以分为以下几步:1.参数校验 2.查询商品信息(调用商品服务) 3.计算总价 4.扣库存(调用商品服务) 5.订单入库
2. 涉及很多参数时候,我们封装前端参数,此处叫:OrderForm,并在里面添加了参数校验,不用在controller方法里面(使用@NotEmpty)校验。OrderForm此处省略getter,setter也可以用lombok加上@Data注解。

public class OrderForm {
/**

 * 买家姓名
 */
@NotEmpty(message = "姓名必填")
private String name;

/**
 * 买家手机号
 */
@NotEmpty(message = "手机号必填")
private String phone;

/**
 * 买家地址
 */
@NotEmpty(message = "地址必填")
private String address;

/**
 * 买家微信openid
 */
@NotEmpty(message = "openid必填")
private String openid;

/**
 * 购物车
 */
@NotEmpty(message = "购物车不能为空")
private String items;

}

1
2
3
4
5
3. 在create方法中添加@Valid注解
4. 如果参数校验有错误时候抛出异常(自定义异常)OrderException(Integer code, String message),其中code,message不使用硬编码,定义一个枚举:ResultEnum

public class OrderException extends RuntimeException {
private Integer code;
public OrderException(Integer code, String message){
super(message);
this.code = code;
}
public OrderException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
}

@Getter
public enum ResultEnum {
PARAM_ERROR(1, “参数错误”),
CART_EMPTY(2, “购物车为空”)
;

private Integer code;

private String message;

ResultEnum(Integer code, String message) {
    this.code = code;
    this.message = message;
}

}

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
5. 在controller里面将校验过的参数:OrderForm转换成OrderDTO,此处转换封装成工具类处理(其中json转换成数据使用Gson),转换失败时候抛出异常,打印日志
```
@Slf4j
public class OrderForm2OrderDTOConverter {
public static OrderDTO convert(OrderForm orderForm) {
Gson gson = new Gson();
OrderDTO orderDTO = new OrderDTO();
orderDTO.setBuyerName(orderForm.getName());
orderDTO.setBuyerPhone(orderForm.getPhone());
orderDTO.setBuyerAddress(orderForm.getAddress());
orderDTO.setBuyerOpenid(orderForm.getOpenid());
List<OrderDetail> orderDetailList = new ArrayList<>();
try {
orderDetailList = gson.fromJson(orderForm.getItems(),
new TypeToken<List<OrderDetail>>() {
}.getType());
} catch (Exception e) {
log.error("【json转换】错误, string={}", orderForm.getItems());
throw new OrderException(ResultEnum.PARAM_ERROR);
}
orderDTO.setOrderDetailList(orderDetailList);
return orderDTO;
}
}

  1. 接口返回的结果data里面只有一个字段,所以不需要创建一个对象,一个封装一个map返回就行
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 1.参数校验
     * 2.查询商品信息(调用商品服务)
     * 3.计算总价
     * 4.扣库存(调用商品服务)
     * 5.订单入库
     */
    @PostMapping("/create")
    private Object create(@Valid OrderForm orderForm,
                          BindingResult bindingResult){
        //检验不通过抛出自定义异常
        if (bindingResult.hasErrors()){
            //https://www.cnblogs.com/weiapro/p/7633645.html
           log.error("【创建订单】参数不正确, orderForm={}", orderForm);
            throw new OrderException(ResultEnum.PARAM_ERROR.getCode(),
                    bindingResult.getFieldError().getDefaultMessage());
        }
        //orderForm->orderDTO
        OrderDTO orderDTO = OrderForm2OrderDTOConverter.convert(orderForm);
        if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){
            log.error("【创建订单】购物车信息为空");
            throw new OrderException(ResultEnum.CART_EMPTY);
        }
        OrderDTO result = orderService.create(orderDTO);
        Map<String, String> map = new HashMap<>();
        map.put("orderId", result.getOrderId());
        return ResultVOUtil.success(map);
    }
}

SpringCloud-2-服务注册与发现

1.SpringCloud Eureka

1.1 简单微服务架构

上图中的服务注册和发现在SpringCloud中用的就是:Eureka

1.2 简单介绍
  1. 基于Netflix Eureka做了二次封装
  2. 主要由两个组件组成: -Eureka Server(注册中心) -Eureka Client(服务中心)

2.Eureka Server

2.1 简介
  1. 注册中心好比老师手中名单 记录着所有同学名字 点名时候,根据手册名单
  2. 注册中心记录着所有应用的信息和状态(应用名,所在服务器,是否正常工作),在微服务架构中,我们把应用叫做服务
  3. 由图所知:服务中心就是找到服务
    2.2 创建Eureka Server应用




项目创建后我们不着急启动,先看下pom.xml文件:
SpringBoot的版本和SpringCloud版本如下所示:

我们如何知道SpringBoot版本和SpringCloud的版本匹配呢?我们进入SpringCloud官网:https://spring.io/

将pom.xml中设置成统一匹配的:

关于版本的具体问题,请参考:https://blog.csdn.net/sinat_33889619/article/details/89403796

2.3 启动Eureka Server

1.启动时候报错:没有出现注册中心界面

2.添加@EnableEurekaServer注解

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}

重启发现出现注册中心的界面:

3.启动时候虽然可以访问,但是出现了下面的错误:

1
com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server

这是因为此应用不仅是一个server端同时也是一个client端。他也需要找到一个注册中心,把自己注册上去。我们配置一下他注册的地址:就是往自己身上注册。

点击service-url进入:EurekaClientConfigBean

1
2
3
4
5
6
7
public void setServiceUrl(Map<String, String> serviceUrl) {
this.serviceUrl = serviceUrl;
}
```
serviceUrl是一个map的key为:defaultZone

eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka/

1
2
3
4
5
6
7
重启项目时候:注册时候是心跳检测注册,刚开始会有错误,等一会就可以了,如下已经注册上:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/37.png)
由于注册的Application的名字为:UNKNOWN 我们修改此应用的名字,添加如下配置:

spring:
application:
name: eureka

1
2
3
4
5
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/38.png)
4.由于此应用本来就是一个注册中心,自己注册到自己不出现在注册实例中:添加如下配置:register-with-eureka: false

eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka/
register-with-eureka: false

1
2
3
4
5
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/39.png)
5.由于我们后面很多应用都是8080端口,所以我们修改注册中心的地址为默认端口:8761

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
register-with-eureka: false
spring:
application:
name: eureka
server:
port: 8761

1
2
3
4
5
6
#### 3.Eureka Client(服务注册)
##### 3.1 启用注册中心
注册中心为了方便不需要每次在idea中启动,我们应该打成jar包,然后通过命令在后台启用。进入eureka的根目录,执行:

mvn clean package

1
2
3
生成后端启动:

nohup java -jar eureka-0.0.1-SNAPSHOT.jar &

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
##### 3.2 创建Eureka Client应用
1. 创建应用
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/25.png)
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/40.png)
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/41.png)
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/42.png)
删除不需要的文件:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/43.png)
client端和server端的版本保持一致:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/44.png)
2. 添加注册中心地址:

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: client

1
2
3
4
5
6
7
8
3. 开启服务注册
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/45.png)
有时候启动报错:

Invocation of destroy method failed on bean with name ‘scopedTarget.eurekaClient’: org.springframework.beans.factory.BeanCreationNotAllowedException: Error creating bean with name ‘eurekaInstanceConfigBean’: Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)

1
2
3
4
解决:添加web依赖


org.springframework.boot
spring-boot-starter-web

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
4. 查看是否已经注册
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/46.png)
5. 如果不停的启动client端,会在注册中心出现:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/47.png)
client<--->server端是采用心跳的机制,server端会不听的检查client端是否存活上线,在一定时间会统计出client端的上线率,当低于某个比例时候 会报出以上警告,在开发环境我们可以把它关掉(在server端里面配置文件加上:)
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/48.png)
#### 4.Eureka高可用
##### 4.1 Eureka单节点
1. 上面的Eureka是单点的,如果挂了的话 所以client端服务都不可以运行了
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/49.png)
##### 4.2 Eureka多节点之间互相注册,但是client注册到其中一个Eureka
1. 让Eureka Server开启两个,之间互相注册,虽然client只在server1上注册,但是会把client注册也会拷贝到server2注册
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/50.png)
2. 开启两个Eureka Server端口分别为:8761、8762
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/51.png)
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/52.png)
以端口来区分:Eureka1:8761 Eureka2:8762
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/53.png)
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/54.png)
注释掉yml中原来端口:

//server:
//port: 8761
```


启动


启动

访问:http://localhost:8762 我们可以看到之前注册到8761的服务也注册到8762上了,这是因为:8761和8762之间互相注册了。互相注册后,他们之上的信息有所交换。

4.3 Eureka1挂了 重启Eureka2和client

此时我们重启client和Eureka2时候,我们发现在Eureka2上没有发现client,这个时候我们就需要把client注册到Eureka1和Eureka2上才行。

4.4 Eureka包含3个以上

有时候我们服务比较多,并且需要的注册中心也比较多,也就是Eureka Server比较多,我们该怎么做呢?

5.Eureka总结

6.分布式下服务注册的地位和原理

6.1 传统服务发现

传统服务A要找到服务B,是通过在服务A中配置B的地址从而找到B,在分布式系统中,各个系统之间是不共享内存的,如下A服务如果要找到B服务就需要,并且B服务是分布式集群的,B中的服务器是不确定多少台,在云服务时代,服务器根据压力容量可能会动态改变,此时如果还是在A里面配置多个B节点注册中心地址不可取。

6.2 注册中心服务发现

  1. 类似于注册中心就是个代理,需要找谁就找谁
  2. 通过以上可以知道注册中心 是分布式服务中最重要的基础部分。

  3. A是如何通过”注册中心”找导服务B的呢?
    客户端发起:A通过注册中心可以找到需要B,从中只需要找到一个B(通过轮训、hash等负载均衡机制等方式)
    服务端发现:通过代理的方式

6.3 思考
  1. SpringCloud是提供java应用程序之间的服务注册,那么如果有些服务是c/php/node.js写的服务,该怎样取进行服务发现和注册了,SpringCloud的Eureka使用客户端发现方式进行服务发现,SpringCloud是一个强大的我服务体系,但是是纯java的,山本大叔倡导轻量级的方式去实现微服务,所以采用了Http Restful API的方式,并且提供了Restful API接口让其他不同语言的服务区实现接口,达到服务注册。
  2. 微服务具有异构特点:各个服务间可以用不同语言,每个服务可以根据需要选择不通的数据库

SpringClond-1-微服务介绍

1.前言

  1. 如今微服务已经是一个趋势,越来越多的人倾向于使用SpringCloud搭建微服务,SpringCloud已经成为了主流趋势。
  2. 要学习微服务的话,需要掌握如下学习曲线:学习微服务,首先需要学习SpringCloud,学习SpringCloud需要学习SpringBoot。微服务–>SpringCloud–>SpringBoot

2.SpringCloud微服务架构实践

2.1 微服务相关技术简介

究竟微服务、SpringCloud、SpringBoot之间有什么联系?并且:SpringFramework、SpringBoot、SpringCloud之间又有什么关系呢?我们看如下分析:

  1. 1.Spring框架作为j2ee的基石,一直在快速发展、是搭建j2ee的框架主要组成部分。
  2. 2.SpringBoot在Spring的上层、基于Spring框架构建的一种延伸的简化框架。能够快速构建Spring应用。
  3. 3.SpringCloud又是在SpringBoot的上层,利用SpringBoot构建分布式应用。
2.2 技术储备
  1. 1.对SpringBoot的基础知识熟练掌握。
  2. 2.对Linux和Docker的基本用法熟练掌握
2.3 重点
  1. 1.SpringCloud构建微服务(我们以点餐项目为例构建基本的SpringCloud微服务)
  2. 2.微服务改造探讨(大多数微服务并不是一开始就设计好的,而是业务发展到一定规模时候产生的,我们会介绍将传统服务改造成微服务)
  3. 3.本文课程使用SpringBoot2.x,课程讲解内容如下:

    主要讲解上面部分外,我们还会讲解:容器编排和服务追踪。
    1.容器编排方面使用:docker+rancher组合。
    2.服务追踪方面使用:SpringCloudSleuth+ZIPKIN组合。
2.4 环境参数

毕业于<br>相信技术可以改变人与人之间的生活<br>码农一枚