在高并发场景下,送你这三根“救命毫毛”

开发 架构
在设计高可用架构时,除需要考虑冗余和备份机制外,还需要实施一系列服务治理策略,来确保系统在面对异常流量、服务依赖故障等特殊情况时的稳定性和弹性。这些策略包括限流、降级、熔断和隔离等,它们有助于系统在压力下保持核心功能,避免全面崩溃。

高可用是指,系统在面对各种挑战时,如硬件故障、软件缺陷、网络问题等,依然能够持续提供服务。高可用通常用“五个九”(99.999%的可用性)这样的术语来衡量,意味着系统一年中的停机时间只有几分钟。

分布式系统的高可用架构设计如图所示:

  • 用户请求:所有用户的请求都首先到达负载均衡器。
  • 负载均衡器:它是高可用架构的关键组成部分。它接收来自用户的请求,并将其均匀地分配给多个服务器,如服务器A和服务器B。这样做的目的是防止任何单个服务器过载并提高资源利用率。负载均衡器还负责进行健康检查,以确保所有流量仅被转发到健康的服务器。
  • 服务器A和服务器B:代表应用服务器的集群,每个服务器都能独立处理请求。这些服务器是冗余的,如果一个服务器失败,则负载均衡器可以将流量重定向到其他健康的服务器。
  • 数据库A和数据库B:代表数据层的冗余,确保数据的持久性和可用性。通过主从复制或其他同步机制,可以在一个数据库发生故障时快速切换到另一个数据库,以维护服务的持续性。
  • 监控系统:负载均衡器也可以连接一个监控系统,用来监控整个架构的健康状态,包括服务器和数据库的性能指标。监控系统可以自动响应某些事件,如在检测到服务器故障时,自动从服务器池中剔除故障服务器,或者在资源使用率达到某个阈值时触发扩容。

图片图片

【实战1】在高并发场景下,使用“限流”防止系统崩溃

在设计高可用架构时,除需要考虑冗余和备份机制外,还需要实施一系列服务治理策略,来确保系统在面对异常流量、服务依赖故障等特殊情况时的稳定性和弹性。这些策略包括限流、降级、熔断和隔离等,它们有助于系统在压力下保持核心功能,避免全面崩溃。

是电商系统中常用的一种保护机制,用于控制流入系统的请求速率,防止系统因短时间内请求过多而过载。

限流的常用策略如下。

  • 固定窗口计数器:在固定的时间窗口(如每分钟)内,只允许一定数量的请求通过。
  • 滑动窗口计数器:在滑动的时间窗口内,计算请求的速率,并动态调整允许的请求数量。
  • 令牌桶(Token Bucket):系统以固定速率生成令牌,请求需要消耗令牌才能通过,令牌的生成和消耗类似于漏桶模型。这种方式允许突发流量在短时间内高速传入,但长期速率限制在固定值。
  • 优先级队列:对请求进行优先级排序,高优先级的请求先通过限流检查。在电商系统中,可能会对以下几方面实施限流。
  • 用户级别的限流:通过用户ID进行识别,为每个用户都设置独立的限流规则,防止单个用户频繁请求,如每秒限制用户下单次数。例如,使用Redis等缓存数据库记录每个用户的请求时间戳,只有当时间间隔满足要求时,才允许新的请求通过。
  • 接口级别的限流:对特定的API设置限流规则,如每个API在一定时间内只能处理一定数量的请求。可以使用Sentinel等工具,为每个接口都配置限流规则,并在网关或API控制器中实现限流逻辑。
  • 全局级别的限流:对整个系统或服务集群设置总体的流量控制,防止整个系统因流量过大而崩溃。可以在负载均衡器或网关层面实现全局限流,如Nginx的限流模块。

1.接口级别的限流

以访问电商系统中的商品详情页为例,假设我们希望限制每个用户每秒对商品详情页的访问次数不超过5次,则可以通过使用令牌桶算法实现。Spring Cloud Gateway提供了集成令牌桶算法限流的能力,结合Redis使用,可以实现分布式的限流策略。Spring Cloud的代码如下。

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;


