高并发系统设计:从理论到实践 —— 一位后端工程师的亲身经历

独立开发小站
2025-06-14 20:48
阅读 667

引言:高并发是每个服务绕不过去的一道坎

引言:高并发是每个服务绕不过去的一道坎

作为一名在一线互联网公司工作的后端开发,我参与过多个从零到一的服务搭建项目。从初期的单体架构到后来的微服务拆分、再到如今的云原生部署,我深刻体会到:系统一旦上线并开始积累用户量,高并发问题就会像一座大山一样扑面而来。

我参与过的最典型的一个项目是一个面向C端用户的优惠券发放和核销平台,在一次营销活动中迎来了突发的流量高峰,瞬间QPS突破2000,数据库直接打满连接数,接口大量超时甚至报错,导致用户体验极差。

那次事件之后,我们团队花了整整三个月时间重构了整个服务的架构和关键路径设计。今天,我就想结合那次实际经历,分享我在高并发系统设计中踩过的坑、学到的经验以及最终落地的解决方案


一、项目背景与挑战

一、项目背景与挑战

背景介绍

这个优惠券平台主要支持以下核心功能:

  • 用户领取优惠券(GET)
  • 查询用户拥有的优惠券列表(GET)
  • 券使用下单时的核销验证(POST)

服务采用Spring Boot + MySQL + Redis的技术栈,一开始部署在一个单一的服务实例上,数据库也是单主库,整体结构简单清晰。

挑战浮现

某次促销活动期间,通过APP首页推荐入口引导大量用户同时点击“领取”按钮。短时间内,我们的服务突然出现了以下几个严重问题:

  1. MySQL连接池打满,报出TooManyConnections异常
  2. 大量请求堆积在线程池内,出现大面积超时(RT > 5s)
  3. 部分请求执行失败,日志中频繁出现Deadlock或Lock wait timeout exceeded错误
  4. Redis缓存击穿严重,缓存未命中导致回源数据库雪崩

当时系统状态如下图所示(节选监控截图):

QPS: 2000+ (平时仅几百)
JVM线程数:300+
GC停顿频率明显增加
DB CPU占用率超过90%

显然,这套原本设计得“能跑就行”的系统根本扛不住真实的业务压力。


二、解决问题:架构演进与性能调优实战

二、解决问题:架构演进与性能调优实战

为了应对这个问题,我们并没有马上去换语言或引入新的框架,而是以业务场景为出发点,一步步地做减法、做优化、做扩展

1. 分析瓶颈 —— 先看数据

我们在Prometheus和Grafana中搭建了完整的APM监控链路,收集了每个接口的调用耗时、数据库操作次数、锁等待时间等指标。

初步发现:

  • 所有慢查询集中在发券接口,每次执行都要写入数据库一条记录,并且要校验是否已经领取
  • 并发度高的情况下,SELECT ... FOR UPDATE语句引发死锁频繁
  • Redis缓存没有有效利用,缓存结构不合理,无法缓解热点访问

于是我们决定先从发券接口入手优化。


2. 发券逻辑优化设计

原始代码逻辑伪代码如下:

@Transactional
public void receiveCoupon(User user, Coupon coupon) {
    if (couponDao.isReceived(user.getId(), coupon.getId())) {
        throw new AlreadyReceivedException();
    }

    couponDao.issue(user.getId(), coupon.getId());
}

这是一段很典型的“查+写”事务逻辑。但在高并发下会出现以下问题:

  • 数据库行锁竞争激烈
  • 索引缺失导致查找效率低下
  • 多个并发线程争夺同一条记录,发生死锁概率陡增

解决方案

我们做了三方面优化:

✅ 使用Redis预检机制减少数据库交互

引入了一个布隆过滤器 + Redis Set缓存机制来判断是否已领取,减少对数据库的查询请求。

使用Redission实现了一个基于set的快速预判能力,Key格式为 coupon:userId:{userId}:couponId:{couponId}

✅ 使用数据库乐观锁控制并发

把原来悲观锁(SELECT FOR UPDATE)改为CAS更新模式,配合版本号或唯一约束。

举个例子:我们给每张券设置了每人只能领一张,设置一个唯一联合索引 (user_id, coupon_id),插入冲突时报已领即可。

✅ 异步持久化落盘

将部分非实时性的操作移至消息队列处理,例如通知用户、积分变更等。这样可以降低主线程IO负载。


3. 架构升级:服务拆分 + 资源隔离

随着业务进一步发展,我们决定对服务做微服务拆分:

  • 发券用户中心优惠券管理后台独立成不同的服务
  • 引入API Gateway统一鉴权及限流
  • 每个服务各自拥有对应的数据库实例,解决主库资源争抢问题

与此同时,我们引入了:

  • Sentinel限流组件,限制单位时间内请求数量,防止突发流量压垮系统
  • LVS+Nginx负载均衡实现服务多节点部署,提升横向扩容能力
  • Elasticsearch异步聚合报表数据,避免同步查询影响交易类接口性能

4. 缓存策略优化

为了避免缓存穿透、缓存击穿、缓存雪崩三大缓存灾难,我们做了如下调整:

