黑马点评-Redis 消息队列-03_stream_consumer_group

黑马点评 Redis 消息队列三:Stream 和消费者组到底强在哪里?

本文继续整理黑马点评 Redis 实战篇第 7 章「Redis 消息队列」。

前两篇分别讲了为什么需要消息队列,以及 Redis List、PubSub 为什么不够。

这一篇进入第 7 章核心:Redis Stream、消费者组、ACK、pending-list 到底在解决什么问题。


1. 这篇文章解决什么问题

Stream 这一节很容易学成"背命令":

text 复制代码
XADD
XREAD
XGROUP CREATE
XREADGROUP
XACK

但如果只背命令,很快会忘。

真正要理解的是:

text 复制代码
每个命令到底补上了消息队列的哪块能力。

先给结论:

Redis Stream 是 Redis 5.0 引入的消息流数据结构。它能保存消息、给消息生成唯一 ID、支持阻塞读取、支持消费者组让多个消费者分工消费、支持 ACK 确认消息,并用 pending-list 记录已经投递但还没确认的消息,从而比 List 和 PubSub 更适合异步秒杀下单。


2. Stream 是什么

Stream 可以理解成:

text 复制代码
Redis 里的消息日志队列。

它保存一条条消息。

每条消息都有:

text 复制代码
1. 消息 ID
2. 消息内容,也就是多个 field-value

比如秒杀订单消息可以长这样:

text 复制代码
stream.orders
  1700000000000-0
    userId    101
    voucherId 10
    id        123456789

这里:

text 复制代码
stream.orders 是队列名称
1700000000000-0 是消息 ID
userId、voucherId、id 是消息字段

Stream 消息结构

#mermaid-svg-ty4oO40U4e9kYKfB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ty4oO40U4e9kYKfB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ty4oO40U4e9kYKfB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ty4oO40U4e9kYKfB .error-icon{fill:#552222;}#mermaid-svg-ty4oO40U4e9kYKfB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ty4oO40U4e9kYKfB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ty4oO40U4e9kYKfB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ty4oO40U4e9kYKfB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ty4oO40U4e9kYKfB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ty4oO40U4e9kYKfB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ty4oO40U4e9kYKfB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ty4oO40U4e9kYKfB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ty4oO40U4e9kYKfB .marker.cross{stroke:#333333;}#mermaid-svg-ty4oO40U4e9kYKfB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ty4oO40U4e9kYKfB p{margin:0;}#mermaid-svg-ty4oO40U4e9kYKfB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ty4oO40U4e9kYKfB .cluster-label text{fill:#333;}#mermaid-svg-ty4oO40U4e9kYKfB .cluster-label span{color:#333;}#mermaid-svg-ty4oO40U4e9kYKfB .cluster-label span p{background-color:transparent;}#mermaid-svg-ty4oO40U4e9kYKfB .label text,#mermaid-svg-ty4oO40U4e9kYKfB span{fill:#333;color:#333;}#mermaid-svg-ty4oO40U4e9kYKfB .node rect,#mermaid-svg-ty4oO40U4e9kYKfB .node circle,#mermaid-svg-ty4oO40U4e9kYKfB .node ellipse,#mermaid-svg-ty4oO40U4e9kYKfB .node polygon,#mermaid-svg-ty4oO40U4e9kYKfB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ty4oO40U4e9kYKfB .rough-node .label text,#mermaid-svg-ty4oO40U4e9kYKfB .node .label text,#mermaid-svg-ty4oO40U4e9kYKfB .image-shape .label,#mermaid-svg-ty4oO40U4e9kYKfB .icon-shape .label{text-anchor:middle;}#mermaid-svg-ty4oO40U4e9kYKfB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ty4oO40U4e9kYKfB .rough-node .label,#mermaid-svg-ty4oO40U4e9kYKfB .node .label,#mermaid-svg-ty4oO40U4e9kYKfB .image-shape .label,#mermaid-svg-ty4oO40U4e9kYKfB .icon-shape .label{text-align:center;}#mermaid-svg-ty4oO40U4e9kYKfB .node.clickable{cursor:pointer;}#mermaid-svg-ty4oO40U4e9kYKfB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ty4oO40U4e9kYKfB .arrowheadPath{fill:#333333;}#mermaid-svg-ty4oO40U4e9kYKfB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ty4oO40U4e9kYKfB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ty4oO40U4e9kYKfB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ty4oO40U4e9kYKfB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ty4oO40U4e9kYKfB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ty4oO40U4e9kYKfB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ty4oO40U4e9kYKfB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ty4oO40U4e9kYKfB .cluster text{fill:#333;}#mermaid-svg-ty4oO40U4e9kYKfB .cluster span{color:#333;}#mermaid-svg-ty4oO40U4e9kYKfB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ty4oO40U4e9kYKfB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ty4oO40U4e9kYKfB rect.text{fill:none;stroke-width:0;}#mermaid-svg-ty4oO40U4e9kYKfB .icon-shape,#mermaid-svg-ty4oO40U4e9kYKfB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ty4oO40U4e9kYKfB .icon-shape p,#mermaid-svg-ty4oO40U4e9kYKfB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ty4oO40U4e9kYKfB .icon-shape .label rect,#mermaid-svg-ty4oO40U4e9kYKfB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ty4oO40U4e9kYKfB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ty4oO40U4e9kYKfB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ty4oO40U4e9kYKfB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} stream.orders
消息ID: 1700000000000-0
userId: 101
voucherId: 10
id: 123456789