@Configuration
public class GatewayConfig {
  // 定义路由规则,指向商品详情服务
  @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder){
        return builder.routes()
                .route("product_detail_route", r -> r.path("/product/details/**")
                     .filters(f -> f.requestRateLimiter().configure(c -> c.setRateLimiter(redisRateLimiter())))
                        .uri("lb://PRODUCT-SERVICE"))
                .build();
    }


    // 应用限流过滤器,使用Redis实现
    @Bean
    RedisRateLimiter redisRateLimiter() {
        // 创建RedisRateLimiter实例
        // 参数分别是允许用户每秒进行5次请求,突发流量大小为10个令牌
        return new RedisRateLimiter(5, 10);
    }
}

对代码的解析如下。

requestRateLimiter():应用限流过滤器,使用Redis实现。

RedisRateLimiter(5, 10):配置令牌桶参数,表示每秒生成5个令牌,允许的突发流量大小为10个令牌。

通过上述配置,当用户对商品详情页的访问频率超过每秒5次时,额外的请求将被限流策略拦截,从而保护了背后的商品详情服务不会因为过高的访问频率而过载。

提示:在实际部署时,需要考虑Redis的性能和高可用配置,确保限流服务本身不成为系统的瓶颈。

全局级别的限流

在电商系统中,全局限流是一个关键的策略,它能够帮助系统在面对大量用户请求时保持稳定和高效运行。全局限流策略通常涉及多个层面,包括客户端、接入层、应用层、数据层的优化。

图10-2是一个常见的电商系统架构,在该架构的基础上实施全局限流的具体步骤和策略如下。

图片图片

(1)接入层限流。

接入层限流是对进入系统的请求进行控制的第一道防线,旨在保护后端服务不被突发的高流量击溃。这通常在API网关(位置③)或负载均衡器层面实现南北流量的限流。通过定义流量阈值来限制短时间内处理的请求量,超过阈值的请求会被暂时阻塞或拒绝,以此来确保系统稳定运行并防止服务过载。

(2)读缓存的优化。

①客户端缓存。客户端缓存(位置①)主要针对变化不频繁的数据,如用户的配置信息、商品的静态详情等。通过在客户端(如Web浏览器、移动应用)存储这些数据,可以减少对后端服务的请求次数。

②CDN缓存。CDN(内容分发网络)(位置②)可以缓存静态资源如图片、CSS、JS文件等,使用户能就近访问这些资源。这不仅加速了用户的访问速度,还大大减轻了后端服务器的读取压力。

③Redis缓存。将热点数据(如频繁查询的商品库存信息、用户会话等)缓存到Redis(位置⑦),可以显著提高读取效率,减轻数据库的查询压力。

【实战2】在高并发场景下,使用“熔断”防止服务崩溃

在电商系统中,服务间的依赖关系复杂,一旦某个服务发生故障,很容易引发连锁反应,导致整个系统瘫痪,这就是所谓的“服务雪崩”效应。

以商品详情页服务为例,这个服务在电商系统中非常关键,它需要从多个依赖服务中获取数据,如商品描述服务、商品价格服务、商品评论服务等。如果其中一个服务发生故障,没有熔断保护,则导致用户无法正常访问商品详情页,严重时甚至可能影响整个系统,如图10-3所示。

图片图片

熔断策略是一种有效的机制,用于防止这种情况的发生。就像电路中的保险丝一样,当电流过大时,保险丝会熔断以保护电路不受损害。在高可用架构中,熔断器也能够在服务出现问题时“断开”,从而保护系统不受进一步损害。

在上述商品详情页服务中添加熔断器后,如图10-4所示。

图片图片

