微服务
微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间相互协调,相互配合,为用户体现最终价值。每个服务运行在其独立的进程当中,服务与服务之间采用轻量级的通信机制互相协作(通常是基于http/restful API)。
SpringCloud
分布式微服务架构的一站式解决方案,是多种微服务架构的技术集合体,俗称微服务全家桶。
SpringCloud技术栈
服务注册与发现:EUREKA、Zookeeper、consul、Nacos
服务负载与调用:RIBBON、OPEN FEIGN
服务熔断降级:HYSTRIX、Sentinel
服务网关:Zuul,GateWay
服务分布式配置:Spring Cloud Config、Nacos
服务消息总线:Spring Cloud Bus、Nacos
服务开发:Spring Boot
Boot和Cloud技术选型
SpringBoot2.X版和SpringCloud H版
SpringCloud Alibaba
微服务架构编码构建
微服务cloud整体聚合父工程Project
父工程步骤
- 1.New Project
- 2.聚合总工程名字
- 3.Maven选版本
- 4.工程名字
- 5.字符编码
- 6.注解生效激活
- 7.java编译版本选8
- 8.File Type过滤
Maven工程落地细节复习
- Maven中的dependencyManagement和dependencies
- dependencyManagement:只在父工程中出现,用于版本固定,不会引入Jar包
- maven中跳过单元测试
- Maven中的dependencyManagement和dependencies
父工程创建完成执行mvn:install将父工程发布到仓库方便子工程继承
创建子模块,pay模块
- 创建module
- 改pom
- 写yml
- 主启动
- 业务类
业务类
sql
实体类
- CommonResult类
- 实体类
dao
接口
mabatis映射文件
在resource下,创建mapper/PayMapper.xml
service
- service
- impl
controller
测试
postman
模拟post请求切换Run DashBoard运行窗口(分布式的运行窗口)
- 在项目路径的.idea/workspace.xml
填入以下内容: <option name="configurationTypes"> <set> <option value="SpringBootApplicationConfigurationType" /> </set> </option>
热部署
order模块
- 创建module
- 改pom
- 写yml
- 主启动
- 业务类
业务类
复制pay模块的实体类,entity类
写controller类
因为这里是消费者类,主要是消费,那么就没有service和dao,需要调用pay模块的方法
并且这里还没有微服务的远程调用,那么如果要调用另外一个模块,则需要使用基本的api调用
使用RestTemplate调用pay模块
RestTemplate(看脑图):用于服务间的调用
注意:使用RestTemplate调用服务传输的是json数据,springboot并不能直接封装进对象中。需要在转发过去的方法中(即8001)使用@RequestBody让json数据封装进对象中,才能加入到数据库里。之前成功是因为url提交的是表单数据,非json数据
工程重构(解决entity重复)
Eureka服务注册与发现
当服务很多时,每个服务之间的依赖关系会比较复杂,管理起来更为复杂,需要服务管理服务间的依赖关系,实现负载均衡、服务调用等,实现服务注册与发现。
Eureka采用CS设计架构,server是服务器,也是注册中心,当eureka客户端连接到服务器,这样维护人员就可以在服务器监控各个服务的正常运行。
当服务器启动时,就会把自己的服务器信息以别名的方式注册在注册中心,消费者,服务提供者都以别名的方式获取该服务的通讯地址,实现本地的RPC调用思想:使用注册中心管理每个服务之间的依赖关系。
Eureka的两个组件:
客户端:在各个服务上,比如登录微服务,启动后会给服务器发送心跳,证明自己还存活
服务器:服务器端,用于接收心跳(类似工商管理局)
单机版Eureka
服务器创建
创建项目cloud_eureka_server_7001
引入pom依赖
Eureka新版本
写yml
主启动
加上@EnableEurekaServer,表示服务是server
测试
注册客户端8001进服务器成为服务提供者provider
其他服务注册进服务器
8001、80都注册进去
改pom依赖
写Yml
主启动
加上@EnableEurekaClient
服务模块重启
集群Eureka
集群原理:
服务注册:将服务信息注册进注册中心
服务发现:从注册中心获取服务信息
实质:key服务名,value调用地址
- 先启动注册中心,再启动服务,服务启动时会将信息以别名的方式注册进注册中心,消费者可以通过使用服务的别名在注册中心获取RPC远程调用地址,底层使用的是HttpClient实现的远程调用。在消费者获取服务地址之后,就会将其缓存在jvm内存中,没30秒更新一次
- RPC远程调用最核心的就是:高可用,一个瘫痪,全部升天。所以集群有多个组成,并且互相注册,相互守望,如:有3台服务器,则1注册12,2-13,3-12
构建新erueka服务器
- cloud_eureka_server_7002
- pom文件:粘贴7001的
- 配置文件
- 修改本地的host文件(看脑图,非yaml)
- 修改yml,进行相互注册(注意defautlZone),7001,7002都要修改
- 主启动类
- 启动测试
将服务模块注册进集群中
- 修改yml文件(defaultZone加一条即可,两个模块都要加)
- 启动模块,先启动服务器7001,7002,再启动8001,之后是80
- 测试
构建新的服务提供者8001集群
- 创建新模块,8002(cloud_pay_8002)
- pom文件,复制8001的
- yml文件修改端口(服务名称不用改,用一样的,为的是为消费者暴露一样的别名)
- 主启动类,复制8001的
- mapper,service,controller都复制一份
- 实现负载均衡
- controller的url不能写死,改成微服务名称,这样每次访问就会从eureka中拿地址,轮询
- 在注册bean的restTemplate方法上加上@LoadBalanced开启负载均衡
修改服务主机名和ip在eureka的web上显示
作用:修改在eureka显示的默认主机名,鼠标移到主机名会显示ip
- 修改yml文件
eureka服务发现:
对于注册进eureka里面的微服务,可以通过服务发现来获取该服务的信息
- 在controller自动注入DiscoveryClient
- 用方法获取各种信息(所有服务集群、服务集群中的服务名、IP、端口、URI)
- 主启动类加入注解:@EnableDiscoveryClient
- 重启测试
Eureka自我保护机制
某个微服务不可用时(客户端没有定时向服务器端发送心跳包),Eureka不会立刻将其清理,依旧会对该服务信息进行保存。
Eureka属于CAP中的AP分支(高可用)
- CAP:
- C:高一致性
- A:高可用
- P:高容错(因为分布式微服务架构都需要高容错,P必备)
- AP:Eureka
- CP:Zookeeper/Consul
- CAP:
如何禁止自我保护:
此时启动erueka和pay.此时如果直接关闭了pay,那么erueka会直接删除其注册信息
修改yml文件
- 注册中心server
- 关闭自我保护
- 设置接收心跳时间(测试用)
- 客户端client
- 设置发送心跳时间
- 设置服务端在收到最后一次心跳等待上限
- 测试
Zookeeper服务注册与发现
注册中心服务器Zookeeper
- 它是一个分布式协调工具,可以实现注册中心功能
- 关闭linux服务器防火墙,启动zookeeper服务器
服务提供者,客户端
创建新的pay服务模块
cloud_pay_8003,单独用于注册到zk中
pom依赖
yml配置文件(端口号,服务别名,zookeeper服务器ip+端口号)
主启动
controller
修改zk版本和jar冲突
解决:
修改pom文件,改为与我们zk版本匹配的jar包
启动,此时8003就注册到zk中了
此时服务就会作为临时节点存入zk中,信息作为流水号存在临时节点中。
当我们的服务一定时间内没有发送心跳
那么zk就会将这个服务的node删除了
这里是测试,就不写service与dao什么的了
服务消费者
- cloud_order_zk_80
- pom
- yml配置文件
- 主启动
- restTemplate
- controller
- 启动注册
集群zk的注册
- zookeeper教学中的集群搭建
- 服务提供者和消费者在配置文件中connect-string指定多个zk地址即可 “,”隔开
Consul服务注册与发现
它是一个服务发现、配置管理系统
作用:服务发现、健康检测、KV存储、可视化web界面
下载
- 需要下载一个安装包,是个exe文件
- 启动是一个命令行界面,需要输入consul agen-dev启动
- localhost:8500查看页面
创建服务提供者模块pay,8006
- cloud_consule_pay_8006
- pom依赖
- yml配置文件(需要写服务的名字)
- 主启动类
- controller
- 启动服务,观察是否注册进去
创建消费者模块order
cloud-consul-order-80
pom依赖
yml配置文件(需要写服务的名字)
主启动类
RestTemplate注册
配置类注册
controller
启动服务,测试是否注册进去,测试方法是否成功执行
三个注册中心的异同
Eureka:AP,以高可用为原则(如Eureka的自我保护机制:宕机依然保留数据)
Consul/Zookeeper:CP,以高一致性为原则,(如Zookeeper的临时节点:宕机直接踢出)
Ribbon负载均衡服务调用
概念
它是一个客户端,提供客户端(服务提供者)的软件负载均衡算法和服务调用。
作用:LB负载均衡(集中式负载均衡,进程内负载均衡)
- 负载均衡:将用户的请求平摊在多个服务上,实现高可用
Ribbon本地负载均衡和Nginx服务端负载均衡的区别
Nginx是服务器端的负载均衡,客户端将请求交给nginx,nginx实现转发请求
ribbon是客户端的负载均衡,会在注册中心拿到注册中心的信息服务列表加到缓存中,从而在本地实现RPC远程服务调用
工作机制
Ribbon就是负载均衡+RestTemplate,它就是一个软负载均衡的客户端组件,需要和其他客户端结合起来,如和eureka结合
- 在选择EurekaServer,它优先选择在同一区域的负载较少的server
- 根据用户的指定策略(轮询、随机、加权),在server里取到信息,进行调用
使用
引入jar包,但默认我们使用eureka的新版本时,它默认集成了ribbon,所以不需要引入
spring-cloud-starter-netflix-eureka-client集成了reibbon
也可以手动引入,放到order(消费者)模块中,因为只有order访问pay时需要负载均衡
RestTemplate类:
- getForObject(url,返回值.class):返回的是响应体数据转化成的对象,即json
- getForEntity(url,返回值.class):返回的是重要信息(响应头、体、状态码),可以通过返回对象的get方法获得这些信息
Ribbon常用负载均衡算法:
核心组件:IRule接口,Riboon使用该接口,根据特定算法从所有服务中,选择一个服务
Rule接口有7个实现类,每个实现类代表一个负载均衡算法:
替换:
选择需要修改的消费者模块
需要自定义配置类,不能放在主启动类所在的包及子包下
额外创建一个包:和启动器的父包是兄弟包
创建配置类,指定负载均衡算法
在主启动类上加一个注解@RibbonClient
- name:要访问服务的名字
- configuration:自定义配置类的class文件
自定义负载均衡算法:
轮询算法原理
rest接口第几次请求 % 服务器集群总数量(集群中的机器数) = 实际调用服务器的下标,重启服务器后,计数从1开始
自定义负载均衡算法(跳)
OpenFeign 服务接口调用
它是webService客户端(消费者端),使用方法是:定义一个服务接口,然后在上面添加注解,写上需要调用的方法,使用时调用这个接口中的方法,即可完成微服务之间的调用,实现接口间的远程调用,不再用Ribbon和restTemplate一样远程调用
就是A要调用B,Feign就是在A中创建一个一模一样的B对外提供服务的的接口,我们调用这个接口,就可以服务到B
使用
新建一个order项目,用于feign测试
cloud_order_feign-80
pom文件(引入openFeign的jar包)
yml配置文件(因为是客户端,不注册进注册中心也可以)
主启动类(@EnableFeignClients,启动feign)
需要调用的其他的服务的接口(这里本应该是8001 Service接口中的方法,但controller也是调用的service方法,所以是一样的)
controller(自动注入接口,调用接口的方法,实现远程调用)
测试
启动7001,7002
启动8001,8002
启动当前80模块
Feign默认使用ribbon实现负载均衡
测试方法是否成功调用
OpenFeign超时机制:
有时我们业务逻辑处理需要久一点,不能很快的让消费者远程调用到,我们需要改一下超时时间
OpenFeign默认等待时间是1秒,超过1秒,直接报错
设置超时时间,修改yml配置文件
因为OpenFeign的底层是ribbon进行负载均衡,所以它的超时时间是由ribbon控制
OpenFeign日志:
它提供了日志打印功能,可以通过配置日志级别,从而了解http请求的细节,就是对feign接口的调用情况的监控和输出
OpenFeign的日志级别有:
使用
实现在配置类中添加OpenFeign的日志类(指定http日志级别)
修改yml配置文件(指定命令行日志级别、哪个接口实现日志监控)
启动服务测试
Hystrix服务降级
概念:
- 服务雪崩:当多个微服务之间互相调用时,a-b-c-d……,这就是所谓的“扇出”,当某个微服务不管用,对a就会占用越来越多的资源,进而引起系统崩溃,雪崩
- 它是一个用于处理分布式的延迟和容错,当出现问题时不会导致级联故障,而是返回一个符合预期的备选响应,提高分布式弹性
- 服务降级:当服务繁忙时,不能一直等待,会返回客户端一个备选方案
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池打满
- 宕机
- 服务熔断:当服务出现问题,出现故障,先关闭对此服务的访问,防止级联故障,再调用服务降级,最后恢复
- 服务限流:当并发量增大,限制访问的请求数量
使用服务降级
创建服务提供者模块(测试服务降级用)
- cloud-hystrix-pay-8007
- pom
- yml
- 主启动(@EnableEurekaClient)
- service(正常访问方法、睡3秒方法)
- controller
- 压力测试JMeter(还没有加入hystrix服务降级)
创建消费者模块(测试服务降级用)
cloud-hystrix-order-80
pom
yml
主启动(@EnableFeignClients)
远程调用接口(@FeignClient(value=”服务名”))
controller(调用接口)
压力测试
出现问题:2W并发之后,消费者的访问速度变慢
解决方案
- 超时:不再等待
- 出错(宕机或运行出错):兜底
配置服务降级
==一般服务降级,都是放在客户端==
==热部署对@HystrixCommand注解不太敏感,建议修改完重启服务==
修改服务提供者payment模块
创建一个用于兜底的方法
为service的指定方法(会延迟的方法)添加@HystrixCommand注解
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000") }) //3秒钟以内就是正常的业务逻辑 //设定超时时间
主启动类上,添加激活hystrix的注解@EnableCircuitBreaker
测试:
- 设置方法时间>设定时间
- 设置异常
修改消费者order模块
修改yml配置文件
主启动类添加注解,启用hystrix,@EnableHystrix
修改controller,添加要测试的降级方法和相关注解
测试
- 设置方法时间>设定时间
- 设置异常
- 这里pay模块和order模块都开启了服务降级,但服务提供者3秒,消费者设置为1.5秒,一定会被降级
出现的问题
- 每个业务方法都写了一个降级方法,重复代码多
- 降级方法与业务方法写在了一块,耦合度高
解决1:
配置一个全局的降级方法,所有方法都可以走这个降级方法,至于某些特殊创建,如上述的一样再单独创建方法
消费者模块创建一个全局方法
在类上使用注解指定其为全局降级方法(默认降级方法)
@DefaultProperties(defaultFallback=”定义的全局方法”)
在方法上加注解@HystrixCommand,不用指定降级方法,使用默认全局
测试
解决2:
修改消费者模块
创建一个类实现远程接口,在实现类中统一处理异常
修改配置文件:添加:
接口中注解增加fallback属性,让PayService的实现类生效:
它的运行逻辑是:
当请求过来,首先还是通过Feign远程调用pay模块对应的方法,但是如果pay模块报错(宕机,无法调用),调用失败,那么就会调用PayMentFalbackService类的当前同名的方法,作为降级方法
测试:
启动正常访问后,关闭服务提供者,消费者再次访问,转向fallback方法
这样虽然解决了代码耦合度问题,但是又出现了过多重复代码的问题,每个方法都有一个降级方法
使用服务熔断
当达到最大访问次数时,出现问题概率大,拒绝访问,拉闸,调用降级的方法
机制:当某个服务不可用或响应时间太长,熔断该微服务,快速返回错误响应信息,当检测到恢复正常,再恢复调用链路
如:当失败调用到一定阈值,5秒内20次调用失败,就会启动熔断机制,熔断机制是注解@HystrixCommand
修改服务提供者模块
修改Payservice接口,添加服务熔断相关的方法:
10秒之内(窗口,会移动),如果并发==超过==10个,或者10个并发中,失败了6个,就开启熔断器
IdUtil是Hutool包下的类,这个Hutool就是整合了所有的常用方法,比如UUID,反射,IO流等工具方法什么的都整合了
断路器的打开和关闭,是按照一下5步决定的
- 并发此时是否达到我们指定的阈值(默认10秒20次请求),到了阈值才有资格熔断
- 错误百分比,比如我们配置了60%(默认10秒50%请求),那么如果并发请求中,10次有6次是失败的,就开启断路器
- 上面的两个条件符合,断路器改变状态为open(开启),拉闸
- 这个服务的断路器开启,所有请求无法访问
- 熔断之后,开启休眠时间窗(默认5秒),此时只会执行降级逻辑,休眠时间窗结束之后,尝试让一些请求通过(半开状态half Open),如果请求还是失败,证明断路器还是开启状态,服务没有恢复,如果请求成功了,证明服务已经恢复,断路器状态变为close关闭状态,恢复
其他参数:见脑图
服务限流(sentinel会详说)
Hystrix服务监控:
一个图形化界面的实时监控平台
使用:
创建监控平台模块(需要创建模块,比较麻烦,后面sentinel不需要)
cloud_hystrixdashboard_9001
pom文件(导入依赖dashboard)
配置文件
server: port:9001
主启动类@EnableHystrixDashboard
修改需要监控的模块(8001,8002)
增加pom依赖(以前都增加有)
actuator,这个是springboot的监控组件
启动
访问: localhost:9001/hystrix
注意,此时仅仅是可以访问HystrixDashboard,并不代表已经监控了8001,8002
如果要监控,还需要配置:(8001为例)
8001主启动类添加
其他8002,8003都是一样的
启动服务7001,8001,9001
然后在web界面,指定9001要监控8001:
GateWay服务网关
zuul停更了
概念
GateWay是Spring生态系统上构建的API网关服务,替代了zuul,基于WebFlux框架,而WebFlux框架底层使用了高性能的Reactor模式通信框架Netty。
gateway之所以性能好,因为底层使用WebFlux,而webFlux底层使用netty通信(NIO)
特性:动态路由(能匹配任何请求),可对路由指定断言、过滤器,集成断路器、SpringCloud服务发现,请求限流
GateWay与zuul的区别:
- Zuul1是基于Sevlet2.5使用阻塞架构,每次IO操作都从工作线程中选一个执行,请求线程会阻塞到工作线程完成,性能较差
- gateWay使用非阻塞API,建立在spring5, project Reactor和spring boot2之上
路由:构建网关的基本模块,它由ID,目标URI,一系列断言和过滤器组成,通过断言为true则匹配该路由
即根据某些规则,将请求转发给指定服务
断言:开发人员可以通过http请求中的内容,与断言相匹配则可以进行路由
过滤:在路由的前后对请求进行修改
工作流程:
就是路由转发+执行过滤器链
使用
新建网关模块GateWay
cloud_gateway_9527
pom(引入网关依赖gateWay)
yml配置文件
主启动
根据服务提供者模块,设置路由网关
我们不想暴露8001端口,所以在8001上套了一层9527
修改网关的yml配置文件
意思就是:
当访问localhost:9527/payment/get/1时,
路由到localhost:8001/payment/get/1
以上是配置文件配置,也可以用硬编码配置,比较麻烦所以还是选择配置文件的方式
动态路由
默认情况gateWay会根据服务中心的服务列表,以微服务名为路径创建动态路由进行转发
使用:
需要1个服务器、两个微服务提供者
修改网关yml文件(开启路由功能,修改路由地址)
启动微服务,进行测试
Pridicate断言
相当于网关的规则,满足了这些规则,找到对应的路由
其中predicates:path=/payment/get/**
就是一个断言
这个断言表示,如果外部访问路径是指定路径,就路由到指定微服务上
- After:只有在指定时间后,才可以路由到指定微服务
before:在指定时间前访问
between:需要指定两个时间,在这之间的时间可以访问
cookie:只有包含某些指定cookie(key,value)的请求才可以路由
- 需要由两个参数,key名,value值
Header:只有包含指定请求头的请求,才可以路由
- 需要两个参数,header名,正则表达式,如:x-Request-id,\d+ 正整数
host:只有指定主机的才可以访问,如:Host=**.shangguigu.com
- 测试:curl http://localhost:9527/paymentinfo -H “host:www.shangguigu.com"
- curl是命令行发送请求的方式,postman可视化图形界面的底层就是使用这个
method:只有指定请求才可以路由,比如get请求…
- Method=GET
path:只有访问指定路径,才进行路由
Query:必须带有请求参数才可以访问,
- 需要两个参数,参数名,正则表达式,如:Query=username,\d+
- localhost:9527?username=123
Filter过滤器
种类:
GatewayFilter:单一的过滤器,与断言类似(怎么加看官网)
GlobalFilter:全局过滤器
自定义过滤器(全局,常用)
自己创建一个过滤器类,实现两个接口 GlobalFilter,Ordered
必须加入容器,实现接口
意思:必须带有参数uname的,如果没有就过滤掉
启动测试
Config服务配置
问题:每个微服务都需要一个配置文件,并且,如果有几个微服务都需要连接数据库,那么就需要配4次数据库相关配置,并且当数据库发生改动,那么需要同时修改4个微服务的配置文件才可以
概念:为微服务架构中的微服务提供集中化外部配置支持,配置服务器为各个不同的微服务应用的所有环境提供了中心化的外部配置
分为服务端和客户端。
服务端就是分布式配置中心,是一个独立的微服务应用,默认采用git存储配置信息
客户端是启动时在配置中心获取加载配置信息
作用:
- 集中管理配置
- 动态化配置更新
- 不同环境不同配置
- 将配置信息以Rest接口的形式暴露(通过controller获取配置信息)
使用配置中心(服务端)
使用github作为配置中心的仓库
初始化git环境:
新建config服务端模块
- pom(引入config-server依赖)
- yml配置文件
主启动(@EnableConfigServer开启配置中心服务端)
修改本地hosts
为了输入url时localhost直接等于config-3344.com
测试
启动3344 (要先启动eureka)
读取配置文件的规则
使用配置中心(客户端)
创建config客户端项目
cloud-config-client-3355
pom(引入config依赖)
yml配置文件
注意:这个配置文件就不是application.yml,bootstrap.yml
这个配置文件的作用是,先到配置中心加载配置,然后加载到application.yml中
因为bootstrap具有高优先级,保证不会被本地配置覆盖,负责从外部源加载配置文件,两个上下文实现共享
主启动类(@EnableEurekaClient,这里没有配置客户端的注解)
controller
这就是上面提到的rest风格将配置对外暴露
如果客户端运行正常,就会读取到github上配置文件的属性
config.info
的值测试:
启动3344,3355
访问3355的 /configInfo
问题:如果配置修改,3344仍然能获得最新配置,但是3355却不能动态刷新
实现动态刷新
修改3355,增加pom依赖actuator监控
修改配置文件,增加一个配置
修改controller(@RefreshScope)
需要外部发送post请求通知3355
curl -X POST "http://localhost:3355/actuator/refresh"
刷新测试是否更换为最新配置
问题:如果由多个客户端,每个都需要通知会很麻烦,所以需要广播,这就引出了Bus消息总线
SpringCloud Bus 消息总线
它是将分布式系统的节点与轻量级消息系统链接起来的框架
目前支持RabbitMQ和Kafka
设计思想:
消息发给客户端,它能让其在其它客户端间传播
也可以将消息发给config配置中心,一次性广播给其他客户端
- 发给配置中心更合理,第一种破坏了业务模块职责的单一性,破坏了服务个节点的对等性
概念:在微服务架构中,通常会使用消息代理构建一个共有的主题,让所有微服务实例都订阅,该主题产生的消息也会被监听和消费,所以它为消息总线。
原理:configClient实例都监听同一个主题,当一个服务刷新数据,它会把消息放在主题中,其他实例就能得到通知,然后更新自身的配置
使用
配置RabbitMQ环境
为演示多个通知,多创建一个配置中心客户端
==复制3355即可,创建为3366==
全部复制3355的即可
使用Bus实现全局广播
- 配置3344(配置中心服务端)
修改yml配置文件
添加pom
springboot的监控组件actuator,和消息总线整合rabbitmq:bus-amqp
- 修改3355(配置中心的客户端1)
增加pom
springboot的监控组件actuator,和消息总线整合rabbitmq:bus-amqp
修改yml文件(注意客户端的配置文件都交bootstrap)
- 修改3366(配置中心的客户端2)
- 和上面的步骤一样
测试:
启动7001(注册中心),3344(配置中心服务器),3355,3366(配置中心客户端)
修改github的配置文件
因为我们采用的是发送通知给服务器,再广播给客户端的方式,所以要给3344发消息
curl -X POST "http://localhost:3344/actuator/bus-refresh"
原理:所有客户端都监听了一个rabbitMq的topic,我们将信息放入这个topic,所有客户端都可以送到,从而实时更新
配置定点通知
只通知部分服务,其他服务不受影响
http://localhost:3344/actuator/bus-refresh/微服务名:端口号
Spring Cloud Stream消息驱动
问题:一个项目中可能有后端–>大数据,如果后端用的是RabbitMq,大数据用的是kafka,就会导致经常切换,影响使用,学习负担重
Spring Cloud Stream就类似jpa,屏蔽底层消息中间件的差异(比如kafka有topic,rabbitMq有exchange),降低切换成本,统一消息编程模型,程序员主要操作Spring Cloud Stream即可,不需要管底层是kafka还是rabbitMq
应用程序通过**inputs(生产者),outputs(消费者)**与stream中的binder对象交互,我们主要操作binder对象与底层mq交互即可。
如何屏蔽底层差异?
由于不同的消息中间件实现细节有较大差异,定义的绑定器作为中间层,实现了应用程序与消息中间件之间的隔离,通过程序暴露统一的channel通道,让应用程序不再需要与消息中间件交互
业务流程
source用于获取数据(要发送到mq的数据)
channel类似SpringCloudStream中的中间件,用于存放source接收到的数据,或者是存放binder拉取的数据
常用api和注解
使用SpringCloudStream
创建3个模块,1个生产者,2个消费者
生产者创建
pom(stream-rabbit)
yml配置文件
设置stream中rabbitmq配置,之后绑定这个配置
主启动
service和实现类
这里,就会调用send方法,将消息发送给channel,
然后channel通过上述的配置文件信息将消费发送给binder,然后发送到rabbitmq中
controller
测试:
启动rabbitmq
启动7001,8801
打开rabbitmq的web界面,看到创建出来的管道Exchange(在kafka里叫做主题topic),就是我们配置文件中配置的exchange
访问8801的/sendMessage
看到消息已经发送到mq中
消费者创建
pom文件(stream-rabbit)
yml配置文件
主启动类
业务类(消费数据)
生产者发送消息时,使用send方法发送,send方法发送的是一个个Message,里面封装了数据
所以接收时也要使用Message
来接收 测试
启动7001,8801,8802
使用生产者生产消息,消费者已经接收到消息
创建消费者2
创建8003,和8002一摸一样,只是端口号不同,为了演示重复消费等问题
问题:重复消费,当生产者生产一条消息,8002/8003都消费到了同一条数据
原因:没有分组,不同组是可以重复消费的,同一个组中的多个消费者是竞争关系,能保证消息只会被其中一个消费者消费一次
自定义分组(解决重复消费)
修改8802,8803的配置文件
将8802,8803都分到A组
重启这两个消费者,生产消息,会发现没有重复消费
持久化问题:
当服务(消费者)挂掉,怎么消费生产者生产还没有消费的数据?
结论:只要加上group分组,当下次重启之后,就会自动获取未消费的消息继续消费
Spring Cloud Sleuth链路追踪
问题:在微服务框架中,客户端发送的请求在后端系统可能会经过很多服务节点,我们可以通过链路追踪查看都调用了哪些服务
sleuth就是用于追踪每个请求的整体链路
原理:
Trace ID:一条链路的唯一标识
Span ID:一次请求的唯一标识
Parent ID:上一次请求的唯一标识
在经过一次请求时,sleuth会记录下各种ID,最后连接起来
使用
安装zipkin(图形化展示链路)
- 运行jar包
java -jar xxxx.jar
- 然后就可以访问web界面, 默认zipkin监听的端口是9411
localhost:9411/zipkin/
- 运行jar包
使用sleuth
修改8001的pom(zipkin)
这个包虽然叫zipkin但是,里面包含了zpikin与sleuth
修改yml配置文件
修改80的pom(一样)
修改yml配置文件(一样)
测试
启动7001,8001,80
localhost:9411/zipkin/
SpringCloud Alibaba
支持的功能
Nacos注册中心+配置中心
Nacos=Eureka+config+bus
安装
需要java8 和 Maven
到github上下载安装包
解压安装包
启动Nacos(注册中心服务器)
在bin下,进入cod
./startup.cmd
访问Nacos
Nacos默认监听8848
localhost:8848/nacos
账号密码:默认都是nacos
使用注册中心
创建服务提供者
注册中心服务器已经不需要我们手写,Nacos启动就是服务器
创建服务提供者模块cloudalibaba-pay-9001
改pom
- 父工程alibaba-dependencies
- alibaba-nacos-discovery
yml配置文件
主启动类@EnableDiscoveryClient
controller
测试
启动nacos,9001服务提供者
查看web界面,看看9001是否注册成功
创建服务提供者2(测试负载均衡)
直接复制上面的9001即可
创建消费者模块
cloudalibaba-order-83
Pom(alibaba-nacos-discovery)
因为内部集成了Ribbon,所以自带负载均衡功能
yml配置文件
主启动类@EnableDiscoveryClient
配置类
因为Naocs要使用Ribbon进行负载均衡,那么就需要使用RestTemplate
@LoadBalanced开启负载均衡
controller
测试
启动83消费者,访问9001,9002,负载均衡
Nacos与其他服务注册的对比
Nacos它既可以支持CP,也可以支持AP,可以切换
一般来说,不需要存储服务级别的,用AP,高可用而削弱了一致性,临时实例
需要服务级别编辑或存储配置信息,CP是必须的,持久化实例
用于切换模式
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
使用配置中心
在项目初始化后,要保证从配置中心进行配置拉取,拉取配置之后,才能保证下项目正常启用
创建配置中心的客户端模块
cloudalibaba-Nacos-config-client-3377
pom
- alibaba-nacos-config
- alibaba-nacos-discovery
yaml配置文件
这里需要配置两个配置文件,application.yml和bootstrap.yml(通用配置)
主要是为了可以与spring clodu config无缝迁移
主启动类@EnableDiscoveryClient
controller
@RefreshScope支持动态刷新
在Nacos添加配置信息
Nacos的配置规则:
就是我们在客户端如何指定读取配置文件,配置文件的命名的规则
prefix:
默认就是当前服务的服务名称,也可以通过spring.cloud.necos.config.prefix配置
spring.profile.active:
就是我们在application.yml中指定的,当前是开发环境还是测试等环境,这个可以不配置,如果不配置,那么前面的 - 也会没有
file-extension:
就是当前文件的格式(后缀),目前只支持yml/yaml和properties
在web页面上创建配置文件
Data ID:就是按照命名规则配置的名字
测试
重启配置文件模块客户端
访问controller,
localhost:3377/config/info
拿到配置文件的值
这时我们已经自动开启了动态刷新,只要修改配置文件,客户端就会立即刷新,因为Nacos支持Bus总线,会自动发送命令更新所有客户端
Nacos配置中心之分类配置:
问题:实际开发中会为一个系统准备多个开发环境,也会有多个配置文件,为了区分读取配置文件,可以将配置进行分类读取
配置文件可以归为:Namespace(命名空间)+Group(组)+Data ID(开发环境)
默认情况:Namespace=public,Group=DEFAULT_GROUP,环境=DEFAULT
按照不同的DataId划分
在配置中心新建两个配置文件,DataID分为两个,dev,test
通过application.yml配置文件,实现多环境读取
此时,改为dev,就会读取dev的配置文件,改为test,就会读取test的配置文件
重启服务
按照不同的GroupID划分
直接在配置中心新建配置文件时指定组
在客户端配置,使用指定组的配置文件
bootstrap+application
重启服务
按照不同的namespace划分
创建新命名空间,在新命名空间中创建配置文件
客户端配置使用不同名称空间:
重启服务
Nacos集群和持久化配置:
Nacos默认有自带嵌入式数据库,derby,但是如果做集群模式的话,就不能使用自己的数据库
不然每个节点一个数据库,那么数据就不统一了,需要使用外部的mysql
Nacos采用了集中式存储方式来支持集群化部署,目前只支持MySQL的存储
切换mysql数据库:
将默认的derby数据库切换为mysql数据库,要求数据库(5.6+)
找到Nacos安装目录下的sql脚本,在Mysql中使用该脚本nacos-mysql.sql
修改Nacos安装目录下的安排application.properties,添加:
spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root #可变 db.password=123456 #可变
此时可以重启nacos,那么就会改为使用我们自己的mysql
数据消失,证明已经切换成功
Linux上配置Nacos集群+Mysql数据库
需要1个nginx,3个Nacos,1个Mysql
下载安装Nacos的Linux版安装包
进入安装目录,现在执行自带的sql文件(配置Mysql)
进入mysql,执行sql文件
修改配置文件,切换为我们的mysql(配置Nacos)
就是上面windos版要修改的几个属性(application.properties)
修改cluster.conf,指定哪几个节点是Nacos集群
这里使用3333,4444,5555作为三个Nacos节点监听的端口
设置端口启动
既然要在一个节点上启动不同Nacos实例,就要修改startup.sh,使其根据不同端口启动不同Nacos实例
可以看到,这个脚本就是通过jvm启动nacos
所以我们最后修改的结果就是,nohup java -Dserver.port=3344
启动Nacos集群
./startup.sh -p 3333
./startup.sh -p 4444
./startup.sh -p 5555
配置Nginx:
修改nginx配置文件
按照指定启动
测试
访问,自己虚拟机ip:1111
如果进入了nacos的Web界面,证明成功
将微服务注册进Nacos集群中
进去web界面,看是否注册成功
Sentinel服务熔断、限流:
Hystrix与Sentinel对比
使用Sentinel
下载Sentinel的jar包
运行sentinel
java -jar
注意:sentinel的默认端口为8080
访问sentinel
localhost:8080
微服务整合sentinel:
启动sentinel
新建模块8401,主要用于配置sentinel
pom
- alibaba-nacos-discovery(主要)
- sentinel-datasource-nacos
- alibaba-sentinel(主要)
- openfeign
yml配置文件
主启动@EnableDiscoveryClient
controller
启动测试
注意:
sentinel是懒加载,需要我们执行一次访问,才会有信息
访问localhost/8401/testA,刷新控制台,即可看到
sentinel的流控规则
流控模式
QPS直接快速失败(每秒请求数量达到阈值,直接失败,都不给分配线程)请求进不去服务内
线程数(当线程数达到阈值,直接失败,即都放进来,线程占满就不失败)进去服务内,超过阈值再失败
关联(服务A关联服务B,当A达到阈值,B也会失败)
==应用场景: 比如支付接口达到阈值,就要限流下订单的接口,防止一直有订单==
链路(多个请求都来自同一个服务)
流控效果
预热Warm up:冷启动方式,当系统长期处于低水位(并发量小)情况下,流量突然增加,拉升到高水位,冷启动可以再一定时间内主键增加到阈值的上限。
起始阈值:阈值/冷加载因子(默认为3),
预热时长:在几秒后达到阈值
==应用场景:秒杀==
排队等待:让请求匀速通过,阈值类型必须是qps
如每秒通过1次请求,超过阈值也不会失败,进行排队等待,如果等待时长超过超时时间,再失败
==应用场景:一会并发量高,一会并发量低==
降级规则
熔断降级:在调用链路中某个资源不稳定,就对其进行限制,让请求快速失败,避免级联错误
Sentinel断路器没有半开状态
- RT:秒级平均响应时间,表示请求处理要在规定时间内完成
必须满足以下条件才能触发断路器:
1秒内超过5个请求(默认的)
响应时间>平均响应时间(就是请求处理时间超过了规定的时间)
- 异常比例:每秒异常总数的比例超过异常比例阈值
1秒内超过5个请求(默认的)
==如果没有触发熔断条件,就会正常抛出异常==
- 异常数:1分钟内异常数大于阈值
1秒内超过5个请求(默认的)
时间窗口要超过60秒
热点规则
热点即经常访问的数据,很多时候某个热点数据访问频次很高,需要我们进行限制
如:商品ID作为参数,我们就要根据商品的ID进行限制
如何自定义降级方法,而不是默认的抛出异常
使用**@SentinelResource**直接实现降级方法,它等同Hystrix的@HystrixCommand
value:随意取,即热点的资源名
blockHandler:指定的降级方法,跳转自定义的页面
注意:当没有设置blockHandler降级方法时,会向前台抛异常
定义热点规则
参数索引从0开始,0就是代表第一个参数
这个意思就是:当传递的参数每秒频次超过1,就会调用降级方法。但是我们访问其他参数没有问题
设置参数例外项
专门设定某个参数的值为几的,有专门的限流阈值处理
如:参数p1的值为5,限流阈值为200,即p1其他值受单机阈值1的影响,而5受200的影响
注意:如果请求中有异常,就不会走降级的方法,它只会抛出异常,因为这个方法只是设置热点规则,没有对异常处理的机制。
系统规则
系统自适应限流:从整体维度对应用入口进行限流
就是大门口,根据整个系统的情况来限流
@SentinelResource注解:
用于配置降级
搭建环境
为8401添加依赖
添加我们自己的common包的依赖
额外创建一个controller类
blockHandler如果不设置,就弹出系统默认的降级页面
设置限流
==注意,我们这里配置规则,资源名指定的是@SentinelResource注解value的值,也可以时访问路径==
测试
问题:当8401服务被关闭后,定义的限流规则就没有了,因为是临时的
可以看到,上面配置的降级方法,又出现Hystrix遇到的问题了
降级方法与业务方法耦合
每个业务方法都需要对应一个降级方法
自定义限流处理逻辑
单独创建一个类,用于处理限流兜底
在controller中,指定使用自定义类中的方法作为降级方法
测试
服务熔断
一般服务熔断是用在消费者模块,对其进行降级、限流等操作
配置环境
启动nacos和sentinel
新建两个服务提供者模块 9003和9004(配置一样)
cloudalibaba-provider-payment9003/9004
pom
yml
主启动@EnableDiscoveryClient
controller
启动9003,9004
新建消费者模块
cloudalibaba-consumer-payment-84
pom
yml配置文件
主启动类@EnableDiscoveryClient
RestTemplate配置类
controller
为业务方法(消费者)增加fallback来指定降级方法(处理java异常)
重启order(因为热部署对java代码敏感,对于注解不是很敏感,建议重启)
==所以,fallback是用于管理异常的,当业务方法发生异常,可以降级到指定方法==
为业务方法添加blockHandler,看看是什么效果
可以看到.,直接报错了,并没有降级
也就是说,==blockHandler只负责sentienl定义的规则降级==
fallback和blockHandler都配置
测试:
设置限流,会发现当两个都同时生效时,==blockhandler优先生效==
@SentinelResource还有一个属性,exceptionsToIgnore
忽略异常,指定一个异常,如果出现了该异常不会走降级方法,直接向用户抛出异常
sentinel整合ribbon+openFeign+fallback
修改消费者84模块,使其支持feign
pom(openfeign)
yml配置文件
主配置类(@EnableFeignClients)
创建远程调用服务提供者模块的接口
测试:
启动9003服务提供者,84消费者
当关闭9003时,如果没有排除异常,而是被调用了降级方法,则为成功
sentinel持久化规则
默认规则是临时存储的,重启sentinel就会消失
将限流配置规则持久化进Nacos保存,只要刷新服务的某个rest地址,sentinel控制台的留空规则就能看到,只要Nacos里面的配置不删除,针对服务上的sentinel上的流控规则就持续有效
这里以8401为例修改
pom(sentinel-datasource-nacos)
修改yml配置文件
实际上就是指定,我们的规则要保证在哪个名称空间的哪个分组下,下次就在这个地方取出
这里没有指定namespace, 但是是可以指定的
注意,这里的dataid要与8401的服务名一致
server-addr:nacos的地址
dataId:服务名
groupId:组名
data-type:数据类型
rule-type:flow(流控配置)
在nacos中创建一个配置文件
dataId就是上面配置文件中指定的,这个就是持久化的配置
==json中,这些属性的含义:==
启动8401,访问rest请求
可以看到sentinel已经读取到了规则
关闭8401后,规则就消失了,再次启动就会再读取到规则
Seata分布式事务
是一个分布式事务的解决方案
一次业务操作需要多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
概念
seata安装
下载安装seata的安装包,解压
修改file.conf(记得先备份)
mysql建库建表
- 上面指定了数据库名为seata,创建一个数据库名为seata
- 建表,在seata的安装目录下有一个db_store.sql,运行即可
修改配置文件registry.conf
配置seata作为微服务,指定注册中心
启动
先启动nacos
再启动seata-server(运行安装目录下的,seata-server.bat)
搭建业务环境
业务说明:
我们需要创建三个服务:订单服务,库存服务,账户服务
场景:
- 当用户下单时,会再订单服务中创建一个订单,
- 然后通过远程调用库存服务扣减商品库存。
- 通过远程调用账户服务扣减账户余额,
- 最后在订单服务中修改订单状态为已完成
创建三个数据库
- seata_order:存储订单数据库
- seata_storage:存储库存数据库
- seata_account:存储账户信息数据库
创建对应的表
- t_order
- t_storage
- t_account
创建回滚日志表,方便查看
注意==每个库都要执行一次==这个sql,生成回滚日志表
每个业务都创建一个微服务,也就是要有三个微服务,订单,库存,账户
==订单==,seta-order-2001
pom
- alibaba-nacos-discovery
- alibaba-seata(排除io.seata)
- seata-all,用自己的Seata
- openfeign
yml配置文件
server: port: 2001 spring: application: name: seata-order-service cloud: alibaba: seata: # 自定义事务组名称需要与seata-server中的对应,我们之前在seata的配置文件中配置的名字 tx-service-group: fsp_tx_group nacos: discovery: server-addr: 127.0.0.1:8848 datasource: # 当前数据源操作类型 type: com.alibaba.druid.pool.DruidDataSource # mysql驱动类 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath*:mapper/*.xml
创建配置文件file.conf
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup # 事务组名称 vgroup_mapping.fsp_tx_group = "default" #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db #mode = "file" mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "root" password = "root" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }
创建registry.conf
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { #serverAddr = "localhost" serverAddr = "localhost:8848" namespace = "" cluster = "default" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "localhost" namespace = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
实际上,就是要将seata中的我们之前修改的两个配置文件复制到这个项目下
主启动
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消数据源的自动创建 @EnableDiscoveryClient @EnableFeignClients//使用openFeign的远程调用 public class SeataOrderMain2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMain2001.class,args); } }
entity类(也叫domain类)
//使用了lombok @Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code;//状态码 private String message;//消息 private T data;//json数据 public CommonResult(Integer code, String message) { this(code, message, null); } }
@Data @AllArgsConstructor @NoArgsConstructor public class Order { private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; private Integer status;//订单状态,0为创建中,1为已完结 }
service层
public interface OrderService { /** * 创建订单 */ void create(Order order); }
@FeignClient(value = "seata-storage-service")//这里是远程调用的接口,真正方法在库存模块写,这个接口用来调用 public interface StorageService { /** * 减库存 * 这里是远程调用 */ //传入产品id,商品的购买数量 @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
@FeignClient(value = "seata-account-service")//这里是远程调用的接口,真正方法在账户模块写,这个接口用来调用 public interface AccountService { /** * 减余额 */ //传入用户id,用户消费的金额 @PostMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
ServiceImpl
@Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private AccountService accountService; @Resource private StorageService storageService; /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 * 简单说: * 下订单->减库存->减余额->改状态 * GlobalTransactional seata开启分布式事务,异常时回滚,name保证唯一即可 * @param order 订单对象 */ @Override ///@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) public void create(Order order) { // 1 新建订单 log.info("----->开始新建订单"); orderDao.create(order); // 2 扣减库存 log.info("----->订单微服务开始调用库存,做扣减Count"); storageService.decrease(order.getProductId(), order.getCount()); log.info("----->订单微服务开始调用库存,做扣减End"); // 3 扣减账户 log.info("----->订单微服务开始调用账户,做扣减Money"); accountService.decrease(order.getUserId(), order.getMoney()); log.info("----->订单微服务开始调用账户,做扣减End"); // 4 修改订单状态,从0到1,1代表已完成 log.info("----->修改订单状态开始"); orderDao.update(order.getUserId(), 0); log.info("----->下订单结束了,O(∩_∩)O哈哈~"); } }
dao层,也就是接口
这里使用的是mapper映射,xml的方式
@Mapper public interface OrderDao { /** * 1 新建订单 */ int create(Order order); /** * 2 修改订单状态,从0改为1 */ //传入用户id,状态为0 int update(@Param("userId") Long userId, @Param("status") Integer status); }
==在resource下创建mapper文件夹,编写mapper.xml==
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--映射dao--> <mapper namespace="com.eiletxie.springcloud.alibaba.dao.OrderDao"> <!--映射sql表和实例的属性,如果相同就不需要写--> <resultMap id="BaseResultMap" type="com.eiletxie.springcloud.alibaba.domain.Order"> <id column="id" property="id" jdbcType="BIGINT"></id> <result column="user_id" property="userId" jdbcType="BIGINT"></result> <result column="product_id" property="productId" jdbcType="BIGINT"></result> <result column="count" property="count" jdbcType="INTEGER"></result> <result column="money" property="money" jdbcType="DECIMAL"></result> <result column="status" property="status" jdbcType="INTEGER"></result> </resultMap> <insert id="create" parameterType="com.eiletxie.springcloud.alibaba.domain.Order" useGeneratedKeys="true" keyProperty="id"> insert into t_order(user_id,product_id,count,money,status) values (#{userId},#{productId},#{count},#{money},0); </insert> <!--找到用户,并且他的状态为0,将状态修改为1--> <update id="update"> update t_order set status =1 where user_id =#{userId} and status=#{status}; </update> </mapper>
controller层
@RestController public class OrderController { @Resource private OrderService orderService; /** * 创建订单 */ @GetMapping("/order/create") //返回通用数据给前台 public CommonResult create(Order order) { orderService.create(order); return new CommonResult(200, "订单创建成功"); } }
config配置类
@Configuration @MapperScan({"com.eiletxie.springcloud.alibaba.dao"})//指定我们的接口的位置 public class MyBatisConfig { }
/** * 使用Seata对数据源进行代理 */ @Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSourceProxy); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); bean.setMapperLocations(resolver.getResources(mapperLocations)); return bean.getObject(); } }
==库存==,seta-storage-2002
==看脑图==
- pom
- 配置文件
- 主启动类
- file.conf
- registry.conf
- domain
- service层
- dao层
- controller层
- config
==账号==,seta-account-2003
==看脑图==
- pom
- 配置文件
- 主启动类
- file.conf
- registry.conf
- domain
- service层
- dao层
- controller层
- config
seata原理:
第一阶段:
第二阶段:
提交
一阶段已经提交,只需要保存即可
回滚
因为一阶段已经保存过了,二阶段回归需要反向sql进行补偿