和 List 相比,Stream 更像"消息记录表"。

消息不是简单地一弹出就没了。

它会保留消息 ID 和内容,这就为消息回溯、确认、异常恢复提供了基础。


3. XADD:向 Stream 发送消息

讲义中介绍的发送消息命令是:

redis 复制代码
XADD key ID field value [field value ...]

在秒杀业务里,Lua 脚本最后会执行:

lua 复制代码
redis.call(
    'xadd',
    'stream.orders',
    '*',
    'userId', userId,
    'voucherId', voucherId,
    'id', orderId
)

它相当于执行:

redis 复制代码
XADD stream.orders * userId 101 voucherId 10 id 123456789

这里每个参数都很重要。

key 是什么

text 复制代码
stream.orders

这是 Stream 队列名称。

意思是:

text 复制代码
所有秒杀订单消息都写到 stream.orders 这个队列里。

ID 是什么

text 复制代码
*

* 表示让 Redis 自动生成消息 ID。

Redis 生成的 ID 通常类似:

text 复制代码
时间戳-序号

比如:

text 复制代码
1700000000000-0

我们不用自己算这个 ID。

field-value 是什么

text 复制代码
userId 101
voucherId 10
id 123456789

这些就是订单任务需要的业务数据。

后台消费者拿到消息后,可以把它转成 VoucherOrder


4. XREAD:普通读取消息

Stream 可以用 XREAD 读取消息:

redis 复制代码
XREAD COUNT 1 STREAMS stream.orders 0

可以理解为:

text 复制代码
从 stream.orders 读取消息。
从指定 ID 后面开始读。
最多读 1 条。

它也支持阻塞读取:

redis 复制代码
XREAD COUNT 1 BLOCK 2000 STREAMS stream.orders $

这里:

text 复制代码
BLOCK 2000:没有消息时最多阻塞等待 2000 毫秒
$:读取最新消息

阻塞读取很好理解。

它和 BRPOPBlockingQueue.take() 的思想类似:

text 复制代码
没消息时不要空转,等一会儿。

5. 为什么 XREAD 用 $ 有漏读风险

讲义里特别提醒:

text 复制代码
当指定起始 ID 为 $ 时,代表读取最新消息。
如果处理一条消息过程中,又有超过 1 条消息到达队列,
下次获取时也只能获取到最新的一条,会出现漏读消息的问题。

这个点刚学时很容易绕。

我们用一个具体例子讲。

消费者第一次执行:

redis 复制代码
XREAD COUNT 1 BLOCK 2000 STREAMS stream.orders $

它读到了消息 A。

然后消费者开始处理 A。

处理 A 的过程中,队列里又来了:

text 复制代码
消息 B
消息 C
消息 D

处理完 A 后,消费者又用 $ 读取:

text 复制代码
$ 表示从最新位置开始等新消息