熔断器主要有如下三种状态。

  • 关闭(CLOSED):在正常状态下,所有请求都可以直接调用服务。
  • 打开(OPEN):当错误率超过阈值时,熔断器打开。此时,对该服务的调用会立即失败,不会执行实际的业务逻辑。这个状态会持续一段时间,被称为“熔断时间”。
  • 半开(HALF-OPEN):熔断时间过后,熔断器进入半开状态。这时,允许有限的请求通过以测试服务是否恢复正常。如果这些请求都成功了,则熔断器回到闭合状态;如果仍有过多的请求失败,则熔断器再次打开。

以下是使用Spring Cloud Hystrix实现的熔断器示例。假设有一个获取商品评论的服务,当评论服务发生故障时,熔断器将自动断开,调用备用方法返回默认评论。

(1)添加Hystrix依赖到项目中(以Maven为例),代码如下。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

(2)在Spring Boot应用主类或配置类上添加@EnableCircuitBreaker注解来启用Hystrix,代码如下。

@SpringBootApplication
@EnableCircuitBreaker
public class ProductServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}

(3)创建一个服务类,使用@HystrixCommand注解定义熔断策略,代码如下。

@Service
public class CommentService {


    @HystrixCommand(fallbackMethod = "getDefaultComments")
    public String getComments(String productId) {
        // 模拟从评论服务获取评论的操作,可能会因为服务发生故障而抛出异常
        if (new Random().nextInt(10) < 8) { // 假设有80%的概率服务会失败
            throw new RuntimeException("评论服务异常");
        }
        return "正常获取评论数据";
    }
     // 定义熔断时的备用方法
    public String getDefaultComments(String productId) {
        return "评论服务当前不可用,请稍后再试";
    }
}

在上述代码中,getComments()方法尝试获取商品的评论信息。这里我们通过随机数模拟服务的不稳定性。@HystrixCommand注解指定了当getComments()方法调用失败时,将自动调用getDefaultComments()方法作为备用逻辑,向用户返回一个友好的提示信息。

通过上面的示例,我们可以将这种策略应用到所有的服务调用中,以保证即使在某个服务发生故障时,也能向用户提供最基本的服务,从而提高系统的整体可用性和用户的体验。

此外,在Node.js中可以使用Hystrix.js库来实现熔断策略,代码如下。

const Hystrix = require('hystrix-js');


// 创建一个熔断器,设置错误率阈值为50%
const paymentHystrix = new Hystrix({
    metrics: {
        healthPercentThresholds: [50]
    }
});


// 包装支付服务的调用
function makePaymentRequest() {
    return paymentHystrix.wrap(((resolve, reject) => {
        // 假设的支付服务调用
        payService支付((err, result) => {
            if (err) {
                reject(err); // 支付失败,触发熔断器
            } else {
                resolve(result); // 支付成功
            }
        });
    })).then(() => {
        // 处理支付成功
    }).catch(err => {
        // 处理支付失败,可能触发熔断器
        console.error('支付失败:', err);
    });
}

在这段代码中,Hystrix对象包装了支付服务的调用。如果支付服务的错误率达到了50%,则将触发熔断器,之后的支付请求将会立即失败,从而保护系统不受进一步的损害。

【实战3】在高并发场景下,使用“降级”应对性能瓶颈

在高并发场景下,电商系统经常会遇到性能瓶颈,这可能导致用户体验下降,甚至服务中断。为了应对这种情况,开发团队通常会采用降级策略,有计划地降低系统的部分功能,以确保核心功能的稳定运行。

常用的降级策略如下。

  • 服务降级:在后端服务响应慢时,返回缓存的数据或者简化的响应内容。
  • 功能降级:暂时关闭或简化一些非核心功能,如商品推荐、促销活动等。
  • 用户体验降级:在无法提供完整服务时,只提供基本的服务,如用文本描述代替图片等。

1.服务降级

对于电商系统的后端服务(如商品服务),服务降级的具体实现策略如下。

