微服务架构
一. 服务架构演变过程
1.1 单体应用架构
所有的功能都在一个项目中
1.2 集群架构
把一个单体项目部署多个,使用nginx进行负载均衡,根据负载均衡策略调用后端服务。
不好的地方:有的服务访问量大,有的服务访问量小,这样不管访问量大小都会进行多次部署。
1.3 垂直架构
将不同功能模块进行拆分,服务之间可以相互调用,还可以根据访问量大小进行选择性的多次部署。
不好的地方:服务之间的管理调用比较麻烦
1.4 微服务架构
微服务就是一套完整的,对多个服务进行管理的解决方案。
服务治理(管理这么多服务)
服务调用
服务网关(对外提供一个统一的入口)
服务容错
链路追踪
常见的微服务解决方案:原生的springCloud
本次使用springCloud alibaba 是阿里巴巴开源的一套微服务解决方案
二. 微服务案例
电商:
-
订单服务
-
商品服务
-
用户服务
以下订单为例,在订单服务中调用商品服务,用户服务
简单演示服务调用:
java
restTemplate.getForObject("url",类.class);
三. 服务管理
服务注册中心,将微服务中的多个服务管理起来。
常见的注册中心:
- Zookeeper
- Eureka
- Nacos (是springcloud alibaba使用的)
nacos是一个注册中心,用来管理服务
安装 启动 在项目中配置一个服务名,再配置一个注册中心的地址
四. Nacos安装与配置
4.1 搭建nacos环境
4.1.1 安装nacos
下载地址: https://github.com/alibaba/nacos/releases 下载zip格式的安装包,然后进行解压缩操作
4.1.2 启动nacos
进入nacos/bin
命令启动
startup.cmd -m standalone
4.1.3 访问nacos
打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是 nacos/nacos
4.2 将商品微服务注册到nacos
4.2.1
在pom.xml中添加nacos的依赖
xml
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
4.2.2
在启动类上添加@EnableDiscoveryClient注解
eg:
java
@SpringBootApplication
@EnableDiscoveryClient //这个注解
@MapperScan("com.ffyc.springcloudshop.dao")
public class ShopOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShopOrderApplication.class);
}
}
4.2.3
在application.yml中为每个微服务定义服务名,并添加nacos服务的地址
yml
application:
name: service-user #服务名 很重要,服务之间调用时,就是通过服务名找相应的服务
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos地址
4.2.4
启动服务,观察nacos的控制面板中是否有注册上来的商品微服务
五. 负载均衡
5.1 什么是负载均衡
通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个 操作单元(服务器,组件)上进行执行。
5.2 服务调用
5.2.1
java
restTemplate.getForObject("http://127.0.0.1:8094/product/get/"+pid, Product.class); //弊端:ip,端口写死的,如果有多个服务使用不方便,没有用到注册中心。
5.2.2
使用nacos提供的客户端DiscoveryClient,动态从注册中心,通过服务名,获取服务,使用到了注册中心。
java
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
ServiceInstance productService = instances.get(new Random().nextInt(instances.size())); //从商品服务中随机获取一个服务
String productUrl=productService.getHost()+":"productService.getPort(); //动态获取服务ip和服务端口
Product product=restTemplate.getForObject("http://"+productUrl+"/product/get/"+pid.Product.class);
5.2.3 基于Ribbon实现负载均衡
在RestTemplate的生成方法上添加@LoadBalanced注解
java
@Configuration
public class RestTemplateConfig {
@LoadBalanced
@Bean
public RestTemplate createRestTemplate(){
return new RestTemplate();
}
}
配置Ribbon:
yml
ribbon:
ConnectTimeout: 2000 # 请求连接的超时时间
ReadTimeout: 5000 # 请求处理的超时时间
service-product: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
使用Ribbon组件实现的负载均衡服务调用:
java
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
OrderService orderService;
@Autowired
RestTemplate restTemplate;
@RequestMapping("/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable("pid") int pid, @PathVariable("uid")int uid,@PathVariable("num") int num){
//基于Ribbon组件的实现负载均衡的服务调用(Ribbon组件帮助我们去注册中心找服务)
Product product=restTemplate.getForObject("http://service-product/product/get/"+pid,Product.class);
User user=restTemplate.getForObject("http://service-user/user/get/"+uid,User.class);
Order order=null;
//如果产品数量大于等于传进来的需要的产品数量并且可以查到用户则
if(product!=null&&product.getStock()>=num&&user!=null){
//下订单
order=orderService.saveorder(pid,uid,num);
}
return order;
}
}
5.2.4 使用Feign组件
依赖:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
开启feign,在启动类上加@EnableFeignClients注解
java
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.ffyc.springcloudshop.dao")
@EnableFeignClients//开启Fegin
public class ShopOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShopOrderApplication.class);
}
}
创建一个ProductService接口,并使用Fegin实现微服务调用
java
@FeignClient(value = "service-product")
public interface ProductService {
//这里的path不是定义一个,是要和我们使用的service-product中的findProductById接口相同
/*
@RequestMapping("/get/{id}")
public Product findProductById(@PathVariable("id") int id){
Product product = productService.findProductById(id);
return product;
}
*/
@GetMapping(path = "product/get/{id}")
Product findProductById(@PathVariable("id")int id);
}
修改controller代码,并启动验证
java
@Autowired
ProductService productService;
@Autowired
UserService userService;
@RequestMapping("/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable("pid") int pid, @PathVariable("uid")int uid,@PathVariable("num") int num){
//基于Feign
Product product=productService.findProductById(pid);
User user=userService.findUserById(uid);
Order order=null;
//如果产品数量大于等于传进来的需要的产品数量并且可以查到用户则
if(product!=null&&product.getStock()>=num&&user!=null){
//下订单
order=orderService.saveorder(pid,uid,num);
}
return order;
}
六. 服务容错
6.1 高并发带来的问题
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相 互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可 用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量 的网络涌入,会形成任务堆积,最终导致服务瘫痪。
6.2 模拟一个高并发场景
安装ApacheJmeter用压测工具,对请求进行压力测试
进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN, 然后点击jmeter.bat启动软件。
添加线程组
添加http取样
结论: 此时会发现, 由于order方法囤积了大量请求, 导致message方法的访问 出现了问题,这就是服务雪崩的雏形。
6.3 Sentinel 使用及概念
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综 合性解决方案。它以流量 为切入点, 从流量控制、熔断降级、系统负载保护等 多个维度来保护服务的稳定性。
6.4 微服务集成Sentinel
- 在 pom.xml 中加入下面依赖
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 编写一个方法测试使用
java
//测试接口
@GetMapping(path = "/message")
public String message() {
return "测试高并发";
}
- application.yml 中配置
yml
sentinel:
eager: true
transport:
port: 9966 #随便定义一个不重复端口即可
dashboard: 127.0.0.1:9999
- 下载客户端
https://github.com/alibaba/Sentinel/releases
-
启动控制台
直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
java -Dserver.port=9999 -Dcsp.sentinel.dashboard.server=localhost:9999 -jar sentinel-dashboard-1.8.5.jar
-
访问控制台:
http://ip+端口 默认用户名密码是 sentinel/sentinel
七. Gateway
网关是为众多的微服务提供一个统一的访问入口
所有请求先进入到网关,可以在王贯中进行一些全局处理,例如权限验证,token验证,限流
-
创建api网关模块(略)
-
导入依赖,不导入web相关的依赖
xml
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 创建主类(启动类)
java
public class GatewayApplication{
public static void main(String[]args){
SpringApplication.run(GatewayApplication.class);
}
}
- 加入nacos依赖
xml
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在主类上添加注解
java
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication{
public static void main(String[]args){
SpringApplication.run(GatewayApplication.class);
}
}
- 修改配置文件
yml
server:
port: 9001
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos服务地址
gateway:
discovery:
locator:
enabled: true
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: product_route # 当前路由的标识, 要求唯一
uri: lb://service-order # lb 指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/order-serv/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: #过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前
- 测试访问
http://127.0.0.1:9001/order-serv/order/message
八. 网关限流
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多, 我们本次采用前 面学过的Sentinel组件来实现网关的限流。Sentinel支持对 SpringCloud Gateway、Zuul 等主流网关进行限流。
8.1 导入依赖
xml
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
8.2 编写配置类
java
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer =serverCodecConfigurer;
}
//初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
//配置初始化的限流参数
@PostConstruct
public void initGatewayRules(){
Set<GatewayFlowRule> rules =new HashSet<>();
//资源名称,对应路由id //限流阈值 //统计时间窗口,单位是秒,默认是1秒
rules.add(new GatewayFlowRule("order_route").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
//配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
//自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map =new HashMap<>();
map.put("code", 0);
map.put("message","接口被限流了");
return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON_UTF8).body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
九. 消息队列-MQ
消息队列(Message Queue)缩写为MQ,一般也称消息队列中间件
中间件:例如tomcat,redis都可以称为中间件,是可以实现两个不同内容之间进行交互的软件
9.1 使用场景
异步解耦:将一些不需即时响应的操作放到消息队列中,例如在项目中发送短信验证码,邮箱验证码。
可以在点击发送后,先将要发送的内容放到消息队列中,然后给用户做出响应,之后发送邮件或者发送短信的服务从消息队列中取出要发送的信息注意处理即可。
实现了从同步发送消息变为异步发送消息
9.2 RokctMQ
9.2.1 安装配置启动
- 配置环境变量
ROCKETMQ_HOME=rocketmq-4.9.3 地址
NAMESRV_ADDR=127.0.0.1:9876
- 启动NameServer
进入到bin目录输入命令: mqnamesrv.cmd
- 启动Broker
进入到bin目录输入命令: mqbroker.cmd -n 127.0.0.1:9876 atuoCreateTopicEnable=true
- 模拟发送消息
进入到bin目录输入命令: tools.cmd org.apache.rocketmq.example.quickstart.Producer
-
模拟接收消息
进入到bin目录输入命令: tools.cmd org.apache.rocketmq.example.quickstart.Consumer
6. 启动控制台
启动控制台 java -jar rocketmq-console-ng-1.0.0.jar 访问:http://127.0.0.1:6060
9.2.2 名词
NameServer(邮局):消息队列的协调者,消息队列的总服务
Broker(邮递员):负责发送,存储,投递消息
Producer(寄件人):消息的生产者
Consumer(收件人):消息的消费者
Topic(地区):用来区分不同类型的消息,可以给不同的消息定义主题,用来区分不同类型消息
Message Queue(邮件):发送的消息内容
9.2.3 使用java消息发送和接收演示
- 依赖
xml
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
- 发送消息
java
public class MQProducerTest {
public static void main(String[] args) throws Exception {
//1. 创建消息生产者, 指定生产者所属的组名
DefaultMQProducer producer = new DefaultMQProducer("myproducer-group");
//2. 指定Nameserver地址
producer.setNamesrvAddr("127.0.0.1:9876");
//3. 启动生产者
producer.start();
//4.创建消息对象,指定主题、标签和消息体
Message msg= new Message("myTopic","myTag", ("RocketMQ Message").getBytes());
//5.发送消息
SendResult sendResult= producer.send(msg,10000);
System.out.println(sendResult);
//6.关闭生产者
producer.shutdown();
}
}
- 接收消息
java
public class MQConsumerTest {
public static void main(String[]args) throws Exception {
//1.创建消息消费者,指定消费者所属的组名
DefaultMQPushConsumer consumer=new DefaultMQPushConsumer("myconsumergroup");
//2.指定Nameserver地址
consumer.setNamesrvAddr("127.0.0.1:9876");
//3.指定消费者订阅的主题和标签
consumer.subscribe("myTopic","*");
//4.设置回调函数,编写处理消息的方法
consumer.registerMessageListener(new MessageListenerConcurrently(){
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println("Receive NewMessages: "+ msgs);//返回消费状态
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消息消费者
consumer.start();
System.out.println("Consumer Started.");
}
}
9.2.4 案例
订单微服务发送消息:
- 添加rocketmq的依赖
xml
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
- 添加配置
yml
rocketmq:
name-server: 127.0.0.1:9876 #rocketMQ服务的地址
producer:
group: shop-order #生产者组
- 编写测试代码
在订单微服务控制器中添加代码
java
@Autowired
private RocketMQTemplaterocketMQTemplate;
rocketMQTemplate.convertAndSend("order-topic", "下单成功");
用户微服务接收消息:
- 添加依赖:
xml
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
- 添加配置
yml
rocketmq:
name-server: 127.0.0.1:9876
- 编写测试代码
在订单微服务控制器中添加代码
java
@Service
@RocketMQMessageListener(consumerGroup="shop-user",topic="order-topic")
public class SmsService implements RocketMQListener<String> {
@Override
public void onMessage(String ms){
System.out.println("收到一个订单信息:"+ JSON.toJSONString(ms)+",接下来发送短信");
}
}
- 启动服务,执行下单操作,观看后台输出
九. Redis实现分布式锁
在微服务系统中,一个项目可以有多个服务(进程)
此时java中的锁例如synchronized锁就会失效
使用分布式锁来对多个进程中操作进行控制
如何实现分布式锁:
基于redis实现分布式锁,在redis种可以存储一个变量,用来当作锁标志,因为redis是共享的。例如,redis种存在共享变量,说明由用户正在操作=正在持有锁,用完之后删除变量,就是释放锁。
9.1 方式1:使用setnx命令
redis中有一个setnx命令,在向redis中设置值的时候,会自动判断redis中是否存在指定的key。
如果redis中不存在,就设置成功(相当于获取锁成功)
如果redis中存在,就设置失败(相当于获取锁失败)
java
public String subStock(){
String clientId= UUID.randomUUID().toString(); //生成一个32位不重复的字符串
try{
//借助redis实现加锁
//获取锁,就是利用setnx命令设置一个键值,如果设置成功,就说明获取锁成功,否则获取锁失败
//设置失效时间,理论也是会有问题,一旦业务执行时间超过设置的时间,key就会失效,此时其他线程就可以进入获取锁
//我们先进入的线程,执行完业务,就会去删除锁,那么先进来的线程会删掉后进来线程的锁,导致其他线程再进入到同步块中
//可以为每一个线程生成版本号
Boolean res=redisTemplate.opsForValue().setIfAbsent("stock_lock","stock_lock",10, TimeUnit.SECONDS);
if(!res){
return "fail";
}
//查询库存
Integer stock=(Integer) redisTemplate.opsForValue().get("stock");
//判断库存是否大于0
//大于0,扣库存
if(stock>0){
redisTemplate.opsForValue().decrement("stock"); //默认库存-1
}
}finally {
//删除分布式锁
String cid=(String)redisTemplate.opsForValue().get("stock");
if(clientId.equals(cid)) {
redisTemplate.delete("stock_lock");
}
}
return "suc";
}
9.2 方式2: 使用redisson
- 导入依赖
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
- 创建Redisson对象
java
@Configuration
public class RedissonConfig {
//创建Redisson对象
@Bean
public Redisson getRedisson(){
org.redisson.config.Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson)Redisson.create(config);
}
}
- 使用Redisson实现加锁,释放锁
java
@Autowired
Redisson redisson;
public String subStock(){
RLock stockLock=redisson.getLock("stock_lock"); //定义redis中锁标志的key
try{
//这个时间是服务结束后防止一直占用锁
stockLock.lock(30,TimeUnit.SECONDS); //获取锁
//查询库存
Integer stock=(Integer) redisTemplate.opsForValue().get("stock");
//判断库存是否大于0
//大于0,扣库存
if(stock>0){
redisTemplate.opsForValue().decrement("stock"); //默认库存-1
}
}finally {
stockLock.unlock(); //释放锁
}
return "suc";
}