这样它可能会跳过已经进入队列的 B、C、D,只等待后续更新的消息。

这就是漏读风险。

所以普通 XREAD 可以用于理解 Stream,但不适合作为最终秒杀订单消费方案。

最终方案要用消费者组。


6. 消费者组解决什么问题

消费者组是 Stream 的核心能力。

它解决几个问题:

text 复制代码
1. 多个消费者可以分工处理同一个队列。
2. 每条消息只会分配给组内某个消费者处理。
3. 消息投递后,如果没有 ACK,会进入 pending-list。
4. 失败消息可以从 pending-list 中重新读取处理。

可以把消费者组想成:

text 复制代码
一个订单处理小组。

小组里有多个消费者:

text 复制代码
c1
c2
c3

它们共同消费:

text 复制代码
stream.orders

Redis 会把消息分配给组内消费者。

消费者组模型

#mermaid-svg-zqcUBbeXRkJiVAIG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zqcUBbeXRkJiVAIG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zqcUBbeXRkJiVAIG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zqcUBbeXRkJiVAIG .error-icon{fill:#552222;}#mermaid-svg-zqcUBbeXRkJiVAIG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zqcUBbeXRkJiVAIG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zqcUBbeXRkJiVAIG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zqcUBbeXRkJiVAIG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zqcUBbeXRkJiVAIG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zqcUBbeXRkJiVAIG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zqcUBbeXRkJiVAIG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zqcUBbeXRkJiVAIG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zqcUBbeXRkJiVAIG .marker.cross{stroke:#333333;}#mermaid-svg-zqcUBbeXRkJiVAIG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zqcUBbeXRkJiVAIG p{margin:0;}#mermaid-svg-zqcUBbeXRkJiVAIG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zqcUBbeXRkJiVAIG .cluster-label text{fill:#333;}#mermaid-svg-zqcUBbeXRkJiVAIG .cluster-label span{color:#333;}#mermaid-svg-zqcUBbeXRkJiVAIG .cluster-label span p{background-color:transparent;}#mermaid-svg-zqcUBbeXRkJiVAIG .label text,#mermaid-svg-zqcUBbeXRkJiVAIG span{fill:#333;color:#333;}#mermaid-svg-zqcUBbeXRkJiVAIG .node rect,#mermaid-svg-zqcUBbeXRkJiVAIG .node circle,#mermaid-svg-zqcUBbeXRkJiVAIG .node ellipse,#mermaid-svg-zqcUBbeXRkJiVAIG .node polygon,#mermaid-svg-zqcUBbeXRkJiVAIG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zqcUBbeXRkJiVAIG .rough-node .label text,#mermaid-svg-zqcUBbeXRkJiVAIG .node .label text,#mermaid-svg-zqcUBbeXRkJiVAIG .image-shape .label,#mermaid-svg-zqcUBbeXRkJiVAIG .icon-shape .label{text-anchor:middle;}#mermaid-svg-zqcUBbeXRkJiVAIG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zqcUBbeXRkJiVAIG .rough-node .label,#mermaid-svg-zqcUBbeXRkJiVAIG .node .label,#mermaid-svg-zqcUBbeXRkJiVAIG .image-shape .label,#mermaid-svg-zqcUBbeXRkJiVAIG .icon-shape .label{text-align:center;}#mermaid-svg-zqcUBbeXRkJiVAIG .node.clickable{cursor:pointer;}#mermaid-svg-zqcUBbeXRkJiVAIG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zqcUBbeXRkJiVAIG .arrowheadPath{fill:#333333;}#mermaid-svg-zqcUBbeXRkJiVAIG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zqcUBbeXRkJiVAIG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zqcUBbeXRkJiVAIG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zqcUBbeXRkJiVAIG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zqcUBbeXRkJiVAIG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zqcUBbeXRkJiVAIG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zqcUBbeXRkJiVAIG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zqcUBbeXRkJiVAIG .cluster text{fill:#333;}#mermaid-svg-zqcUBbeXRkJiVAIG .cluster span{color:#333;}#mermaid-svg-zqcUBbeXRkJiVAIG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zqcUBbeXRkJiVAIG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zqcUBbeXRkJiVAIG rect.text{fill:none;stroke-width:0;}#mermaid-svg-zqcUBbeXRkJiVAIG .icon-shape,#mermaid-svg-zqcUBbeXRkJiVAIG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zqcUBbeXRkJiVAIG .icon-shape p,#mermaid-svg-zqcUBbeXRkJiVAIG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zqcUBbeXRkJiVAIG .icon-shape .label rect,#mermaid-svg-zqcUBbeXRkJiVAIG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zqcUBbeXRkJiVAIG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zqcUBbeXRkJiVAIG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zqcUBbeXRkJiVAIG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} stream.orders
消费者组 g1
消费者 c1
消费者 c2
消费者 c3
处理部分订单消息
处理部分订单消息
处理部分订单消息