(1)使用缓存数据。

  • 系统预先将热门商品的详情页信息缓存起来,如缓存在Redis或Memcached中。
  • 当后端服务响应变慢或不可用时,系统自动切换为返回缓存数据。
  • 可以定期更新缓存,以保证信息的相对新鲜度。

(2)简化响应内容。

  • 对于一些非核心的信息(如用户评论、推荐商品等),可以在系统负载高时不加载这些内容。
  • 提供一个简化版本的商品详情页,只包含最关键的购买信息,如价格、库存和基本描述。这样可以减少网络传输的数据量和前端渲染的复杂度。
  • 在某些情况下,可以根据实时的系统负载动态决定是否触发降级处理。例如,如果检测到CPU或内存使用率超过预设阈值,则自动开始返回简化的响应内容。

下面是一段简单的Java代码,展示了如何根据后端服务的状态返回不同的响应。

public class ProductService {
    private CacheManager cacheManager;


    public ProductService(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }


    // 获取商品详情
    public ProductDetail getProductDetail(String productId) {
        try {
          // 尝试从数据库中获取最新的商品详情页
            ProductDetail productDetail = fetchProductDetailFromDB(productId);
            return productDetail;
        } catch (Exception e) {
            // 如果数据库访问出错或超时,则使用缓存数据
            ProductDetail cachedDetail = cacheManager.getCachedProductDetail(productId);
            if (cachedDetail != null) {
                return cachedDetail; // 返回缓存的商品详情页信息
            }
            // 如果没有缓存,则返回简化的商品信息
            return new ProductDetail(productId, "暂无详细描述,请稍后再试。");
        }
    }


    // 模拟从数据库中获取商品详情页
    private ProductDetail fetchProductDetailFromDB(String productId) throws Exception {
        // 模拟数据库操作可能的延迟
        Thread.sleep(200); // 假设有200ms的延迟
        return new ProductDetail(productId, "完整的商品描述。");
    }
}

对代码的解析如下。

  • ProductService类负责处理商品详情页的获取。
  • 如果从数据库中获取数据失败(如抛出异常),则尝试从缓存中获取商品详情页。
  • 如果缓存也不存在,则返回一个包含极简信息的ProductDetail对象,提示用户稍后再试。

通过这种方式,即使后端服务由于某些原因而性能下降,用户依然可以得到必要的商品信息,保证了用户体验的连续性和系统的高可用。

2.功能降级

假设电商系统中有一个商品推荐服务,它根据用户的浏览历史和购买历史来推荐商品。虽然这项服务可以增强用户体验,但它并不是完成交易的核心部分。在高流量事件中,如大型促销活动或购物季等,服务器可能会面临极大的压力。

这时就需要对商品推荐服务进行降级处理,处理步骤如下。

(1)定义服务状态控制。

在服务的Java实现中,定义一个静态变量isServiceEnabled,用来标识推荐服务当前是否可用。

(2)服务降级开关。

在服务的Java实现中,提供一个公共方法setServiceStatus(boolean status),允许动态地控制推荐服务的开启或关闭。

(3)服务方法调整。

在推荐方法中,首先检查isServiceEnabled的状态。如果服务被设置为不可用,则进行降级处理,推荐方法将直接返回一个空列表或预设的简单推荐列表,这样可以大幅减少对系统资源的占用。

上述降级处理的具体代码如下。

public class RecommendationService {
    // 标志位,用于标识推荐服务是否可用
    private static volatile boolean isServiceEnabled = true;


    // 设置服务状态的方法,可以被外部调用来开启或关闭服务
    public static void setServiceStatus(boolean status) {
        isServiceEnabled = status;
    }


    // 推荐商品的方法
    public List<Product> recommendProducts(User user) {
        // 检查推荐服务是否被降级(即关闭)
        if (!isServiceEnabled) {
            // 在推荐服务被关闭时,返回一个空列表或者预设的简单推荐列表
            return Collections.emptyList(); // 这里为了简化,直接返回空列表
        }
        // 正常情况下的推荐逻辑
        // 假设有一个复杂的逻辑,根据用户的浏览历史、购买历史等信息生成推荐列表
        // ……
        return new ArrayList<>(); // 在实际业务中应有复杂逻辑返回推荐商品
    }
}