问题 对应策略
缓存穿透 设置Bloom Filter拦截非法请求
缓存击穿 设置互斥锁(Mutex Lock)+ TTL自动续期
缓存雪崩 给不同key加随机TTL偏移

此外,我们还使用了Redis本地缓存(Caffeine)作为第一层缓存,只读高频字段比如用户基础信息、券详情信息,大大减少了网络往返次数。


三、代码实践:发券接口改造示例

三、代码实践:发券接口改造示例

下面展示一个简化版的发券接口实现:

@Autowired
private RedissonClient redisson;

@Autowired
private CouponRepository couponRepository;

@PostMapping("/receive")
public ResponseEntity<?> receiveCoupon(@RequestParam Long userId, @RequestParam Long couponId) {
    String cacheKey = "coupon:received:" + userId + ":" + couponId;

    // Step 1: Redis预检
    RSet<String> receivedSet = redisson.getSet(cacheKey);
    if (receivedSet.contains("1")) {
        return ResponseEntity.status(HttpStatus.CONFLICT).body("Already received");
    }

    // Step 2: DB尝试插入(唯一索引冲突则抛异常)
    try {
        couponRepository.issue(userId, couponId);
        receivedSet.add("1");
        receivedSet.expire(1, TimeUnit.DAYS); // 设置缓存有效期
        return ResponseEntity.ok().build();
    } catch (DataAccessException e) {
        if (isUniqueConstraintViolation(e)) {
            // 已存在该记录
            receivedSet.add("1");
            receivedSet.expire(1, TimeUnit.DAYS);
            return ResponseEntity.status(HttpStatus.CONFLICT).body("Already received");
        }
        log.error("Issue coupon failed", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

private boolean isUniqueConstraintViolation(Exception e) {
    // 根据具体数据库驱动判断是否为唯一键冲突
    return e.getMessage().contains("Duplicate entry");
}

四、踩坑经验总结

在实际开发过程中,我们遇到几个“看似不起眼,却让人头疼”的坑:

1. 锁粒度过粗导致性能下降

早期使用Synchronized或者Redis分布式锁时,误用了全局锁,结果导致并发度不升反降。

✅ 后期改为“细粒度”锁,按照userId + couponId做锁 key,效果显著。

2. 数据库自增ID性能瓶颈

当QPS过高时,表的自增ID生成速度成为瓶颈。特别是MySQL,如果插入压力过大,会导致InnoDB内部的自增锁等待。

✅ 最终使用雪花算法生成订单id + 分表策略缓解问题。

3. 忽略慢SQL的影响

某个后台统计接口全表扫描,导致主库CPU飙升。虽然后台接口不是交易核心,但会影响其他正常接口响应。

✅ 增加慢SQL监控,配合Explain分析,建立复合索引,从根本上解决问题。


五、效果对比与收益分析

经过优化改造后,我们将核心接口的平均响应时间从原来的800ms降至120ms以内,成功率从70%提升到了99.5%以上

指标 改造前 改造后
平均RT 800ms <120ms
成功率 ~70% 99.5%+
QPS峰值 2000+ 可稳定支撑4000+
数据库连接占用 经常爆满 稳定在安全水位
系统可扩展性 单节点不可扩 支持水平扩展

最关键的是:我们能够从容应对各种营销活动带来的瞬时高并发压力了。


六、我的经验分享与建议

如果你正在做或即将面临高并发系统的构建工作,这里有几点我认为非常重要的经验想与你分享:

1. 不要迷信某种技术或框架

我们曾经试图用Go语言重写服务,结果发现并不是性能瓶颈所在,最后改回Java也没问题。所以:

技术选择永远服务于业务诉求,而不是为了技术而技术。

2. 性能优化是一个渐进的过程

不要妄想着一次性“一步到位”,而是根据流量变化逐步迭代。先保证系统能跑起来,再考虑怎么跑得更快、更稳。

3. 监控是你的眼睛,日志是你的心脏

如果没有完善的监控体系,那高并发就像开车闭着眼睛上路。一定要尽早引入APM工具,如SkyWalking、Pinpoint等,方便定位问题。

4. 合理使用缓存,但别过度依赖缓存

缓存是双刃剑,用不好会反过来伤害系统。合理使用本地缓存+Caffeine、Redis集群、热点自动降级策略,才能发挥它的威力。

5. 线上问题优先于理论推导

真实环境永远比理论复杂得多。当你面对线上OOM、CPU飙红、数据库死锁的时候,书上的“最佳实践”往往并不适用。这时候,靠的是经验和冷静。


结语:高并发不是终点,而是不断演化的起点

这次优惠券系统的高并发问题只是我工作中的一次缩影,但它让我更加坚定一个理念:

“真正的系统设计,永远来自于对真实场景的深刻理解和持续打磨。”

每一次性能优化,不只是代码层面的改进,更是对业务本质的理解;每一个架构演进,不只是技术层面的重构,更是对未来不确定性的准备。

希望这篇文章能带给你一些启发,少走一些弯路。如果你也经历过类似的问题,欢迎留言交流。咱们一起成长,一起变得更强。

💪

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