使用周期性线程池实现流量平滑,我将Redis并发从300+降到1

问题背景

在开发一个大型文件处理服务时,系统需要解析用户上传的文件包。在处理过程中,为了给前端提供实时进度反馈,我们设计了一个进度查询接口。

随后运维报告Redis的CPU占用率持续高于90%。通过本地监控工具排查,发现该进度查询接口的QPS(每秒查询数)高达300-400,这正是导致Redis压力过大的根本原因。

问题根因:高频轮询拖垮数据库

问题的核心在于进度查询的实现方式。处理任务开始后,前端通过频繁调用进度查询接口来获取最新进度。

java 复制代码
// 最初的问题实现
public int getProgress(String taskId) {
    // 每次调用都直接访问Redis
    return redisTemplate.opsForValue().get("progress:" + taskId);
}

这种实现导致在单个任务处理的数十秒内,会产生数百次Redis查询。当多个任务并发执行时,大量重复的查询请求完全淹没了Redis。

解决方案:内存缓存 + 定时同步

经过分析,我发现优化方案的核心在于将高频的Redis读取操作,转换为高效的内存读取。

1. 引入内存缓存作为"进度缓冲区"

首先,我在JVM内存中维护了一个全局的、线程安全的进度映射:

java 复制代码
@Component
public class ProgressManager {
    private static final ConcurrentHashMap<String, AtomicInteger> PROGRESS_MAP = 
        new ConcurrentHashMap<>();
    
    /**
     * 更新进度 - 原子操作保证线程安全
     */
    public void updateProgress(String taskId, int progress) {
        PROGRESS_MAP.computeIfAbsent(taskId, k -> new AtomicInteger(0))
                   .set(progress);
    }
    
    /**
     * 获取进度 - 内存读取,性能极高
     */
    public int getProgress(String taskId) {
        AtomicInteger progress = PROGRESS_MAP.get(taskId);
        return progress == null ? 0 : progress.get();
    }
    
    /**
     * 清理已完成任务的进度数据
     */
    public void removeProgress(String taskId) {
        PROGRESS_MAP.remove(taskId);
    }
}

2. 使用周期性线程池实现"定时同步"

接着,我创建了调度任务,以固定频率从Redis同步进度到内存缓冲区:

java 复制代码
@Service
public class ProgressSyncService {
    private final ScheduledExecutorService scheduler = 
        Executors.newSingleThreadScheduledExecutor();
    
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    
    @Autowired
    private ProgressManager progressManager;
    
    /**
     * 启动进度同步任务
     */
    public void startSyncProgress(String taskId) {
        // 关键优化:每秒仅从Redis同步一次进度
        scheduler.scheduleAtFixedRate(() -> {
            try {
                Integer remoteProgress = redisTemplate.opsForValue()
                                    .get("progress:" + taskId);
                if (remoteProgress != null) {
                    progressManager.updateProgress(taskId, remoteProgress);
                }
            } catch (Exception e) {
                // 异常处理,保证定时任务不被中断
                log.error("同步进度失败, taskId: {}", taskId, e);
            }
        }, 0, 1, TimeUnit.SECONDS); // 周期设置为1秒
    }
    
    /**
     * 停止同步任务
     */
    public void stopSyncProgress(String taskId) {
        // 清理资源
        progressManager.removeProgress(taskId);
    }
}

3. 改造查询接口

最后,对原有的查询接口进行改造:

java 复制代码
@RestController
public class ProgressController {
    
    @Autowired
    private ProgressManager progressManager;
    
    @GetMapping("/progress/{taskId}")
    public int getProgress(@PathVariable String taskId) {
        // 现在直接从内存读取,不再访问Redis
        return progressManager.getProgress(taskId);
    }
}

架构演进对比

优化前后的架构对比如下:

flowchart TD A[前端] --> B[进度查询接口] B --> C{查询来源} C -->|优化前| D[Redis
高频直接访问
QPS: 300+] C -->|优化后| E[内存缓冲区
AtomicInteger
响应: 微秒级] F[文件处理服务] --> G[Redis
写入进度
更新频率: 正常] H[定时同步任务] -->|每秒1次| G H -->|更新| E

优化效果

量化指标对比

指标 优化前 优化后 提升效果
Redis QPS 300-400 1 下降99.7%
Redis CPU占用 90%+ 恢复正常 系统稳定性提升
接口响应时间 网络IO耗时 内存读取耗时 提升百倍级
数据库连接数 高频占用 大幅减少 资源利用率提升

监控数据验证

通过监控系统可以明显看到优化效果:

  • Redis QPS图表:从持续的高峰变为平稳的低位直线
  • CPU使用率:从持续高位警报恢复到正常波动范围
  • 接口响应时间:P95从几十毫秒降低到亚毫秒级别

技术总结

成功关键因素

  1. 精准识别痛点
    区分了"进度更新的实时性需求"与"进度查询的频繁度控制"这两个不同维度的问题。
  2. 架构解耦设计
    通过引入内存缓冲区,将"进度读取"与"进度更新"两个关注点有效分离。
  3. 资源合理权衡
    用少量的JVM内存(ConcurrentHashMap)换取珍贵的Redis CPU资源和网络带宽。
  4. 经典模式应用
    ScheduledExecutorService + 原子变量的组合,构成了一个经典的生产者-消费者变种模式。

适用场景

这种"内存缓存 + 低频同步"的模式适用于:

  • 高频查询、低频更新的数据访问模式
  • 对实时性要求不极致的场景(秒级延迟可接受)
  • 需要减轻后端存储压力的高并发场景
  • 读多写少的业务数据缓存

注意事项

  1. 数据一致性:内存与Redis之间存在秒级延迟,不适合强一致性场景
  2. 内存管理:需要及时清理已完成任务的进度数据,避免内存泄漏
  3. 异常处理:保证Redis异常不影响内存缓存的基本功能
  4. 集群部署:在多实例部署时,需要保证进度查询路由到同一实例

写在最后

这次优化让我深刻体会到:有时候最有效的解决方案不是引入更复杂的技术,而是重新思考架构设计。通过简单的内存缓存和定时同步,我们用最小的代价解决了严重的性能问题。

这种思路可以扩展到很多类似的业务场景中,比如配置信息缓存、计数器缓存、状态查询等。关键在于识别出那些"高频读取但低频更新"的数据访问模式。

相关推荐
深圳蔓延科技3 小时前
单点登录到底是什么?
java·后端
道可到3 小时前
程序员养生十大违章:你中了几条?
前端·后端·面试
SimonKing3 小时前
除了 ${},Thymeleaf 的这些用法让你直呼内行
java·后端·程序员
间彧3 小时前
Java拦截器与过滤器的区别及生命周期分析
后端
XXX-X-XXJ4 小时前
二:RAG 的 “语义密码”:向量、嵌入模型与 Milvus 向量数据库实操
人工智能·git·后端·python·django·milvus
努力的白熊嗨4 小时前
多台服务器文件共享存储
服务器·后端
调试人生的显微镜4 小时前
CSS开发工具推荐与实战经验,让样式开发更高效、更精准
后端
渣哥4 小时前
多环境配置利器:@Profile 在 Spring 项目中的实战价值
javascript·后端·面试
东百牧码人4 小时前
还在使用ToList太Low了
后端