在系统监控工具观察到服务器负载达到临界点时,系统管理员或自动化脚本可以调用setServiceStatus(false)方法来关闭推荐服务,减轻服务器的压力。当系统负载恢复正常后,再次调用setServiceStatus(true)方法来重新开启推荐服务。

3.用户体验降级

假设一个电商网站在特价促销期间遭遇流量激增,服务器负载急剧上升,导致图片服务器响应变慢。为了不影响核心购买流程,网站可以实施用户体验降级策略,即在图片加载困难时用文本描述或图标作为替代,确保页面加载速度和核心交易功能的正常运行。

用户体验降级策略如下。

  • 用文本描述代替图片:在图片加载缓慢或失败时,使用文本描述来代替图片,让用户至少知道商品的基本信息。
  • 简化页面布局:在前端页面渲染能力受限时,提供一个简化的页面布局,只包含必要的元素和信息。
  • 延迟加载:对于非关键的页面元素,如广告和推荐商品,可以采用延迟加载的方式,优先保证核心内容的加载。

例如,在前端代码中实现降级逻辑,确保在后端服务不稳定或网络条件差时,用户仍然能够获取基本信息。前端的代码如下。

// 假设这是一个商品详情页的前端代码片段
function fetchProductDetails(productId) {
    fetch(`/api/product/${productId}`)
        .then(response => {
            if (!response.ok) {
                // 如果请求失败,则返回降级内容
                return getDegradedProductDetails(productId);
            }
            return response.json();
        })
        .then(details => renderProductDetails(details))
        .catch(error => {
            // 如果请求失败,则返回降级内容
           getDegradedProductDetails(productId).then(degradedDetails =>{
                renderDegradedProductDetails(degradedDetails);
           });
        });
}


function getDegradedProductDetails(productId) {
    // 这里可以从本地缓存中获取,或者是更简单的数据
    return new Promise(resolve => {
        resolve({
            name: '商品名称',
            description: '商品描述',
            price: '商品价格'
        });
    });
}


function renderDegradedProductDetails(degradedDetails) {
 // 使用简化的模板或布局渲染降级内容
    const container = document.getElementById('product-details');
    container.innerHTML = '
        <h1>${degradedDetails.name}</h1>
        <p>${degradedDetails.description}</p>
        <p>价格:$${degradedDetails.price}</p>
    ';
}

在上述代码中,fetchProductDetails()方法尝试从后端获取完整的商品详情页。如果请求失败,则它会调用getDegradedProductDetails()方法来获取一个简化的商品详情页,并使用 renderDegradedProductDetails()方法来渲染。

责任编辑:武晓燕 来源: JAVA日知录
相关推荐

2018-07-27 10:56:10

2024-03-05 10:03:17

NoSQL数据库算法

2016-11-09 21:09:54

mysqlmysql优化

2021-01-13 05:27:02

服务器性能高并发

2023-07-18 09:24:04

MySQL线程

2019-07-05 17:40:24

MySQL并发数据库

2018-05-04 15:15:37

数据库MySQL并发场景

2022-05-11 11:25:49

模型方案

2020-10-15 06:26:24

高并发场景冰河

2024-08-29 09:32:36

2023-10-07 08:54:28

项目httpPost对象

2024-02-01 09:51:17

数据库缓存

2023-05-28 13:13:54

高并发场景JUC

2021-01-13 05:23:27

缓存数据库高并发

2024-11-27 00:20:32

2023-08-16 11:39:19

高并发调优

2022-05-27 09:25:49

数据并发

2024-06-27 08:04:39

2020-09-03 06:33:35

高并发场景分布式锁

2024-02-27 13:00:26

数据库架构
点赞
收藏

51CTO技术栈公众号