跳到正文
Joeplover
学习笔记·2026-07-11·约 6 分钟阅读

异步、并发、MQ、Redis:一次畅聊的技术笔记

从 JavaScript 异步为什么是原生的聊到 Java 为什么需要 MQ,从 CPU 大小核到并发并行的区别,从 RabbitMQ 和 Kafka 的设计哲学到 Redis、Redisson、RedisTemplate 的关系,最后到缓存三大经典难题——记录一次从浅到深的完整对话。

异步、并发、MQ、Redis:一次畅聊的技术笔记

Q1:JavaScript 异步是原生的,Java 为什么需要 MQ?

开始之前我问了一个问题:JS 原生支持 async/await,FastAPI 也是,但 Spring Boot 要异步就得用 MQ,为什么?

先看运行时模型。

JavaScript 和 Python asyncio 是单线程 Event Loop 模式。只有一个线程在处理所有请求,如果有一个请求在等数据库,这个线程不能干等着——它必须切去干别的请求,等数据库回来了再切回来。所以 async/await 是呼吸一样的本能,不异步就卡死全世界。

Java 不一样。Tomcat 默认维护一个线程池,来一个请求分一个线程。这个线程可以安心地阻塞等数据库,因为别的请求有其他线程在处理。Java 的设计哲学是:我有的是线程,你阻塞你的,不影响别人。

所以结论是:JS/Python 的异步是运行时设计出来的,Java 的同步是多线程模型设计出来的,没有谁对谁错,是历史路径不同。

那 Spring Boot 想异步怎么办?两个层面:

  1. I/O 异步(非阻塞 IO)——可以用 WebFlux,但代价很大。JDBC 本身是阻塞的,要用 R2DBC,现有 MyBatis 代码全废,项目一般不用。
  2. 任务异步(耗时操作丢给后台)——简单场景用 @Async,正式场景上 MQ。因为 Tomcat 线程池有限,占满了就拒绝服务,所以把耗时任务丢给 MQ 让另一个进程处理,既是解耦也是保命。

Q2:CPU 核心数和大小核是什么

核心数 = 真正能同时干活的数量。4 核就能同时干 4 件事,但线程数可以远大于核心数——线程是任务描述,核心是执行者,多个线程在少量核心上时分复用。

大小核(big.LITTLE)是 ARM 先搞的,Intel 后来也抄了(P 核 + E 核):

  • P 核(性能核):跑得快但耗电,像法拉利
  • E 核(能效核):跑得慢但省电,像自行车

操作系统调度器会自动分配。看书刷网页时用 E 核省电,玩游戏编译代码时唤醒 P 核。但有个坑:你的 Java 线程如果被调度到 E 核上,同样的代码可能比 P 核慢两三倍。延迟敏感的任务(高频交易、实时音频)需要绑核(CPU affinity)。

Q3:并发和并行,哪个是切着跑哪个是一起跑?

我一开始搞反了,被纠正了。

并发(Concurrency) = 切着跑,逻辑上同时,单核也能做到。 并行(Parallelism) = 一起跑,物理上同时,必须多核。

多线程本身是并发的手段,只有在多核 CPU 上运行时才顺便并行。

经典记忆法(Go 之父 Rob Pike):并发是结构设计,并行是执行结果。

并发但不并行:你一个人做三件事,先煮水,等水开的空档切菜,切完菜水开了继续煮。 并行且并发:你煮水,女朋友切菜——两个人同时干不同的活。

Q4:RabbitMQ vs Kafka 深入对比

这两个虽然都叫消息队列,但设计哲学完全不同。

RabbitMQ 像邮局:你寄信(生产消息),邮局保管,收信人取走(消费消息)。一封信只能给一个人。消息确认消费后就从队列里删掉。

Kafka 像报社:报纸印出来(写日志),谁订阅谁看。同一份报纸可以给成千上万人。消息不会消失,保留一段时间(默认 7 天),谁想重读都可以。

核心架构区别:

RabbitMQ 是 Producer → Exchange(按路由规则) → Binding → Queue → Consumer。支持 direct/topic/fanout 多种路由,每个消息只给一个消费者,消息确认机制确保不丢。