消费者组的价值是:

text 复制代码
既能提高消费速度,又能记录消息是否处理完成。

7. XGROUP CREATE:创建消费者组

创建消费者组的命令是:

redis 复制代码
XGROUP CREATE key groupName ID MKSTREAM

比如:

redis 复制代码
XGROUP CREATE stream.orders g1 0 MKSTREAM

逐个解释。

key

text 复制代码
stream.orders

表示给哪个 Stream 创建消费者组。

groupName

text 复制代码
g1

消费者组名称。

后续消费者读取消息时,要指定自己属于哪个组。

ID

text 复制代码
0

表示消费者组从哪条消息开始读。

常见选择:

text 复制代码
0:从第一条消息开始读。
$:从当前最后一条消息之后开始读,只读新消息。

学习阶段可以记:

text 复制代码
0 更适合"不想错过已有消息"。
$ 更适合"只关心创建组之后的新消息"。

MKSTREAM

text 复制代码
MKSTREAM

表示如果 stream.orders 不存在,就自动创建。

不加它时,如果 Stream 不存在,创建消费者组可能失败。


8. XREADGROUP:消费者组读取消息

讲义中的命令结构:

redis 复制代码
XREADGROUP GROUP group consumer
COUNT count
BLOCK milliseconds
STREAMS key ID

项目代码里对应:

java 复制代码
stringRedisTemplate.opsForStream().read(
        Consumer.from("g1", "c1"),
        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
        StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);

它相当于:

redis 复制代码
XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >

这里逐个拆。

GROUP g1 c1

text 复制代码
g1:消费者组名称
c1:消费者名称

意思是:

text 复制代码
消费者 c1 以 g1 组成员身份读取消息。

COUNT 1

一次最多读取 1 条消息。

BLOCK 2000

没有消息时最多阻塞等待 2 秒。

这样消费者线程不会疯狂空转。

STREAMS stream.orders >

这里最关键的是:

text 复制代码
>

在消费者组里,> 表示:

text 复制代码
读取从未投递给当前消费者组的新消息。

也就是正常消费新消息。


9. ACK:为什么处理完要确认

消费者组里,消费者读取消息后,Redis 不会立刻认为这条消息彻底完成。

它会先把消息放到 pending-list。

可以理解为:

text 复制代码
这条消息已经交给某个消费者了,但还没确认处理成功。

当业务处理成功后,需要执行:

redis 复制代码
XACK stream.orders g1 消息ID

项目代码对应:

java 复制代码
stringRedisTemplate.opsForStream()
        .acknowledge("stream.orders", "g1", record.getId());

ACK 的意思是:

text 复制代码
这条消息我已经处理成功了,可以从 pending-list 中移除了。

ACK 流程图

