由于是模块化项目,那么父工程不需要实际的代码逻辑,因此无需创建src,那么可以有几种方式创建,例如:
下面使用方式1来创建:
这里我们先不管SpringBoot的版本号,因为也没几个可选的合适的,创建完之后再改到合适的版本。默认3.0.3创建出来的java版本是17。不选择依赖
可以看到创建出来的父工程只有一个pom文件没有src目录
分别创建test1和test2模块。File–>new–>Module
依赖就勾一个Spring Web就行,剩下的慢慢补
创建之后的结构如下。展开模块 都带src文件夹和各自的pom文件
在父pom中增加
在父pom中增加
修改module pom中的parent信息,删除parent中的
2、加入Spring Cloud依赖
首先需要确定版本号,Spring Cloud Alibaba、Spring Cloud、Spring Boot三者需要版本兼容
整体要以Spring Cloud Alibaba的版本为主,来决定SpringBoot的版本。
Spring Cloud Alibaba、Spring Cloud、Spring Boot官方版本说明地址:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E(此处如果打不开可能需要搬个梯子)
目前企业中SpringBoot大部分使用Spring2.几,那么我们采用最新的稳定版2.3.12.RELEASE,Spring Cloud Alibaba采用2.2.9.RELEASE,那么Spring Cloud的版本就是Hoxton.SR12
修改父工程与子工程的java version为8,因为创建的时候用的Spring Boot 3.0.x,需要使用java 17,而改到2.x.x,就需要修改java版本。并在父pom中加入Spring Cloud Alibaba,Spring Cloud依赖(每个模块单独依赖也行,就是要写多个)
注意:Spring Cloud Alibaba和Spring Cloud都是使用import pom的方式在依赖中引入进来。默认的依赖scope为compile,type默认为jar,那么依赖的包就会在编译时将jar加载进来。而引入的jar过多会导致pom越来越大很难维护,于是可以定义一个父pom,通过import的方式继承过来,而把这部分相关的所有jar引用放到父pom里。其实直接定义
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR12version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.9.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
直接放到
由于直接用默认的maven,不需要使用maven wrapper,删除.mvn、mvnw、mvnw.cmd文件,HELP.md也删了吧,留着没什么用,需要可以后期补一个README.md。删完看效果(右图)
-------
官网地址:https://nacos.io/zh-cn/index.html
在readme里面找到下载地址
或者直接看官网文档,也可以跳转到github下载链接
根据操作系统下载包,上面两个是安装包,下面两个是源码,windows直接下载nacos-server-2.2.0.zip即可
下载完之后,再看readme,里面告诉你启动方式,windows用cmd脚本启动,其实双击就行,但是因为默认的启动方式不是单机的,而是cluster集群模式。要么就是如下图一样在启动时后面跟上启动方式,要么直接修改脚本,将cluster修改为standalone
双击cmd脚本,如下图所示
然后在本地浏览器中输入URL:http://localhost:8848/nacos,账号密码:nacos/nacos,即可打开
在父项目中引入nacos,由于是Spring Cloud Alibaba的starter,使用Spring Cloud Alibaba的版本:2.2.9.RELEASE
上面提到了,Spring Cloud的引入放在了dependencyManagement中,dependencyManagement只声明依赖,不发生实际的引入,管理公共版本号。而nacos的引入在单独的dependencies中,无需显示的写入版本号,则直接从dependencyManagement的com.alibaba.cloud中继承,也可手写覆盖默认版本号。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
在test1模块的properties中配置nacos地址与服务名称
spring.application.name=test1
spring.cloud.nacos.discovery.server-addr=localhost:8848
继续新建一个module作为test1的消费端:test1-client
在test1中写如下方法:
@RestController
public class Hello {
@RequestMapping("/hello")
public String hello(){
return "Hello World";
}
}
使用test1-client去调用
没有注册中心的情况
我们直接去使用RestTemplate发生HTTP调用,RestTemplate不能直接AutoWried,需要写一个配置类,如下
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder){
return builder.build();
}
}
然后调用
@RestController
public class Hello {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("hello-client")
public String hello() {
return restTemplate.getForObject("http://localhost:8080/hello", String.class);
}
}
postman访问http://localhost:8081/hello-client,返回得到test1的hello方法值
由于注册中心的存在,我们每次不需要直接访问固定的IP+端口,而是使用预先在nacos中注册的服务名直接调用,如下,将http://localhost:8080/hello修改为http://test1/hello,使用test1的服务名代替IP+端口:
@RequestMapping("hello-client")
public String hello() {
return restTemplate.getForObject("http://test1/hello", String.class);
}
同时restTemplate需要增加@LoadBalanced注解,具备负载均衡能力
@Bean
@LoadBalanced
public RestTemplate restTemplate(RestTemplateBuilder builder){
return builder.build();
}
目前我们针对内部服务大多使用RPC,而针对外部服务仍然提供HTTP,那我们当做客户端去调用HTTP请求时,会用到以下技术:
Ribbon:是Netflix发布的开源项目,目前已停止维护,SpringCloud开发的LoadBalancer试图代替Ribbon,但目前负载均衡算法不足够,暂时无法代替。
Ribbon的诞生主要用于解决客户端负载均衡的问题。负载均衡一般包括客户端负载均衡和服务器端负载均衡。例如Nginx为服务器端负载均衡,你把请求给我,我替你完成请求分发,相当于你不知道你要被分发到哪,完全由我来决定,这就是服务器端负载均衡;而客户端负载均衡通常会获取到服务方的地址,由自身算法来决定某个请求访问哪一台服务器。
Feign:也是Netflix发布的开源项目,目前已停止维护。而OpenFeign是SpringCloud自己研发的,在Feign的基础上支持了Spring MVC的注解,因此OpenFeign已经用于替代Feign。而Feign集成了Ribbon,且将Ribbon+RestTemplate请求的方式模板化,像调用本地方法那样更加优雅的完成开发。
目前来说Ribbon+RestTemplate、OpenFeign的方式均可,但个人更倾向于OpenFeign。
创建openfeign的module,仍然作为test1的消费端
使用openfeign分为以下四步。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
@SpringBootApplication
@EnableFeignClients
public class Test1ClientOpenfeignApplication {
public static void main(String[] args) {
SpringApplication.run(Test1ClientOpenfeignApplication.class, args);
}
}
新建一个接口,使用@FeignClient注解标注,并指定要访问的nacos服务名;同时新建方法,其@RequestMapping路径与要访问的接口保持一致;若访问的服务类上面存在路径,则在@FeignClient中指定path
@FeignClient(name = "test1")
@Service
public interface FeignService {
@RequestMapping("/hello")
String hello();
}
依赖注入刚刚定义的FeignService,并使用FeignService访问定义的方法,则会指向配置的服务及路径
@RestController
public class Hello {
@Autowired
private FeignService feignService;
@RequestMapping("/hello-client")
public String hello() {
return feignService.hello();
}
}
当我们在内部服务之间调用时,可以RPC进行访问。
由于Dubbo在Spring Cloud Alibaba 2021.0.1.0移出主干,那么想使用Spring Cloud中的Dubbo就需要降低版本。
原本是想将Spring Cloud Alibaba的版本号修改为2.2.7.RELEASE;相应的Spring
Cloud对应的版本号仍为Spring Cloud
Hoxton.SR12,相应的SpringBoot版本号仍为2.3.12.RELEASE。因此仅调整Spring Cloud
Alibaba版本号即可。 但实际发现,感觉是兼容性没做好还是怎么的,一直报错:An attempt was made to call a
method that does not exist. The attempt was made from the following …
那我们退而求其次降低一个版本:
Spring Cloud Alibaba:2.2.6.RELEASE
Spring Cloud:Hoxton.SR9 Spring
Boot:2.3.2.RELEASE
dubbo作为RPC协议,需要服务方和消费方均遵循该协议,因此需要新建两个module,分别依赖Dubbo。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-dubboartifactId>
dependency>
首先要先建立一个公共业务api,一般都是单独放在一个工程里,并且消费方、服务方dubbo工程分别引入该api工程。如下,新建test3-dubbo-api工程,单独一个HelloService接口,一个hello()方法,并且入参出参的实体类必须实现序列化。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-dubboartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.examplegroupId>
<artifactId>test3-dubbo-apiartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
因为我的nacos在父工程pom中已经引入,这里不再进行单独依赖
新建服务方HelloServiceImpl,继承api中的HelloService并实现其方法
并使用注解@DubboService修饰类,将服务暴露
@DubboService
public class HelloServiceImpl implements HelloService {
@Override
public HelloResponse hello(HelloRequest request) {
return new HelloResponse("hello world");
}
}
spring.application.name=test3-server-dubbo
server.port=8090
# dubbo服务扫描路径(@DubboService修饰的类)
dubbo.scan.base-packages=com.example.test3serverdubbo
spring.cloud.nacos.discovery.server-addr=localhost:8848
dubbo.protocol.name=dubbo
# 服务端口,-1指不限制,也可使用其他数值指定
dubbo.protocol.port=-1
# 注册中心:使用当前技术栈的注册中心nacos
dubbo.registry.address=spring-cloud://localhost
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-dubboartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.examplegroupId>
<artifactId>test3-dubbo-apiartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
因为我的nacos在父工程pom中已经引入,这里不再进行单独依赖
spring.application.name=test3-client-dubbo
server.port=8091
dubbo.protocol.name=dubbo
## 服务端口,-1指不限制,也可使用其他数值指定
dubbo.protocol.port=-1
# 注册中心:使用当前技术栈的注册中心nacos
dubbo.registry.address=spring-cloud://localhost
# 需要调用的nacos服务名称
dubbo.cloud.subscribed-services=test3-server-dubbo
然后新建Hello的Controller类,使用@DubboReference注解
@RestController
public class Hello {
@DubboReference
private HelloService helloService;
@RequestMapping("/hello-client")
public HelloResponse hello() {
return helloService.hello(null);
}
}
当我们存在服务A调用B,B调用C,即:A–>B–>C
雪崩效应:当C服务因某种原因不可用时,B的请求发出一直得不到响应,就持续占用B的线程,最终因为B的资源耗尽而导致B不可用,进而引发C的不可用,这样的现象称为雪崩效应。
一般处理高并发问题会采用如下方式:
控制台git地址:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
下载jar地址:https://github.com/alibaba/Sentinel/releases,看一下Spring Cloud Alibaba对应的Sentinel版本是1.8.1,翻到版本是v1.8.1的部分,展开Assets:
启动jar:直接java -jar就行,例如我的放在了D盘,启动后默认端口是8080。-Dserver.port=端口号可以用于修改端口号,其余设置参数可见官网(例如:用户名密码等)
java -jar D:\ComputerSoftware\sentinel-dashboard-1.8.1.jar
浏览器访问:http://127.0.0.1:8080/#/dashboard/home,用户名密码:sentinel / sentinel
pom中加入如下依赖,需要控制台可添加第二个依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-transport-simple-httpartifactId>
dependency>
支持两种方式:
1、可以启动java项目时指定启动参数,idea启动直接如下图配置即可
-Dcsp.sentinel.dashboard.server=127.0.0.1:8081
2、支持在配置文件中指定。如下图 设置端口号9090,服务名test2,sentinel为刚刚启动设置的8081端口
spring.application.name=test2
server.port=9090
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8081
然后再使用hello方法
postman调用9090端口
Sentinel控制台即可展示该服务,可以设置限流、熔断等。
设置2并发
然后使用jmeter跑10并发查看结果,验证OK,有一些失败的,并且失败原因为:flow limiting。至于成功的为什么不是只有2个请求而是4个,其实这个是随机的,一次性发出的十个并发和服务器设置的线程数相关,那么处理不完的线程就需要排队,轮到他的时候可能已经有成功的请求释放了令牌。
传统的项目都使用Nginx等负载均衡器来完成服务的路由功能,而在微服务中,由于服务数量的剧增,服务之间重复的问题尤其明显,例如:负载均衡、流量控制、黑白名单、容灾切换等。我们可以使用网关来完成这些功能。
当然网关也分很多种,例如:
- 入口网关(Ingress Gateway):负责保护和控制来自集群外部的流量对集群的访问
- 出口网关(Egress Gateway):负责控制离开集群的所有流量
- 边车网关(Sidecar gateway):兼具以上两种功能只不过粒度更小,应用于服务网格,具体可百度
(记住不要同时引入web依赖,gateway与servlet冲突,会导致项目启动失败)
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
使用gateway来路由test1(http)、test3-server-dubbo(rpc)
由于test3-server-dubbo是dubbo服务,因此配置dubbo协议开头,最后两行的意思是 使用gateway本身的路径加上/temp来代替test3-server-dubbo/HelloService,/temp后面加的路径 即为:test3-server-dubbo/HelloService后面加的,例如:/temp/hello,就代表:test3-server-dubbo/HelloService/hello
而为什么没有配置test1的,因为我们同时接入nacos,test1可以直接服务发现,不用在配置文件里单独配(test3-server-dubbo在nacos中也有,但不知道为什么不行,可能是因为协议问题),假如想访问test1的/hello路径就直接:http://gateway的IP+端口/test1/hello。test1的为http协议,能直接对接过去,test3-server-dubbo的需要在gateway中仍然使用dubbo来对接,这里不再赘述。
spring.application.name=gateway
server.port=9091
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.routes[0].id=test1&test3-dubbo
spring.cloud.gateway.routes[0].uri=dubbo://test3-server-dubbo/HelloService
spring.cloud.gateway.routes[0].predicates[0]=Path=/temp/**
gateway作为网关,除了路由还可以进行流量控制,因此可以整合sentinel来做限流熔断等,略。
当我们服务与服务之间发生调用关系且存在对数据的变更,那么就存在一个场景:任何一个环节出现问题,多个服务的数据必须全部回滚,才能保证数据的一致性。
例如现在我们实现一个:服务test1-client # insertUser方法 调用test1 # insertUser方法,存在如下步骤:
- test1-client # insertUser 往user_inf中插入一条数据:123,张三
- test1 # insertUser 往user_inf中插入一条数据:456,李四
这两个插入的数据要保证原子性,第一个user插入成功了再插入第二个,一旦第二个插入失败了 第一个也同时回滚,这就是分布式事务。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.21version>
dependency>
表名:user_inf,两个字段:user_id,user_name
test1(服务方),插入:456 李四
@RestController
public class Hello {
@Autowired
private JdbcTemplate jdbcTemplate;
@RequestMapping("/insertUser")
public String insertUser(){
jdbcTemplate.execute("INSERT INTO user_inf (user_id,user_name) VALUES ('456','李四')");
return "insert zhangsan & lisi success";
}
}
test1-client(消费方),插入:123 张三;并调用test1,插入:456 李四
@RestController
public class UserController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
@RequestMapping("/insertUser")
public String insertUser() {
jdbcTemplate.execute("INSERT INTO user_inf (user_id,user_name) VALUES ('123','张三')");
return restTemplate.getForObject("http://test1/insertUser", String.class);
}
}
把数据库清空,这时候我们给test1的insertUser改造一下,加入一个异常,并且加入事务@Transactional,使其抛出异常数据插入发生回滚
@RequestMapping("/insertUser")
@Transactional(rollbackFor = Exception.class)
public String insertUser() throws Exception {
jdbcTemplate.execute("INSERT INTO user_inf (user_id,user_name) VALUES ('456','李四')");
throw new Exception("故意的exception");
// return "insert zhangsan & lisi success";
}
看效果
发生异常后,test1 # insertUser方法发生了事务回滚,但test1-client # insertUser仍然插入成功,数据库中仅存在123,张三的字样。原子性遭到破坏
官网下载:http://seata.io/zh-cn/blog/download.html,按照与Spring Cloud Alibaba对应的版本下载,我这里选择1.3.0
修改如下配置文件,高版本的seata的conf文件夹下没有nacos-config.txt,可以下载低版本的copy过来
nacos-config.txt
删除如下行:
service.vgroup_mapping.my_test_tx_group=default
添加刚刚的两个服务配置:
注意:spring cloud alibaba2.1以后的版本用这个
service.vgroupMapping.test1=default
service.vgroupMapping.test1-client=default
注意:spring cloud alibaba2.1以前的版本用这个
service.vgroup_mapping.test1=default
service.vgroup_mapping.test1-client=default
使用git执行nacos-config.sh如下命令。因为是shell脚本,要用git bash执行,路径记得先cd好或者写绝对路径
sh nacos-config.sh localhost
在bin目录中,执行.bat文件。注意:jdk1.8以上无法启动
seata-server.bat -p 8090 -m file
初始化数据库,在所有需要分布式事务的数据库中添加事务日志记录表
高版本这个sql脚本也没有,去低版本里找
在你使用的数据库中执行这个脚本的命令。其实就是新建一个undo_log的表
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
使用Seata的重新包了一下。服务方和消费方都要重写!
@Configuration
public class JdbcTemplateConfig {
@Bean
public JdbcTemplate setJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(new DataSourceProxy(dataSource));
}
}
在application.properties中加入seata配置。服务方和消费方都要加入!
其中spring.application.name的配置必须与刚刚在seata中配置的nacos-config.txt结尾相同
spring.cloud.alibaba.seata.tx-service-group=${spring.application.name}
################################### 注意:只有Spring Cloud Alibaba 2.1之后版本要这样配 start ###############################
#################### Spring Cloud Alibaba 2.1之前的版本需要修改seata的registry.conf文件然后复制到工程中 ####################
### seata注册中心
# seata注册中心使用的服务
seata.registry.type=nacos
# nacos服务地址
seata.registry.nacos.server-addr=localhost:8848
# seata注册到nacos中的服务名
seata.registry.nacos.application=seata-server
seata.registry.nacos.username=nacos
seata.registry.nacos.password=nacos
seata.registry.nacos.group=SEATA_GROUP
### seata配置中心
seata.config.type=nacos
seata.config.nacos.server-addr=localhost:8848
seata.config.nacos.username=nacos
seata.config.nacos.password=nacos
seata.config.nacos.group=SEATA_GROUP
################################### 注意:只有Spring Cloud Alibaba 2.1之后版本要这样配 end ###############################
spring cloud alibaba2.1前的版本需要修改conf目录中registry.conf文件内容,然后复制到工程的resource根目录下
registry.conf,保留以下配置,其他的直接删除
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
# seata注册中心
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
namespace = "public"
username = "nacos"
password = "nacos"
}
}
# seata配置中心
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "public"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
把刚刚的registry.conf复制到Spring工程的resource根目录下
直接在消费方的调用方法上增加如下注解即可生效。
@GlobalTransactional
test1-client
@GlobalTransactional
@RequestMapping("/insertUser")
public String insertUser() {
jdbcTemplate.execute("INSERT INTO user_inf (user_id,user_name) VALUES ('123','张三')");
return restTemplate.getForObject("http://test1/insertUser", String.class);
}
test1
@RequestMapping("/insertUser")
public String insertUser() throws Exception {
jdbcTemplate.execute("INSERT INTO user_inf (user_id,user_name) VALUES ('456','李四')");
throw new Exception("故意的exception");
// return "insert zhangsan & lisi success";
}
请求发生异常
查看,分布式事务生效!
user_inf表,未成功插入任何一条记录,而undo_log记录了两条,说明插入的两条数据均进行了回滚。