本文共 8385 字,大约阅读时间需要 27 分钟。
查看之前的博客可以点击顶部的【分类专栏】
高并发是指在同一个时间点,有很多用户同时的访问同一 API 接口或者 Url 地址。它经常会发生在有大活跃的用户量,用户高聚集的业务场景中(比如秒杀、抢票)。
在高并发的情况下,网站仍然可以不间断的提供服务。
在信息技术领域,高可靠性指的是运行时间能够满足预计时间的一个系统或组件。可靠性可以用“100%可操作性”或者“从未失败”这两种标准来表示。一个被广泛应用但却难以达到的标准是著名的“5个9标准”,就是说工作的可靠性要达到99.999%。
根据业务需求进行拆分成N个子系统,多个子系统相互协作才能完成业务流程,子系统之间通讯使用RPC远程通讯技术。
微服务是指开发一个单个、小型的但有业务的服务,每个服务都有自己的处理和轻通讯机制,可以部署在单个服务器上,让专业的人做专业的事情。
系统设计不仅需要考虑实现业务功能,还要保证系统高并发、高可用、高可靠等。同时还应考虑系统容量规划(流量、容量等)、SLA制定(吞吐量、响应时间、可用性、降级方案等)、监控报警(机器负载、响应时间、可用率等)、应急预案(容灾、降级、限流、隔离、切流量、可回滚版本等)。
集群、负载均衡、降级、限流、熔断、缓存、异步并发、连接池、线程池、扩容、消息队列、分布式任务、主从复制、读写分离、防幂等、请求令牌、接口验签、数据备份、前端图形验证(防机器攻击)等。
比如:
1、通过负载均衡和反向代理实现分流(Nginx 负载均衡,恶意 IP 使用 Nginx Deny 策略或者 iptables 拒绝)
2、通过限流保护服务免受雪崩之灾。
3、通过降级实现部分可用、有损服务。
4、通过隔离实现故障隔离。
5、通过合理设置的超时与重试机制避免请求堆积造成雪崩。
6、通过回滚机制快速修复错误版本。
OK,本系列博客只着重讲【降级、限流、熔断】。
在分布式的微服务架构中,系统往往被拆分成很多个服务单元,每个服务单元部署在不同的机器或者不同的进程中,通过远程调用的方式进行通讯。这样一来,就有可能因为网络故障等原因导致服务调用异常或延迟。如果此时调用方的请求不断增加,最后因为服务的请求形成积压、阻塞,结果导致服务瘫痪,宕机,俗称“服务雪崩”。(就像堆木积,不断的往上堆积,造成底部的压力增大,容易导致崩塌。)
如果某一个接口返回的数据都相同,在高并发中,可以把这个接口返回的数据进行缓存,以便能快速的响应。
在微服务架构中,我们将一个项目拆分成多个独立的子模块,这些独立的模块通过远程调用来互相通讯,在高并发情况下,通信次数的增加会导致总的通信时间增加。同时,线程池的资源是有限的,高并发环境会导致大量的线程处于等待状态,进而导致响应延迟。为了解决这个问题,我们使用 Hystrix 的请求合并来解决。(就像过安检一样,凑够一撮人,就放行)。请求合并主要是解决请求多次在网络传输的问题:减少网络的多次传输,不再讲解此方法,用得少,还麻烦。
没有线程池隔离的项目所有接口默认都运行在一个同一个线程池中,当某个接口的请求压力过大,达到服务的线程池(默认是 tomcat)的最大限制,其它接口的调用一直无法获取到线程资源导致请求大量堆积,发生服务雪崩效应。为防止服务雪崩,可以使用服务隔离机制(线程池方式或信号量),让每个服务或者每个接口都有自己独立的线程池,解决雪崩效应。
在高并发下,如果请求达到一定极限(可以自己设置阔值)如果流量超出了设置阈值,为了避免请求的堆积造成服务的瘫痪,可以直接拒绝服务,并且配合服务降级方式返回一个友好提示(托底数据),以保护当前服务以免宕机。
在高并发情况下,当某个服务不可用,使用 fallback 方法直接返回一个友好的错误提示(托底数据),防止用户一直等待,提高用户的体验感。
服务限流就是对接口访问进行限制,常用服务限流算法令牌桶、漏桶。计数器也可以进行粗暴限流实现。
本篇博客代码地址: 提取码:p4yw
OK,我们先看本次案例代码:有3个微服务:一个注册中心(8080端口),一个商品服务(9091端口),一个订单服务(9090端口)。订单服务通过声明式调用 Feign 调用商品服务。
正常的请求如下(都是写固定的测试数据):
订单接口:
多个产品接口:
订单调用产品接口:
OK,我们来模拟以下高并发的场景。
在商品服务获取结合的接口,增加线程休眠 2 秒
我们知道,tomcat 默认的并发是200个,我们先把 product-server 的 tomcat 服务器的最大线程数调整为10
同时,还要调整 order-server 微服务的 Feign 的 Hystrix 的超时时间,否则 Hystrix 会一直生效,导致无法模拟超时的效果:注意,这是 SpringBoot2.0+ 的版本的配置方法。
feign: client: config: default: connect-timeout: 8000 read-timeout: 8000
否则报错:
java.net.SocketTimeoutException: Read timed out at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_91] at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ~[na:1.8.0_91]
重启2台服务。
请求产品服务集合 getList 的接口,浏览器打开 F12 调试工具,看到响应时间250毫秒左右,其中2秒是我们 sleep 的。
然后我们使用 JMeter 模拟高并发场景。
不懂用 JMeter?查看博客:
我们模拟30个线程,每个线程发送50个请求,并发就是1500。查询产品接口。
然后我们对 product/getList 这个接口发送请求。注意订单微服务调用的是获取单个的接口。
点击开始,模拟高并发的请求 getList 接口,这时候,产品微服务的请求压力是很大的。那么我们看下订单微服务去请求产品微服务接口,有什么效果。
时间花费了4秒多。
我们使用 Redis 做缓存,在 pom.xml 中增加 Redis 依赖。注意 SpringBoot 2.0 之后,默认使用 ,在 2.0 之前是使用 Jedis 客户端,Lettuce 和 Jedis 的都是连接Redis Server的客户端程序。Jedis在实现上是直连redis server,多线程环境下非线程安全(即多个线程对一个连接实例操作,是线程不安全的),除非使用连接池,为每个Jedis实例增加物理连接。Lettuce基于Netty的连接实例(StatefulRedisConnection),可以在多个线程间并发访问,且线程安全,满足多线程环境下的并发访问(即多个线程公用一个连接实例,线程安全),同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
在 order-server 增加 Redis 依赖
org.springframework.boot spring-boot-starter-data-redis-reactive org.apache.commons commons-pool2
配置文件增加 Redis 配置
spring: application: name: order-server # Redis 相关配置 redis: host: 192.168.0.105 port: 6379 password: 123456 database: 0 timeout: 5000 #连接超时时间 lettuce: pool: max-active: 50 #最大连接数,默认是8 max-wait: 1000 #最大连接阻塞数 max-idle: 100 #最大空闲连接,默认是8 min-idle: 5 #最小空闲连接,默认是0 #在关闭客户端连接之前等待任务处理完成的最长时间,在这之后,无论任务是否执行完成,都会被执行器关闭,默认100ms shutdown-timeout: 5000我们知道,Redis 的模板对象需要重新配置,Redis 默认使用 JDK 序列化,JDK 序列化的好处就是在反序列化的时候,不需要声明类型。但是数据是 JSON 的五倍大小,非常消耗 Redis 的内存。因此我们需要把它的序列化重写,不使用 JDK 默认的序列化。
package com.study.config;import org.apache.commons.pool2.impl.GenericObjectPoolConfig;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisPassword;import org.springframework.data.redis.connection.RedisStandaloneConfiguration;import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;/** * @author biandan * @description * @signature 让天下没有难写的代码 * @create 2021-06-20 上午 12:43 */@Configurationpublic class RedisConfig { @Value("${spring.redis.database}") private Integer database; @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private Integer port; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.timeout}") private Long timeout; @Value("${spring.redis.lettuce.shutdown-timeout}") private Long shutDownTimeout; //最大连接数 @Value("${spring.redis.lettuce.pool.max-active}") private Integer maxActive; //最大连接阻塞数 @Value("${spring.redis.lettuce.pool.max-wait}") private Long maxWait; //最大空闲连接 @Value("${spring.redis.lettuce.pool.max-idle}") private Integer maxIdle; //最小空闲连接 @Value("${spring.redis.lettuce.pool.min-idle}") private Integer minIdle; // Redis连接工厂 @Bean public LettuceConnectionFactory lettuceConnectionFactory() { //Redis 基本连接 RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); redisStandaloneConfiguration.setDatabase(database); redisStandaloneConfiguration.setHostName(host); redisStandaloneConfiguration.setPort(port); redisStandaloneConfiguration.setPassword(RedisPassword.of(password)); //lettuce 连接池配置 GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig(); genericObjectPoolConfig.setMaxIdle(maxIdle); genericObjectPoolConfig.setMinIdle(minIdle); genericObjectPoolConfig.setMaxTotal(maxActive); genericObjectPoolConfig.setMaxWaitMillis(maxWait); genericObjectPoolConfig.setTimeBetweenEvictionRunsMillis(100); LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() .commandTimeout(Duration.ofMillis(timeout)) .shutdownTimeout(Duration.ofMillis(shutDownTimeout)) .poolConfig(genericObjectPoolConfig) .build(); LettuceConnectionFactory factory = new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig); return factory; } @Bean public RedisTemplateredisTemplate() { RedisTemplate template = new RedisTemplate<>(); //为String类型的key设置序列化器 template.setKeySerializer(new StringRedisSerializer()); //为String类型value设置通用的序列化器 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); //为Hash类型key设置序列化器 template.setHashKeySerializer(new StringRedisSerializer()); //为Hash类型value设置通用的序列化器 template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setConnectionFactory(lettuceConnectionFactory()); return template; }}
然后在 order-server 启动类开启缓存:
@EnableCaching //开启缓存注解
然后在获取 product-server 的产品接口上,增加以下代码:
@Cacheable(cacheNames = "order:product",key = "#id")
如图:
注意:
使用 @Cacheable 注解的方法,不能跟调用其方法在相同的类里,否则缓存无效。比如本案例中调用 getProductById 是在 OrderServiceImpl 类里。
然后重启 order-server 服务,还要启动我们的 Redis 环境。如果你们没有 Redis 环境,直接往下看就行了。
启动后,直接访问订单微服务,让数据先缓存到 Redis。请求:
因为要访问产品服务,还要存入 Redis,因此时间久一点。在产品服务里有打印:
然后查看 Redis:
然后,我们继续刷新访问:
第二次访问,直接从 Redis 读取缓存,速度非常快。
我们开启 JMeter 模拟高并发,然后再次访问订单-产品的接口:
说明产品微服务处于高并发下,并没有影响订单微服务获取相同 API 接口的数据,因为已经缓存到 Redis 了,直接从 Redis 缓存里读取数据,不需要跨服务调用数据。这就是请求缓存的案例。
OK,这篇博客就把相关概念讲解到这,以及讲解请求缓存这个案例,Hystrix 还有很多重要的特性解决高并发的问题。欲知后事如何,请听下回分解。
本篇博客代码地址: 提取码:p4yw
转载地址:http://wkuhf.baihongyu.com/