MySQL 消费者 c1 Redis Stream MySQL 消费者 c1 Redis Stream #mermaid-svg-0JMi4iei6vhwPuiK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0JMi4iei6vhwPuiK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0JMi4iei6vhwPuiK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0JMi4iei6vhwPuiK .error-icon{fill:#552222;}#mermaid-svg-0JMi4iei6vhwPuiK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0JMi4iei6vhwPuiK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0JMi4iei6vhwPuiK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0JMi4iei6vhwPuiK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0JMi4iei6vhwPuiK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0JMi4iei6vhwPuiK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0JMi4iei6vhwPuiK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0JMi4iei6vhwPuiK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0JMi4iei6vhwPuiK .marker.cross{stroke:#333333;}#mermaid-svg-0JMi4iei6vhwPuiK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0JMi4iei6vhwPuiK p{margin:0;}#mermaid-svg-0JMi4iei6vhwPuiK .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0JMi4iei6vhwPuiK text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0JMi4iei6vhwPuiK .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0JMi4iei6vhwPuiK .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0JMi4iei6vhwPuiK .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0JMi4iei6vhwPuiK .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0JMi4iei6vhwPuiK #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0JMi4iei6vhwPuiK .sequenceNumber{fill:white;}#mermaid-svg-0JMi4iei6vhwPuiK #sequencenumber{fill:#333;}#mermaid-svg-0JMi4iei6vhwPuiK #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0JMi4iei6vhwPuiK .messageText{fill:#333;stroke:none;}#mermaid-svg-0JMi4iei6vhwPuiK .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0JMi4iei6vhwPuiK .labelText,#mermaid-svg-0JMi4iei6vhwPuiK .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0JMi4iei6vhwPuiK .loopText,#mermaid-svg-0JMi4iei6vhwPuiK .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0JMi4iei6vhwPuiK .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0JMi4iei6vhwPuiK .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0JMi4iei6vhwPuiK .noteText,#mermaid-svg-0JMi4iei6vhwPuiK .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0JMi4iei6vhwPuiK .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0JMi4iei6vhwPuiK .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0JMi4iei6vhwPuiK .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0JMi4iei6vhwPuiK .actorPopupMenu{position:absolute;}#mermaid-svg-0JMi4iei6vhwPuiK .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0JMi4iei6vhwPuiK .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0JMi4iei6vhwPuiK .actor-man circle,#mermaid-svg-0JMi4iei6vhwPuiK line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0JMi4iei6vhwPuiK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} XREADGROUP 读取消息投递消息,并放入 pending-list扣库存并保存订单处理成功XACK 确认消息从 pending-list 移除

ACK 是 Stream 比 List 更适合订单业务的关键能力。

List 的 BRPOP 是:

text 复制代码
取出来就删除。

Stream 消费者组是:

text 复制代码
投递给你
你处理成功后 ACK
Redis 再认为它完成

可靠性明显更好。


10. pending-list 是什么

pending-list 是消费者组里非常重要的概念。

它保存的是:

text 复制代码
已经投递给消费者,但还没有 ACK 的消息。

比如:

text 复制代码
消息 A 投递给 c1
c1 处理时宕机
c1 没来得及 ACK

这时消息 A 不会凭空消失。

它会留在 pending-list 中。

后续消费者可以从 pending-list 重新读取它,再处理一次。

这就是第 7 章异常恢复的核心。

pending-list 示意图

