尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

缓存与消息队列:高并发系统设计核心技术与面试要点

缓存与消息队列:高并发系统设计核心技术与面试要点 1. 为什么缓存与消息队列是架构师面试的必考题在Java技术栈的中高级面试中缓存和消息队列这两个技术点出现的频率高得惊人。我作为面试官时发现90%的候选人在被问到如何设计一个高并发订单系统时第一反应都是加Redis缓存和引入MQ。但真正能说清楚技术选型依据、适用场景边界和落地细节的可能不到30%。去年我面试过一个P7候选人当被问到本地缓存Caffeine和Redis如何配合使用时他给出了教科书式的分层缓存方案。但当我继续追问缓存穿透时本地缓存会有什么连锁反应他却开始支支吾吾。这暴露了一个典型问题很多开发者只记住了技术组合的形却没理解背后的神。2. 缓存技术从基础使用到高阶治理2.1 缓存选型的三个维度考量当面试官问为什么用Redis而不用Memcached时他们期待的不是背诵对比表格而是业务场景的适配分析。我总结的选型三维度数据特性维度结构化数据如用户画像→ Redis Hash二进制数据如图片→ Memcached热点数据→ 本地缓存Caffeine/Guava访问模式维度随机读取→ Redis/Memcached批量扫描→ Redis ZSET多级缓存→ CaffeineRedis组合一致性要求维度强一致→ 考虑Write-Through模式最终一致→ 异步刷新过期策略实际案例在电商库存系统中我们采用Redis集群本地缓存的二级方案。关键技巧是给本地缓存设置不同的TTL抖动基础30秒±随机5秒避免缓存雪崩。2.2 缓存一致性的实战解法教科书常讲的先更新数据库再删除缓存在分布式环境下会遭遇这些坑并发写导致脏数据删除失败导致长期不一致集群环境下缓存漂移我们团队通过以下方案解决// 基于版本号的乐观锁方案 public boolean updateProduct(Product product) { // 获取当前版本 long currentVersion redisTemplate.opsForValue().get(product:product.id:version); if(product.version currentVersion) { throw new OptimisticLockException(); } // 更新数据库 int affected productMapper.update(product); if(affected 0) { // 双删策略 redisTemplate.delete(product:product.id); Thread.sleep(100); redisTemplate.delete(product:product.id); // 更新版本号 redisTemplate.opsForValue().set(product:product.id:version, currentVersion1); } return affected 0; }2.3 缓存治理的进阶技巧当缓存集群达到TB级别时会遇到这些教科书不会教的问题热点Key探测我们开发了基于滑动窗口的实时监控当某个Key的QPS超过阈值时自动触发本地缓存大Key拆分将1MB以上的商品详情拆分为多个Hash字段通过Lua脚本保证原子性缓存预热基于Flink实时分析查询日志预测次日热点数据3. 消息队列不只是解耦工具3.1 消息模型的选择陷阱很多候选人能说出Kafka和RocketMQ的区别但当我问为什么订单系统用RocketMQ而不用Kafka时高质量的回答应该包含这些要点对比维度KafkaRocketMQ延迟毫秒级微秒级事务消息不支持完整支持消息回溯固定时间窗口任意时间点消费模式纯PullPullPush混合真实案例在支付系统中我们选择RocketMQ关键原因是其事务消息机制可以确保本地事务与消息发送的原子性定时消息支持精确到秒级的延迟消息重试策略可定制不同于Kafka的固定间隔3.2 消息积压的应急处理去年大促时我们的订单MQ积压了200万条通过以下步骤快速定位监控指标分析消费延迟从正常50ms飙升到2s线程池状态活跃线程占满瓶颈定位# 查看消费端JVM jstack pid | grep -A 10 ConsumerThread # 监控RocketMQ Broker mqadmin consumerStatus -n namesrv:9876 -g order_consumer_group应急方案动态扩容Consumer实例K8s快速扩容降级非核心业务如关闭日志记录修改消费批次大小从10条改为100条3.3 消息幂等的五种实现方式在分布式环境下消息重复消费是必然事件。我整理的实际项目中的解决方案唯一索引法CREATE TABLE message_idempotent ( biz_id VARCHAR(64) PRIMARY KEY, status TINYINT );Redis原子操作Boolean result redisTemplate.opsForValue() .setIfAbsent(msg:messageId, 1, 1, TimeUnit.HOURS);乐观锁UPDATE account SET balancebalance-100, versionversion1 WHERE user_id123 AND versioncurrent_version;状态机校验if(order.getStatus() ! OrderStatus.NEW){ log.warn(重复消息:{}, message); return; }去重表本地缓存结合Guava Cache做短时间窗口去重4. 系统设计中的组合拳4.1 缓存与消息队列的协同模式在秒杀系统中我们是这样协同使用两种技术的库存预热活动前1小时通过消息队列异步加载数据到Redis// 生产者 rocketMQTemplate.send(preheat_topic, MessageBuilder.withPayload(skuId).build()); // 消费者 RocketMQMessageListener(topicpreheat_topic, consumerGrouppreheat_group) public void preheatConsumer(String skuId) { int stock stockService.getDBStock(skuId); redisTemplate.opsForValue().set(stock:skuId, stock); }实时同步数据库变更后通过BinlogMQ更新缓存最终一致性通过定时任务补偿MQ处理失败的数据4.2 面试中的高频问题拆解当被问到如何保证缓存和数据库的一致性时建议分层次回答基础方案Cache Aside Pattern进阶方案延迟双删基于Binlog的异步刷新特殊场景金融级强一致使用Redisson的WriteThrough模式海量数据场景BloomFilter防穿透4.3 性能优化的黄金组合在某个日活千万的社交APP中我们通过以下组合将接口RT从200ms降到50ms多级缓存本地缓存Caffeine10万条用户基础信息分布式缓存Redis Cluster全量用户数据持久层缓存MyBatis二级缓存冷数据消息队列削峰写操作异步化如点赞记录批量合并如用户行为日志关键配置caffeine: spec: maximumSize100000,expireAfterWrite5m rocketmq: producer: group: social_group sendMessageTimeout: 3000 consumer: pullBatchSize: 325. 避坑指南那些年我们踩过的坑5.1 Redis大集群的慢查询风暴现象某次大促期间Redis CPU飙升至90%但QPS没有明显增长 根因分析使用KEYS命令扫描匹配模式的Key开发环境遗留代码某个Hash结构的Field达到10万 解决方案替换KEYS为SCAN命令对大Hash进行分片存储增加慢查询监控# redis-cli CONFIG SET slowlog-log-slower-than 10000 SLOWLOG GET 105.2 RocketMQ消息丢失的诡异事件现象订单完成消息偶尔丢失概率约0.1% 排查过程检查Broker磁盘IO正常检查Consumer Ack发现部分消息消费时间超过15分钟默认超时时间最终定位某个商品详情查询的HTTP请求没有设置超时 修复方案// 消费端代码改造 RocketMQMessageListener( topic order_complete, consumerGroup order_group, consumeTimeout 30 // 分钟 ) public class OrderConsumer implements RocketMQListenerString { Override public void onMessage(String message) { // 所有外部调用必须设置超时 HttpUtil.get(http://product-service/detail, 5000); } }5.3 本地缓存的GC危机现象某次全量发布后应用出现频繁Full GC 分析过程MAT分析堆dump发现Caffeine缓存占用了2GB内存代码检查没有设置大小限制且缓存了可变对象 解决方案Caffeine.newBuilder() .maximumSize(10000) // 必须设置上限 .weakKeys() // 防止内存泄漏 .removalListener((key, value, cause) - log.debug(缓存移除{}, key)) .build();在技术架构的道路上缓存和消息队列就像武侠中的内功心法表面看起来都是存数据和发消息但真正的功力体现在对细节的把控。建议每个Java开发者都亲自实现一个简易版的Redis和MQ哪怕只有核心功能这会让你在面试中展现出与众不同的深度。
返回列表