深入理解异步编程的精髓:一次线上事故带来的启示与架构优化探讨

在高并发的环境中,"异步" 是一种至关重要的优化策略。最近,生产环境中发生了一次事故,笔者认为这个场景非常具有 “典型性”。通过撰写这篇文章,我希望与大家分享该场景的架构优化方案,帮助大家更深刻地理解 “异步” 的内涵。

业务场景分析

在教研平台中,教师可以登录并查看课程列表,点击课程后,课程会以视频的形式展现出来。

图片访问课程详情页面时,包含两个核心动作:

  1. “读取课程视频信息”
    从缓存服务器 Redis 获取课程的视频信息,并将其返回给前端,前端通过视频组件进行渲染。
  2. “写入课程观看行为记录”
    教师在观看视频时,浏览器每隔 3 秒发起请求,教研服务将观看行为记录插入数据库表中。随着用户在线人数的增加,写操作的频率呈指数增长。

在系统上线的初期,这种设计运行良好,但随着在线用户数量的攀升,系统响应速度逐渐减慢,许多线程开始阻塞在写入视频观看进度表的 Dao 方法上。

首先,考虑到这一问题,我们可能会想到一个直接的解决方案,即**“提升写入数据库的能力”**:

  1. 优化 SQL 语句;
  2. 提升 MySQL 数据库的硬件配置;
  3. 实施分库分表。

尽管这些方案能够满足我们的需求,但通过扩容硬件的成本较高,而且写操作本身可以适当延迟并容许少量数据的丢失。因此,更具性价比的优化方向应当为:“减少写操作的耗时与提升写操作的并发性”。唯有如此,才能使系统更加流畅地运行。

因此,我们提出了第二种方案:“写请求异步化”

各种异步架构方案

线程池模式

2014 年,笔者在艺龙旅行网负责红包系统的相关工作。当运营系统调用红包系统向特定用户发送红包时,这些用户登录 app 后,app 端会调用红包系统的激活红包接口。

激活红包接口是一个写操作,速度较快(约 20 毫秒),日请求量可达 2000 万次。 在访问高峰期间,红包系统变得不稳定,激活接口经常超时。为了迅速解决这一问题,我采用了一个简单粗暴的方案:

“将写操作放入独立的线程池中,控制器立即返回响应,而线程池异步执行激活红包的方法。”

坦率而言,这是一个非常有效的方案,优化后红包系统稳定性显著提升。

回到教研场景,如下图所示,我们也可以设计一个类似线程池模式的方案:

图片采用线程池模式时,需要注意以下几点:

  1. 线程数不宜设置过高,避免占用过多的数据库连接池;
  2. 需评估线程池队列的大小,以防止内存溢出。

本地内存 + 定时任务

开源中国的浏览统计方案非常经典。用户访问一次文章、新闻或代码详情页面时,操作仅为将访问次数加 1。该操作为异步进行,访问时数据存储在内存中,定时将这些数据写入数据库。

图片示例代码如下:

图片我们可以借鉴开源中国的方案:

  1. 控制器接收请求后,将观看进度信息存储在本地内存的 LinkedBlockingQueue 对象中;
  2. 异步线程每隔 1 分钟从队列里获取数据,组装成 List 对象,最后调用 Jdbc batchUpdate 方法批量写入数据库;
  3. 批量写入的目的是为了提升整体系统的吞吐量,但每次批量写入的 List 大小也不宜过大。

此方案的优点在于不改变原有业务架构,简单易用且性能高。然而,同样需要注意内存溢出的风险。

MQ 模式

许多人会想到 MQ 模式,消息队列最核心的功能是**“异步”“解耦”**,MQ 模式的架构清晰且易于扩展。

图片核心流程如下:

  1. 控制器接收写请求,将观看视频行为记录转化为消息;
  2. 教研服务发送消息到 MQ,成功后将写操作信息返回给前端;
  3. 消费者服务从 MQ 中获取消息,进行批量数据库操作。

采用此方案的优点包括:

  1. MQ 本身支持高可用和异步,发送消息效率高,并支持批量消费;
  2. 消息在 MQ 服务端会持久化,可靠性高于保存在本地内存的方式。

不过,MQ 模式需要引入新的组件,增加了额外的复杂度。

Agent 服务 + MQ 模式

互联网大型企业还常用一种异步方案:Agent 服务 + MQ 模式。

图片在教研服务器上部署 Agent 服务(独立进程),教研服务接收写请求后,将请求以固定格式(如 JSON)写入本地磁盘,然后返回成功信息给前端。

Agent 服务会监听文件变化,将文件内容发送到消息队列,消费者服务获取观看行为记录并将其存储到 MySQL 数据库中。

若我们不希望在应用中依赖消息队列而生成本地文件,可采用如下方式:

图片

这种方案的最大优点是:架构分层清晰,业务服务无需引入 MQ 组件。

笔者曾接触过的性能监控平台或日志分析平台都采用了这种模式。

总结与反思

学习需要逐层深入的思考。

第一层:什么场景下需要异步?

  • 大量写操作占用了过多的资源,影响了系统的正常运行;
  • 写操作异步后,不影响主流程,允许适当延迟。

第二层:异步的外在应用

本文提到四种异步实现方式:

  • 线程池模式
  • 本地内存 + 定时任务
  • MQ 模式
  • Agent 服务 + MQ 模式

它们的共同特点是:将写操作命令存储在一个池中后,立即响应前端请求,从而减少写动作的耗时。任务服务异步从池中获取任务并执行。

第三层:异步的本质

在笔者看来,“异步是更细粒度地使用系统资源的一种方式。”

在教研课程详情场景中,数据库资源是固定的,但大量写操作占用了过多数据库资源,导致整体系统的阻塞。然而,写操作并不是核心业务流程,因而不应消耗过多的系统资源。

在追求异步的过程中,务必注意不应为了异步而异步。无论是采用线程池、本地内存 + 定时任务,还是 MQ,对数据库资源的使用都需保持在合理范围内,否则异步的效果可能并不理想。