#mermaid-svg-M9sqOD5Vu8n4lEyV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M9sqOD5Vu8n4lEyV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M9sqOD5Vu8n4lEyV .error-icon{fill:#552222;}#mermaid-svg-M9sqOD5Vu8n4lEyV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M9sqOD5Vu8n4lEyV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M9sqOD5Vu8n4lEyV .marker.cross{stroke:#333333;}#mermaid-svg-M9sqOD5Vu8n4lEyV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M9sqOD5Vu8n4lEyV p{margin:0;}#mermaid-svg-M9sqOD5Vu8n4lEyV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M9sqOD5Vu8n4lEyV .cluster-label text{fill:#333;}#mermaid-svg-M9sqOD5Vu8n4lEyV .cluster-label span{color:#333;}#mermaid-svg-M9sqOD5Vu8n4lEyV .cluster-label span p{background-color:transparent;}#mermaid-svg-M9sqOD5Vu8n4lEyV .label text,#mermaid-svg-M9sqOD5Vu8n4lEyV span{fill:#333;color:#333;}#mermaid-svg-M9sqOD5Vu8n4lEyV .node rect,#mermaid-svg-M9sqOD5Vu8n4lEyV .node circle,#mermaid-svg-M9sqOD5Vu8n4lEyV .node ellipse,#mermaid-svg-M9sqOD5Vu8n4lEyV .node polygon,#mermaid-svg-M9sqOD5Vu8n4lEyV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M9sqOD5Vu8n4lEyV .rough-node .label text,#mermaid-svg-M9sqOD5Vu8n4lEyV .node .label text,#mermaid-svg-M9sqOD5Vu8n4lEyV .image-shape .label,#mermaid-svg-M9sqOD5Vu8n4lEyV .icon-shape .label{text-anchor:middle;}#mermaid-svg-M9sqOD5Vu8n4lEyV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M9sqOD5Vu8n4lEyV .rough-node .label,#mermaid-svg-M9sqOD5Vu8n4lEyV .node .label,#mermaid-svg-M9sqOD5Vu8n4lEyV .image-shape .label,#mermaid-svg-M9sqOD5Vu8n4lEyV .icon-shape .label{text-align:center;}#mermaid-svg-M9sqOD5Vu8n4lEyV .node.clickable{cursor:pointer;}#mermaid-svg-M9sqOD5Vu8n4lEyV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M9sqOD5Vu8n4lEyV .arrowheadPath{fill:#333333;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M9sqOD5Vu8n4lEyV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M9sqOD5Vu8n4lEyV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M9sqOD5Vu8n4lEyV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M9sqOD5Vu8n4lEyV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M9sqOD5Vu8n4lEyV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M9sqOD5Vu8n4lEyV .cluster text{fill:#333;}#mermaid-svg-M9sqOD5Vu8n4lEyV .cluster span{color:#333;}#mermaid-svg-M9sqOD5Vu8n4lEyV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M9sqOD5Vu8n4lEyV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M9sqOD5Vu8n4lEyV rect.text{fill:none;stroke-width:0;}#mermaid-svg-M9sqOD5Vu8n4lEyV .icon-shape,#mermaid-svg-M9sqOD5Vu8n4lEyV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M9sqOD5Vu8n4lEyV .icon-shape p,#mermaid-svg-M9sqOD5Vu8n4lEyV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M9sqOD5Vu8n4lEyV .icon-shape .label rect,#mermaid-svg-M9sqOD5Vu8n4lEyV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M9sqOD5Vu8n4lEyV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M9sqOD5Vu8n4lEyV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M9sqOD5Vu8n4lEyV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

stream.orders 新消息
XREADGROUP 投递给 c1
pending-list
c1 是否 XACK?
从 pending-list 移除
保留在 pending-list,等待恢复处理


11. > 和 0 的区别

这是第 7 章最容易混的点之一。

消费者组读取时,最后的 ID 可以是:

text 复制代码
>
0

它们含义完全不同。

> 表示读新消息

redis 复制代码
XREADGROUP GROUP g1 c1 STREAMS stream.orders >

表示:

text 复制代码
读取从未被投递给消费者组的新消息。

这是正常消费流程。

0 表示读 pending-list

redis 复制代码
XREADGROUP GROUP g1 c1 STREAMS stream.orders 0

表示:

text 复制代码
从 pending-list 中读取已经投递但未确认的消息。

这是异常恢复流程。

可以记成一句话:

> 找新活,0 捡旧账。

项目代码里也是这样:

java 复制代码
// 正常读取新消息
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())

// 异常恢复读取 pending-list
StreamOffset.create("stream.orders", ReadOffset.from("0"))

这里 ReadOffset.lastConsumed() 在消费者组场景下对应的就是读取新消息的 > 语义。


12. Stream 消费者组完整消费流程

把消费者组正常流程串起来:

text 复制代码
1. Lua 用 XADD 写入订单消息。
2. 消费者用 XREADGROUP ... > 读取新消息。
3. Redis 把消息投递给消费者,并放入 pending-list。
4. 消费者解析消息,执行数据库下单。
5. 数据库处理成功后,消费者执行 XACK。
6. Redis 从 pending-list 移除该消息。

如果第 4 步失败:

text 复制代码
消息不会 ACK
它会留在 pending-list
后续 handlePendingList 可以用 ID=0 重新读取处理

完整流程图

