电商网站性能测试实战:双11大促压力测试案例分析_千锋教育

来源:千锋教育
发布时间:2026-05-14 06:03:50

千锋教育品牌logo

01 压测前的准备:为什么你的压测永远测不出问题?

大多数团队的压测,都在“自欺欺人”。他们用本地环境跑200个虚拟用户,看到响应时间200ms就欢呼“系统没问题”。结果上线当天,真实用户一拥而入,系统直接瘫痪。

真正的压测准备,从“模拟真实”开始。

双11场景下,用户行为不是均匀分布的。零点抢购那1分钟,流量是平时的500倍。用户不是点开首页就下单,他们浏览、搜索、加购物车、对比价格、犹豫、最终付款。这个过程,每一步都在消耗服务器资源。

我们团队在压测前,做了三件事:

第一,构建用户行为模型。 不是简单发HTTP请求,而是用脚本模拟完整购物路径。代码示例:

python
from locust import HttpUser, task, between

class ShopperUser(HttpUser):
    wait_time = between(0.5, 3.0)
    
    @task(3)
    def browse_homepage(self):
        self.client.get("/")
    
    @task(5)
    def search_products(self):
        self.client.get("/search?q=手机")
    
    @task(2)
    def add_to_cart(self):
        self.client.post("/cart/add", json={"product_id": 12345, "qty": 1})
    
    @task(1)
    def checkout(self):
        self.client.post("/order/create", json={
            "items": [{"product_id": 12345, "qty": 1}],
            "address_id": 6789
        })

这个模型,把80%的流量压在浏览和搜索上,只有20%的流量进入下单环节。真实用户就是这样。

第二,数据量必须真实。 测试数据库里只有100个商品、10个用户?压测结果毫无意义。我们把数据库灌到了1000万商品、500万用户、2亿条订单记录。数据量大了,索引失效、慢查询、缓存穿透,这些问题才会暴露。

第三,压测环境必须和生产环境一致。 不能拿开发环境的4核8G服务器测试,然后说“系统可以扛10000并发”。我们租用了和生产环境完全相同的云服务器配置,包括CDN、负载均衡、数据库集群。

压测测的不是代码,是代码在真实压力下的脆弱性。

02 三个致命漏洞:压测数据不会说谎

第一轮压测,1000并发用户,持续10分钟。结果让人崩溃。

漏洞一:库存扣减接口成了“黑洞”

压测开始后,库存扣减接口的响应时间从50ms飙升到12秒。查看日志,发现所有请求都在等待数据库的行锁。代码是这样的:

java
@Transactional
public boolean deductStock(Long productId, Integer qty) {
    // 先查询库存
    Stock stock = stockRepository.findById(productId);
    if (stock.getAvailable() < qty) {
        return false;
    }
    // 再更新库存
    stock.setAvailable(stock.getAvailable() - qty);
    stockRepository.save(stock);
    return true;
}

问题出在@Transactional和行锁上。1000个并发请求同时操作同一个商品,数据库的行锁让请求排队处理。一个请求耗时1秒,第1000个请求就要等1000秒。

漏洞二:缓存穿透让数据库“裸奔”

用户搜索热门商品时,Redis缓存命中率只有30%。大量请求直接打到数据库,导致数据库CPU飙升到95%。检查发现,缓存设置有问题:

java
public Product getProduct(Long id) {
    // 先从缓存取
    Product product = redisTemplate.opsForValue().get("product:" + id);
    if (product != null) {
        return product;
    }
    // 缓存未命中,查数据库
    product = productRepository.findById(id).orElse(null);
    if (product != null) {
        // 缓存设置5分钟过期
        redisTemplate.opsForValue().set("product:" + id, product, 300, TimeUnit.SECONDS);
    }
    return product;
}

问题在于:热门商品被频繁查询,但缓存过期时间统一设为5分钟。一旦缓存过期,所有请求同时查数据库。更致命的是,如果商品不存在(比如被下架),每次查询都会穿透缓存直接打数据库。

漏洞三:支付回调接口的“雪崩效应”

支付成功后,回调接口需要更新订单状态、发送短信、推送消息、更新库存。这个接口依赖了5个外部服务。压测时,其中一个短信服务超时,导致整个接口的线程池被占满,其他请求全部排队等待。

性能瓶颈从来不是一个点,而是一条链。一个环节崩溃,整条链断裂。

03 优化实战:把系统从悬崖边拉回来

发现问题后,团队分三路同时优化。

优化一:库存扣减用“预扣+异步”模式

把同步扣库存改成两步:第一步,在Redis中预扣库存(原子操作)。第二步,异步写入数据库。

