深入理解异步编程的精髓:一次线上事故带来的启示与架构优化探讨
在高并发的环境中,"异步" 是一种至关重要的优化策略。最近,生产环境中发生了一次事故,笔者认为这个场景非常具有 “典型性”。通过撰写这篇文章,我希望与大家分享该场景的架构优化方案,帮助大家更深刻地理解 “异步” 的内涵。
业务场景分析
在教研平台中,教师可以登录并查看课程列表,点击课程后,课程会以视频的形式展现出来。
访问课程详情页面时,包含两个核心动作:
- “读取课程视频信息”:
从缓存服务器 Redis 获取课程的视频信息,并将其返回给前端,前端通过视频组件进行渲染。 - “写入课程观看行为记录”:
教师在观看视频时,浏览器每隔 3 秒发起请求,教研服务将观看行为记录插入数据库表中。随着用户在线人数的增加,写操作的频率呈指数增长。
在系统上线的初期,这种设计运行良好,但随着在线用户数量的攀升,系统响应速度逐渐减慢,许多线程开始阻塞在写入视频观看进度表的 Dao 方法上。
首先,考虑到这一问题,我们可能会想到一个直接的解决方案,即**“提升写入数据库的能力”**:
- 优化 SQL 语句;
- 提升 MySQL 数据库的硬件配置;
- 实施分库分表。
尽管这些方案能够满足我们的需求,但通过扩容硬件的成本较高,而且写操作本身可以适当延迟并容许少量数据的丢失。因此,更具性价比的优化方向应当为:“减少写操作的耗时与提升写操作的并发性”。唯有如此,才能使系统更加流畅地运行。
因此,我们提出了第二种方案:“写请求异步化”。
各种异步架构方案
线程池模式
2014 年,笔者在艺龙旅行网负责红包系统的相关工作。当运营系统调用红包系统向特定用户发送红包时,这些用户登录 app 后,app 端会调用红包系统的激活红包接口。
激活红包接口是一个写操作,速度较快(约 20 毫秒),日请求量可达 2000 万次。 在访问高峰期间,红包系统变得不稳定,激活接口经常超时。为了迅速解决这一问题,我采用了一个简单粗暴的方案:
“将写操作放入独立的线程池中,控制器立即返回响应,而线程池异步执行激活红包的方法。”
坦率而言,这是一个非常有效的方案,优化后红包系统稳定性显著提升。
回到教研场景,如下图所示,我们也可以设计一个类似线程池模式的方案:
采用线程池模式时,需要注意以下几点:
- 线程数不宜设置过高,避免占用过多的数据库连接池;
- 需评估线程池队列的大小,以防止内存溢出。
本地内存 + 定时任务
开源中国的浏览统计方案非常经典。用户访问一次文章、新闻或代码详情页面时,操作仅为将访问次数加 1。该操作为异步进行,访问时数据存储在内存中,定时将这些数据写入数据库。
示例代码如下:
我们可以借鉴开源中国的方案:
- 控制器接收请求后,将观看进度信息存储在本地内存的 LinkedBlockingQueue 对象中;
- 异步线程每隔 1 分钟从队列里获取数据,组装成 List 对象,最后调用 Jdbc batchUpdate 方法批量写入数据库;
- 批量写入的目的是为了提升整体系统的吞吐量,但每次批量写入的 List 大小也不宜过大。
此方案的优点在于不改变原有业务架构,简单易用且性能高。然而,同样需要注意内存溢出的风险。
MQ 模式
许多人会想到 MQ 模式,消息队列最核心的功能是**“异步”和“解耦”**,MQ 模式的架构清晰且易于扩展。
核心流程如下:
- 控制器接收写请求,将观看视频行为记录转化为消息;
- 教研服务发送消息到 MQ,成功后将写操作信息返回给前端;
- 消费者服务从 MQ 中获取消息,进行批量数据库操作。
采用此方案的优点包括:
- MQ 本身支持高可用和异步,发送消息效率高,并支持批量消费;
- 消息在 MQ 服务端会持久化,可靠性高于保存在本地内存的方式。
不过,MQ 模式需要引入新的组件,增加了额外的复杂度。
Agent 服务 + MQ 模式
互联网大型企业还常用一种异步方案:Agent 服务 + MQ 模式。
在教研服务器上部署 Agent 服务(独立进程),教研服务接收写请求后,将请求以固定格式(如 JSON)写入本地磁盘,然后返回成功信息给前端。
Agent 服务会监听文件变化,将文件内容发送到消息队列,消费者服务获取观看行为记录并将其存储到 MySQL 数据库中。
若我们不希望在应用中依赖消息队列而生成本地文件,可采用如下方式:
这种方案的最大优点是:架构分层清晰,业务服务无需引入 MQ 组件。
笔者曾接触过的性能监控平台或日志分析平台都采用了这种模式。
总结与反思
学习需要逐层深入的思考。
第一层:什么场景下需要异步?
- 大量写操作占用了过多的资源,影响了系统的正常运行;
- 写操作异步后,不影响主流程,允许适当延迟。
第二层:异步的外在应用
本文提到四种异步实现方式:
- 线程池模式
- 本地内存 + 定时任务
- MQ 模式
- Agent 服务 + MQ 模式
它们的共同特点是:将写操作命令存储在一个池中后,立即响应前端请求,从而减少写动作的耗时。任务服务异步从池中获取任务并执行。
第三层:异步的本质
在笔者看来,“异步是更细粒度地使用系统资源的一种方式。”
在教研课程详情场景中,数据库资源是固定的,但大量写操作占用了过多数据库资源,导致整体系统的阻塞。然而,写操作并不是核心业务流程,因而不应消耗过多的系统资源。
在追求异步的过程中,务必注意不应为了异步而异步。无论是采用线程池、本地内存 + 定时任务,还是 MQ,对数据库资源的使用都需保持在合理范围内,否则异步的效果可能并不理想。