#mermaid-svg-RSnOJIDTQxANA5c0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RSnOJIDTQxANA5c0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RSnOJIDTQxANA5c0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RSnOJIDTQxANA5c0 .error-icon{fill:#552222;}#mermaid-svg-RSnOJIDTQxANA5c0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RSnOJIDTQxANA5c0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RSnOJIDTQxANA5c0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RSnOJIDTQxANA5c0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RSnOJIDTQxANA5c0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RSnOJIDTQxANA5c0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RSnOJIDTQxANA5c0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RSnOJIDTQxANA5c0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RSnOJIDTQxANA5c0 .marker.cross{stroke:#333333;}#mermaid-svg-RSnOJIDTQxANA5c0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RSnOJIDTQxANA5c0 p{margin:0;}#mermaid-svg-RSnOJIDTQxANA5c0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RSnOJIDTQxANA5c0 .cluster-label text{fill:#333;}#mermaid-svg-RSnOJIDTQxANA5c0 .cluster-label span{color:#333;}#mermaid-svg-RSnOJIDTQxANA5c0 .cluster-label span p{background-color:transparent;}#mermaid-svg-RSnOJIDTQxANA5c0 .label text,#mermaid-svg-RSnOJIDTQxANA5c0 span{fill:#333;color:#333;}#mermaid-svg-RSnOJIDTQxANA5c0 .node rect,#mermaid-svg-RSnOJIDTQxANA5c0 .node circle,#mermaid-svg-RSnOJIDTQxANA5c0 .node ellipse,#mermaid-svg-RSnOJIDTQxANA5c0 .node polygon,#mermaid-svg-RSnOJIDTQxANA5c0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RSnOJIDTQxANA5c0 .rough-node .label text,#mermaid-svg-RSnOJIDTQxANA5c0 .node .label text,#mermaid-svg-RSnOJIDTQxANA5c0 .image-shape .label,#mermaid-svg-RSnOJIDTQxANA5c0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-RSnOJIDTQxANA5c0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RSnOJIDTQxANA5c0 .rough-node .label,#mermaid-svg-RSnOJIDTQxANA5c0 .node .label,#mermaid-svg-RSnOJIDTQxANA5c0 .image-shape .label,#mermaid-svg-RSnOJIDTQxANA5c0 .icon-shape .label{text-align:center;}#mermaid-svg-RSnOJIDTQxANA5c0 .node.clickable{cursor:pointer;}#mermaid-svg-RSnOJIDTQxANA5c0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RSnOJIDTQxANA5c0 .arrowheadPath{fill:#333333;}#mermaid-svg-RSnOJIDTQxANA5c0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RSnOJIDTQxANA5c0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RSnOJIDTQxANA5c0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RSnOJIDTQxANA5c0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RSnOJIDTQxANA5c0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RSnOJIDTQxANA5c0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RSnOJIDTQxANA5c0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RSnOJIDTQxANA5c0 .cluster text{fill:#333;}#mermaid-svg-RSnOJIDTQxANA5c0 .cluster span{color:#333;}#mermaid-svg-RSnOJIDTQxANA5c0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RSnOJIDTQxANA5c0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RSnOJIDTQxANA5c0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-RSnOJIDTQxANA5c0 .icon-shape,#mermaid-svg-RSnOJIDTQxANA5c0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RSnOJIDTQxANA5c0 .icon-shape p,#mermaid-svg-RSnOJIDTQxANA5c0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RSnOJIDTQxANA5c0 .icon-shape .label rect,#mermaid-svg-RSnOJIDTQxANA5c0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RSnOJIDTQxANA5c0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RSnOJIDTQxANA5c0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RSnOJIDTQxANA5c0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败
XADD 写入订单消息
stream.orders
XREADGROUP ... > 读取新消息
消息进入 pending-list
消费者处理业务
处理是否成功?
XACK 确认消息
从 pending-list 移除
不 ACK,消息留在 pending-list
XREADGROUP ... 0 重新处理

这张图就是第 7 章的核心。


13. Stream 相比 List 和 PubSub 强在哪里

讲义最后有一个对比。

我们用自己的话总结:

相比 List

Stream 更强在:

text 复制代码
1. 每条消息有 ID,可以回溯。
2. 支持消费者组。
3. 支持 ACK。
4. 支持 pending-list 异常恢复。

相比 PubSub

Stream 更强在:

text 复制代码
1. 消息会保存,不是在线广播。
2. 消费者离线后,消息仍然可以后续处理。
3. 有确认机制。
4. 更适合订单这种关键业务。

相比 BlockingQueue