java
public boolean deductStock(Long productId, Integer qty) {
    // 使用Redis原子操作预扣库存
    Long remaining = redisTemplate.opsForValue()
        .decrement("stock:" + productId, qty);
    if (remaining < 0) {
        // 库存不足,回滚
        redisTemplate.opsForValue().increment("stock:" + productId, qty);
        return false;
    }
    // 异步写入数据库
    asyncStockService.updateDatabase(productId, qty);
    return true;
}

这个改动,把库存扣减的响应时间从12秒降到3ms。因为Redis的单线程模型天然支持原子操作,1000个并发请求在Redis层面就能完成扣减,只有写入数据库时才需要排队。

优化二:缓存策略从“统一过期”改成“热点+永不过期”

对热门商品,缓存永不过期,只通过后台任务定时更新。对普通商品,缓存时间随机化(300-600秒),避免同时过期。

java
public Product getProduct(Long id) {
    // 先从缓存取
    Product product = redisTemplate.opsForValue().get("product:" + id);
    if (product != null) {
        return product;
    }
    // 缓存未命中,查数据库
    product = productRepository.findById(id).orElse(null);
    if (product != null) {
        // 判断是否是热门商品
        if (hotProductService.isHot(id)) {
            // 热门商品缓存永不过期
            redisTemplate.opsForValue().set("product:" + id, product);
            // 后台任务定时更新
            hotProductService.scheduleUpdate(id);
        } else {
            // 普通商品随机过期时间
            int ttl = 300 + new Random().nextInt(300);
            redisTemplate.opsForValue().set("product:" + id, product, ttl, TimeUnit.SECONDS);
        }
    } else {
        // 防止缓存穿透:空值也缓存,过期时间短
        redisTemplate.opsForValue().set("product:" + id, null, 60, TimeUnit.SECONDS);
    }
    return product;
}

缓存命中率从30%提升到92%,数据库压力下降80%。

优化三:支付回调接口用“熔断+降级”

给每个外部服务设置超时时间和熔断阈值。如果某个服务连续失败5次,自动熔断,不再调用该服务,直接返回降级结果。

java
@HystrixCommand(
    fallbackMethod = "processPaymentFallback",
    commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"),
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")
    }
)
public void processPayment(Payment payment) {
    // 更新订单状态
    orderService.updateStatus(payment.getOrderId(), "PAID");
    // 发送短信(可能超时)
    smsService.sendNotification(payment.getUserId(), "支付成功");
    // 推送消息(可能超时)
    pushService.send(payment.getUserId(), "订单已支付");
}

这个改动,让支付回调接口的可用性从53%提升到99.9%。单个服务故障不再拖垮整个系统。

系统优化的本质,不是消除所有故障,而是让故障的影响范围最小化。

04 双11当天的验证:数据不会骗人

优化后的第二轮压测,数据亮眼。

5000并发用户,持续30分钟。库存扣减接口平均响应时间8ms,P99响应时间45ms。缓存命中率92%,数据库CPU稳定在30%。支付回调接口成功率99.8%。

但真正的考验是双11当天。

零点刚过,流量如潮水般涌入。监控面板上,并发数从1000瞬间飙到8000。所有人心都提到嗓子眼。但系统扛住了。库存扣减接口响应时间稳定在10ms以内,没有一次超时。支付回调接口在外部短信服务短暂故障时,自动熔断降级,用户没感受到任何异常。

当天最终数据:总订单量1200万单,峰值并发12000,系统可用性99.99%。没有崩盘,没有超卖,没有用户投诉。

但有一个细节值得记住:压测时我们模拟了5000并发,但真实流量是12000并发。为什么系统扛住了?因为真实用户的行为比压测脚本更“温和”——他们不会像脚本一样,在1秒内同时发出所有请求。真实用户有思考时间、有网络延迟、有操作间隔。

压测是给系统做“极限运动”,但真实用户不会天天跑马拉松。

双11结束后,团队复盘时发现一个更残酷的事实:我们优化的三个漏洞,其实在代码层面存在了半年。如果不是这次压测,它们会在双11当天集体爆发,导致至少10%的订单失败。

性能测试不是一次性的任务,而是持续的过程。每次代码提交、每次架构变更、每次流量增长,都需要重新压测。因为系统在变,用户行为在变,瓶颈也在变。

双11过去了,但下一个流量高峰永远不会迟到。你的系统,准备好了吗?

声明:本站部分稿件版权来源于网络,如有侵犯版权,请及时联系我们。

相关推荐

  • 电商网站性能测试实战:双11大促压力测试案例分析_千锋教育
  • Allure vs ExtentReports:测试报告框架体验对比_千锋教育
  • SonarQube vs Codacy:代码质量检测工具对比_千锋教育
  • 从0到1搭建实时通信测试:一个订单流失1.2亿,只因延迟慢了200毫秒_千锋教育
  • Web测试面试中的浏览器兼容性问题解析_千锋教育
  • Sentry监控端到端测试:一次错误捕获,团队效率提升87%_千锋教育