Kafka 是 Producer → Topic(分区日志,追加写不可变) → Consumer Group(各读各的分区,用 offset 记录读到哪了)。消费者可以独立调整 offset 重读历史消息。

为什么 Kafka 这么快?顺序写磁盘。消息追加到文件末尾,磁盘顺序写和内存差不多快,机械硬盘也能跑百万条/秒。

什么时候选谁:

  • 任务分发(一个任务一个人处理)、需要灵活路由、需要消息确认和重试、延迟消息 → RabbitMQ
  • 日志收集(百万级埋点)、多个消费者各自消费相同数据、数据管道(binlog → Kafka → ES)、流处理 → Kafka

Q5:Routing Key 和 Binding Key 的区别

这两个最容易搞混。

Routing Key = 生产者贴在消息上的标签,描述"这条消息是什么"。 Binding Key = 消费者贴在队列和 Exchange 之间的规则,描述"这个队列想要什么"。

用邮局比喻:Routing Key 是包裹上的目的地地址,Binding Key 是你家门口贴的"我只收什么包裹"。邮局(Exchange)看你包裹上的地址(routing key),查你家门口贴的规则(binding key),对上就放进你家信箱(Queue)。

在 Direct Exchange 中 routing key 和 binding key 是精确匹配的同一个值,最容易混淆。到了 Topic Exchange 就明显了——生产者写 "order.create",消费者绑 "order.#",通配符匹配,一看就是两个东西。

Q6:Redis 和 Redisson 是什么关系

Redis 是一个 C 语言写的内存数据库。数据存在内存里,读写不到 1 毫秒,比 MySQL 快几十倍。最常见的用途是给 MySQL 当缓存。

Redis 有五种基本数据结构:字符串(String)、列表(List)、哈希(Hash)、集合(Set)、有序集合(Sorted Set)。你把它理解成一个远程的、能共享的 HashMap就够用了。

Redisson 不是 Redis 的变种,它是一个 Java 客户端工具包。运行在你的 Java 应用里,把 Redis 的原始命令包装成 Java 程序员熟悉的对象和方法。

其实就是三层的封装关系:Redis(服务器) ← Jedis/Lettuce(通信客户端) ← Redisson(高级封装工具包)。Redisson 的招牌功能是分布式锁——一行 lock() 就能在多台服务器之间互斥,自动处理续期、重入、等待,比自己拼 SETNX 命令靠谱得多。

Q7:RedisTemplate 是什么

RedisTemplate 是 Spring 对 Redis 客户端的封装。

没有 RedisTemplate 的时候,你要用 Jedis 写 jedis.set("name", "张三")——得记 Redis 命令。有了 RedisTemplate,你写 redisTemplate.opsForValue().set("name", "张三")——用 Java 的习惯操作 Redis。

它还帮你做了连接池管理、异常转换、Java 对象序列化(自动转 JSON)、Spring 事务整合这些脏活累活。

RedisTemplate 和 Redisson 的关系是:前者偏"存数据取数据"(缓存 90% 的场景),后者偏"分布式锁、分布式队列"(高级功能)。很多项目两个一起用。

Q8:缓存三大经典难题

缓存穿透:查一个根本不存在的 id。每次 Redis 没命中,都穿透到 MySQL。恶意攻击者循环查 id=-1 就能把 MySQL 打垮。

解决:查不到也在 Redis 里存个空值(短期过期),或者用布隆过滤器提前拦截非法 id。

缓存击穿:一个热点 key 刚好过期,大量并发同时来。比如首页头条每秒被访问一万次,缓存过期那一瞬间所有请求都冲到 MySQL。

解决:互斥锁——一万个请求同时来,只有一个人去查 MySQL,其他人等着然后从缓存取。或者设计为永不过期 + 异步刷新。

缓存雪崩:大量 key 同时过期,或者 Redis 整个挂了,所有请求打到 MySQL。

解决:过期时间加随机值(3600~4200 秒之间随机分布,不要全设 3600),多级缓存(本地缓存 Caffeine + Redis),以及 Redis 高可用(主从+哨兵或 Cluster)。

三个口诀:穿透是没有这个数据,击穿是有但刚好过期,雪崩是大片过期或全挂。