Stream 更强在:

text 复制代码
1. 不依赖单个 JVM 内存。
2. 服务宕机后消息仍然在 Redis。
3. 多个服务实例可以共享同一个队列。

14. 本篇最容易混淆的几个点

1. Stream 是不是等于 Redis 里的 List

不是。

List 是简单链表队列。

Stream 是带消息 ID、消费者组、ACK、pending-list 的消息流结构。

2. XREAD 和 XREADGROUP 是不是一样

不一样。

XREAD 是普通读取。

XREADGROUP 是消费者组读取,能配合消费者组、pending-list、ACK 使用。

3. ACK 是不是删除 Stream 里的消息

不是简单删除 Stream 消息。

ACK 的核心是:

text 复制代码
把该消息从消费者组的 pending-list 中移除,表示这个组已经处理完成。

4. pending-list 是不是一个新队列

不是。

它是消费者组内部维护的"已投递但未确认消息列表"。

5. >0 怎么记

可以记:

text 复制代码
>:读新消息
0:读 pending-list 里的旧消息

15. 面试怎么回答

如果面试官问:Redis Stream 为什么比 List 更适合做消息队列?

可以这样回答:

Redis List 可以模拟简单队列,但取出消息后消息就从队列中移除,缺少完善的消息确认和失败恢复机制。Redis Stream 是 Redis 5.0 引入的消息流结构,每条消息有唯一 ID,支持阻塞读取、消费者组、ACK 和 pending-list。消费者读取消息后,如果处理成功再 ACK;如果处理失败或宕机,消息会留在 pending-list 中,可以后续重新处理,因此更适合订单这类需要可靠消费的业务。

如果面试官问:Stream 消费者组中的 pending-list 是什么?

可以这样回答:

pending-list 保存的是已经被投递给消费者但还没有 ACK 的消息。当消费者读取消息后,Redis 会把消息记录到 pending-list;消费者业务处理成功后执行 XACK,消息才会从 pending-list 中移除。如果消费者处理过程中异常或宕机,没有 ACK,这条消息就会留在 pending-list 中,后续可以通过读取 pending-list 来恢复处理。

如果面试官问:XREADGROUP>0 有什么区别?

可以这样回答:

在消费者组模式下,> 表示读取从未投递给当前消费者组的新消息,是正常消费流程;0 表示从 pending-list 中读取已经投递但未确认的消息,通常用于异常恢复。


16. 总结

这一篇的主线是:

text 复制代码
Stream 保存消息并生成消息 ID
    ↓
XADD 写入消息
    ↓
XREAD 可以普通读取,但有漏读风险
    ↓
消费者组让多个消费者协作消费
    ↓
XREADGROUP 读取新消息
    ↓
处理成功后 XACK
    ↓
处理失败则消息留在 pending-list
    ↓
后续用 ID=0 从 pending-list 恢复处理

最重要的是记住:

Stream 的强大不是某一个命令,而是"消息保存 + 消费者组 + ACK + pending-list"这一整套机制组合起来,让 Redis 更像一个真正的消息队列。

下一篇把这些机制放回黑马点评业务:

text 复制代码
Stream 如何改造异步秒杀下单?
相关推荐
8125035331 小时前
第 9 篇:子网掩码:如何划分“小区”
开发语言·php
Jun6261 小时前
QT(12)-制作lib库
开发语言·qt
Java面试题总结1 小时前
C#12 中的 Using Alias
开发语言·windows·c#
加号32 小时前
【C#】 ASCII 码转字符串技术解析
开发语言·c#
DIY源码阁2 小时前
JavaSwing航班订票管理系统 - MySQL版
数据库·mysql
qqxhb2 小时前
47|成本与性能:缓存、批处理、模型路由与降级
缓存·批处理·智能模型路由·多级降级预案·成本预算
Cloud_Shy6182 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第五章 Item 33 - 35)
开发语言·人工智能·笔记·python·学习方法
星恒随风2 小时前
C++ 类和对象入门(五):初始化列表、explicit 和 static 成员详解
开发语言·c++·笔记·学习·状态模式
艾利克斯冰2 小时前
Java 设计模式-行为型模式(更新中)
java·开发语言·设计模式