1. 项目背景

1.1 业务场景

运营后台来了两个批量任务。第一个是"618 积分补发"——运营从投诉工单中筛选出 8000 个用户,每人补发 50 积分。开发同学写了最简单的实现:

for (String userId : userIds) {
    redis.hincrBy("mall:point:user:" + userId, "total", 50);
    redis.zincrBy("mall:rank:point", 50, "user:" + userId);
}

这个循环在网络延迟 0.5ms 的环境下,8000 个用户 × 2 条命令 = 16000 次 RTT × 0.5ms = 8 秒。加上序列化和线程切换,实际耗时 12 秒。运营吐槽"点个按钮要等半分钟"。

第二个任务是账户余额扣减。用户下单后从账户余额中扣款,两个并发请求同时扣同一个账户。如果直接用 GET balance + DECRBY,就会出现第 13 章那样的并发覆盖问题。但这里业务逻辑没那么复杂(不需要限购判断),用完整的 Lua 脚本有点牛刀杀鸡。

这两个问题分别对应 Redis 的两种批量处理方案——Pipeline(解决 RTT 问题)和事务+MULTI/WATCH(解决并发一致性问题)。

1.2 三种方案的区别(必须理清)

方案 解决的问题 是否原子 是否减少 RTT 典型场景
Pipeline 减少网络往返 否 是 批量导入、批量查询
MULTI/EXEC 命令排队执行 部分(排队连续但无回滚) 否 批量无依赖更新
WATCH+MULTI/EXEC 乐观锁并发控制 CAS 语义 否 余额扣减等低冲突 CAS
Lua 多步逻辑原子化 是 中等 判断后修改、库存扣减

1.3 本章目标

  • 用 Pipeline 批量写入积分——对比 RTT 和延迟改善。
  • 用 WATCH + MULTI/EXEC 实现账户余额的乐观锁扣减。
  • 理解 MULTI/EXEC 的错误处理——语法错误 vs 运行时错误的区别。
  • 清晰地对比三种方案的适用边界。

2. 项目设计

2.1 第一幕:Pipeline 是批发,不是团购

小胖:"我要批量写 8000 个用户的积分——一个 MULTI 包起来,EXEC 一次性执行,完美。"

小白:"MULTI/EXEC 是排队执行,不是批量传输。你每一条命令还是要从客户端发到 Redis——MULTI 只是告诉 Redis'我开始排队了',EXEC 是'把排队的都执行了'。网络往返没有减少。"

大师画了两张图:

MULTI/EXEC 的网络交互(未减少 RTT):

  客户端                                    Redis
    │ ─── MULTI ──────────────────────────▶ │
    │ ◀── +OK ─────────────────────────────  │
    │ ─── HINCRBY user:1 total 50 ────────▶ │ (排队)
    │ ◀── +QUEUED ─────────────────────────  │
    │ ─── ZINCRBY rank:point 50 user:1 ──▶ │ (排队)
    │ ◀── +QUEUED ─────────────────────────  │
    │ ... (8000 次往返!) ...                 │
    │ ─── EXEC ───────────────────────────▶ │
    │ ◀── [结果1, 结果2, ...] ────────────  │
    │                                        │
  每次命令都要等 QUEUED 响应 → RTT 堆积

Pipeline 的网络交互(大幅减少 RTT):

  客户端                                    Redis
    │ ─── HINCRBY user:1 ... ──────────────▶ │
    │ ─── ZINCRBY rank:point ... ──────────▶ │ (全部发出,不等待响应)
    │ ─── HINCRBY user:2 ... ──────────────▶ │
    │ ... (一批发完) ...                      │
    │ ◀── [结果1, 结果2, 结果3, ...] ─────── │ (一批返回)

大师:"Pipeline 就像批发进货——你把一批订单一次性发给供应商,供应商一次把货发回来。不是一个个打电话问'这个有货吗?''那有货吗?'。8000 个用户的写操作,从 16000 次 RTT 减少到 1 次(或按批次分几批)。"

技术映射:Pipeline 优化的是网络层的吞吐,不是 Redis 内部的执行方式。Redis 仍然按顺序执行每一条命令,但客户端的"发-等-收"模式变成了"发发发-收收收"。

2.2 第二幕:MULTI/EXEC 没有回滚!

小胖:"那 MULTI/EXEC 是不是和数据库事务一样?如果中间某条命令失败,自动回滚。"

大师:"这是最大的误解。Redis 事务不支持回滚。MULTI 只是让命令排队,EXEC 把所有命令按顺序执行一遍。如果某条命令执行时出错(比如对 String 执行了 HINCRBY),这条会返回错误——但队列中的其他命令仍然会正常执行。"

示例:
  MULTI
  HINCRBY mall:point:user:1001 total 10     ← 正常
  HINCRBY mall:cache:product:1001 total 10   ← 错误!这是个 String 不是 Hash
  HINCRBY mall:point:user:1003 total 10     ← 仍然正常执行!

  EXEC 返回:
  1) (integer) 10     ← 第1条成功
  2) (error) WRONGTYPE ← 第2条失败
  3) (integer) 10     ← 第3条成功(没有回滚!)

大师:"Redis 的设计哲学是——保持简单和快速。回滚需要 undo log 或锁,会明显增加复杂度。Redis 假设命令错误是编程 bug,应该在测试阶段发现,而不是靠运行时回滚。"

技术映射:Redis 事务提供的是"一组命令在 EXEC 前不被其他客户端插入"的隔离性,不是传统 ACID 的原子性。某条命令失败不会影响队列中的其他命令。

2.3 第三幕:WATCH——Redis 的乐观锁

小胖:"那余额扣减怎么保证并发安全?两个请求同时扣同一个账户。"

大师:"WATCH 提供了一种 CAS(Compare-And-Swap)机制。把它想象成版本号——WATCH account 后,Redis 记住这个 key 的当前版本。在 EXEC 执行前,如果 key 被其他客户端修改了,EXEC 会返回空,事务不执行。"

WATCH 乐观锁流程:

  客户端 A                          客户端 B
    │                                 │
    ├── WATCH account ────────────▶   │
    ├── GET account → 100             │
    │                                 ├── WATCH account ──▶
    │                                 ├── GET account → 100
    │                                 ├── MULTI
    │                                 ├── DECRBY account 30
    │                                 ├── EXEC → 成功 [70]
    │                                 │
    ├── MULTI                         │
    ├── DECRBY account 50             │
    ├── EXEC → (nil) ← 失败!          │
    │   因为 B 修改了 account           │
    │                                 │
    └── 重试: WATCH → GET(70) →       │
        MULTI → DECRBY 30 → EXEC     │
        → 成功 [40]

大师:"这个模式适合冲突不高的场景。冲突高的话(比如秒杀同一件商品),重试次数会很多,建议用 Lua。余额扣减的冲突一般不高——大部分请求扣不同的账户。"

技术映射:WATCH 不是锁——它不会阻止其他客户端修改 key,只是在修改发生时让你的事务失败。这是"乐观锁"的经典实现。

2.4 第四幕:Pipeline 不是事务!

小胖:"我把 100 条命令 Pipeline 发过去,中间不会被别人的命令插队吧?"

小白:"会被插队。Pipeline 只是减少网络往返——Redis 仍然一条一条执行命令。你的 100 条 Pipeline 命令发过去后,Redis 按顺序执行,但执行到你第 50 条和第 51 条之间时,另一个客户端的命令可能被插入。"

大师:"所以 Pipeline + 需要原子性 = 错误组合。如果积分补发要求'一个人积分和排名必须同时更新',Pipeline 做不到——因为另一个人可能在你更新积分之后、更新排名之前插进来。这时候要用 Lua。"


3. 项目实战

3.1 环境准备

docker exec -it redis-lab redis-cli -a redis123

3.2 实战一:MULTI/EXEC 基础

# 启动事务
127.0.0.1:6379> MULTI
OK

# 队列中的命令返回 QUEUED
127.0.0.1:6379> HINCRBY mall:point:user:1001 total 50
QUEUED
127.0.0.1:6379> ZINCRBY mall:rank:point 50 user:1001
QUEUED
127.0.0.1:6379> HINCRBY mall:point:user:1002 total 30
QUEUED
127.0.0.1:6379> ZINCRBY mall:rank:point 30 user:1002
QUEUED

# 执行事务
127.0.0.1:6379> EXEC
1) (integer) 50
2) "50"
3) (integer) 30
4) "30"

# DISCARD: 放弃事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET mall:tmp:bad 1
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> GET mall:tmp:bad
(nil)

3.3 实战二:事务中的错误处理

# 准备一个 String 类型的 key
127.0.0.1:6379> SET mall:test:wrongtype "i-am-string"
OK

# 事务中包含错误的命令
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> HINCRBY mall:point:user:1001 total 10
QUEUED
127.0.0.1:6379> HINCRBY mall:test:wrongtype total 10
QUEUED
127.0.0.1:6379> HINCRBY mall:point:user:1003 total 10
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 60      # ← 第1条成功
2) (error) WRONGTYPE # ← 第2条失败(类型错误)
3) (integer) 10      # ← 第3条仍然成功!没有回滚

关键理解:Redis 事务中某条命令失败不会自动回滚。调用方必须逐项检查 EXEC 的返回数组。

3.4 实战三:WATCH 乐观锁——余额扣减

步骤 1:准备账户

127.0.0.1:6379> SET mall:account:user:1001 100
OK

步骤 2:模拟并发扣减(双终端)

# 终端 A: WATCH + MULTI
127.0.0.1:6379> WATCH mall:account:user:1001
OK
127.0.0.1:6379> GET mall:account:user:1001
"100"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY mall:account:user:1001 40
QUEUED

# 此时切换到终端 B,修改同一个 key
# 终端 B:
127.0.0.1:6379> INCRBY mall:account:user:1001 20
(integer) 120

# 回到终端 A,执行 EXEC
127.0.0.1:6379> EXEC
(nil)       # ← 事务失败!因为 key 被 B 修改了

# 终端 A 应该重试:
127.0.0.1:6379> WATCH mall:account:user:1001
OK
127.0.0.1:6379> GET mall:account:user:1001
"120"       # 重新读取最新值
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY mall:account:user:1001 40
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 80  # 成功!

步骤 3:Java 乐观锁余额扣减

public class BalanceService {

    private static final int MAX_RETRY = 3;

    public DeductResult deduct(String userId, long amount) {
        String key = "mall:account:user:" + userId;

        for (int retry = 0; retry < MAX_RETRY; retry++) {
            // 1. 监视 key
            redis.watch(key);

            // 2. 读取余额
            String balanceStr = redis.get(key);
            long balance = balanceStr != null ? Long.parseLong(balanceStr) : 0;

            if (balance < amount) {
                redis.unwatch();  // 放弃监视
                return DeductResult.INSUFFICIENT_BALANCE;
            }

            // 3. 开启事务
            redis.multi();

            // 4. 入队扣减命令
            redis.decrBy(key, amount);

            // 5. 执行事务
            List<Object> results = redis.exec();

            if (results != null && !results.isEmpty()) {
                // EXEC 成功
                return DeductResult.SUCCESS;
            }

            // EXEC 返回 null → 被其他请求修改了 → 重试
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return DeductResult.RETRY_FAILED;
            }
        }

        return DeductResult.RETRY_EXHAUSTED;
    }
}

3.5 实战四:Pipeline 批量积分发放

# redis-cli 的 --pipe 模式(从文件批量执行)
# 创建一个文件 batch_commands.txt:
#   HINCRBY mall:point:user:1001 total 50
#   ZINCRBY mall:rank:point 50 user:1001
#   HINCRBY mall:point:user:1002 total 50
#   ZINCRBY mall:rank:point 50 user:1002
#   ... (8000 行)

# 执行:
# cat batch_commands.txt | redis-cli -a redis123 --pipe

Java Pipeline 实现:

public class PointBatchService {

    private static final int BATCH_SIZE = 500;  // 每批 500 条

    public BatchResult batchAddPoints(List<String> userIds, int points) {
        List<String> failedUsers = new ArrayList<>();
        long startTime = System.currentTimeMillis();
        int total = userIds.size();
        int processed = 0;

        // 分批执行
        for (int i = 0; i < total; i += BATCH_SIZE) {
            int end = Math.min(i + BATCH_SIZE, total);
            List<String> batch = userIds.subList(i, end);

            List<Object> results = redis.executePipelined((RedisCallback<Object>) conn -> {
                for (String userId : batch) {
                    byte[] pointKey = ("mall:point:user:" + userId).getBytes();
                    byte[] rankKey = "mall:rank:point".getBytes();
                    conn.hashCommands().hIncrBy(pointKey, "total".getBytes(), points);
                    conn.zSetCommands().zIncrBy(rankKey, points, ("user:" + userId).getBytes());
                }
                return null;
            });

            // 处理结果(每条命令对应一个结果)
            for (int j = 0; j < results.size(); j++) {
                Object result = results.get(j);
                if (result instanceof RedisSystemException) {
                    String userId = batch.get(j / 2);  // 每个用户 2 条命令
                    failedUsers.add(userId);
                    log.error("积分发放失败: userId={}", userId, (Exception) result);
                }
            }

            processed += batch.size();
        }

        long elapsed = System.currentTimeMillis() - startTime;
        return new BatchResult(total, processed, failedUsers.size(), elapsed);
    }
}

Pipeline 性能对比:

方案 8000 用户耗时 网络往返次数
逐条执行 ~12 秒 16000 次
Pipeline(每批 500) ~0.8 秒 16 次
Pipeline(一批全发) ~0.3 秒 1 次

3.6 实战五:Pipeline 批量查询

# 不使用 Pipeline: 每个 GET 一次 RTT
127.0.0.1:6379> GET mall:cache:product:1001
127.0.0.1:6379> GET mall:cache:product:1002
127.0.0.1:6379> GET mall:cache:product:1003
# → 3 次 RTT

# 使用 MGET(Redis 原生命令):一次 RTT
127.0.0.1:6379> MGET mall:cache:product:1001 mall:cache:product:1002 mall:cache:product:1003
1) "{\"id\":1001,...}"
2) "{\"id\":1002,...}"
3) "{\"id\":1003,...}"

# 混合类型查询(MGET 只能用于 String)
# 如果同时查 String, Hash, ZSet → 只能用 Pipeline

3.7 三种方案选型速查

场景 推荐方案 理由
批量写 8000 用户积分 Pipeline 只需减少 RTT,不需要原子性
余额扣减(低冲突) WATCH+MULTI 乐观锁足够,重试少
余额扣减(高冲突) Lua 减少重试,服务端原子执行
库存扣减+限购检查 Lua 需要 if 判断,WATCH 做不到
批量预热商品缓存 Pipeline 纯批量 SET,不需要事务
批量查询用户状态 Pipeline 混合类型查询,MGET 不支持

3.8 实战六:Pipeline 批量预热与缓存热加载

大促前的缓存预热是 Pipeline 的经典用例:

public class CacheWarmUpService {

    private static final int BATCH_SIZE = 500;

    public WarmUpResult warmUpHotProducts(List<Long> productIds) {
        long start = System.currentTimeMillis();
        int total = productIds.size();
        int success = 0;
        int failed = 0;

        // 1. 先从数据库批量加载
        List<Product> products = database.findProductsByIds(productIds);
        Map<Long, Product> productMap = products.stream()
            .collect(Collectors.toMap(Product::getId, Function.identity()));

        // 2. Pipeline 批量写入 Redis
        for (int i = 0; i < total; i += BATCH_SIZE) {
            int end = Math.min(i + BATCH_SIZE, total);
            List<Long> batch = productIds.subList(i, end);

            List<Object> results = redis.executePipelined((RedisCallback<Object>) conn -> {
                for (Long pid : batch) {
                    Product product = productMap.get(pid);
                    if (product != null) {
                        String key = ("mall:cache:product:" + pid).getBytes();
                        String value = serialize(product);
                        int ttl = 600 + ThreadLocalRandom.current().nextInt(120);
                        conn.setEx(key, ttl, value.getBytes());
                    }
                }
                return null;
            });

            // 统计成功/失败
            for (Object result : results) {
                if (result instanceof Exception) {
                    failed++;
                } else {
                    success++;
                }
            }
        }

        long elapsed = System.currentTimeMillis() - start;
        log.info("缓存预热完成: total={}, success={}, failed={}, elapsed={}ms",
            total, success, failed, elapsed);
        return new WarmUpResult(total, success, failed, elapsed);
    }
}

3.9 实战七:使用 redis-benchmark 对比 Pipeline 效果

# 无 Pipeline:逐条 PING(模拟逐条写)
docker exec redis-lab redis-benchmark -a redis123 -t ping -n 10000 -c 1 -q
# PING: 15678.90 requests per second

# 有 Pipeline:一次发 16 条命令
docker exec redis-lab redis-benchmark -a redis123 -t ping -n 10000 -c 1 -P 16 -q
# PING: 125000.00 requests per second  ← 吞吐提升约 8 倍!

# 不同 Pipeline 大小的对比:
# -P 1:  约 15000 QPS (无 Pipeline)
# -P 4:  约 50000 QPS
# -P 8:  约 85000 QPS
# -P 16: 约 125000 QPS
# -P 32: 约 140000 QPS (收益递减)

Pipeline 的最佳实践:

  • 批量大小建议控制在 500-2000 条命令之间——太小收益不够,太大增加内存峰值和延迟尖刺。
  • 不要在 Pipeline 中混合读写依赖——Pipeline 中命令是顺序执行的,但客户端不会等前一条返回再发下一条。
  • 注意 Redis 的输出缓冲区限制——client-output-buffer-limit 如果太小,大量 Pipeline 响应可能被截断。

3.10 实战八:Pipeline 错误处理与重试

Pipeline 中某条命令失败不会影响其他命令,需要逐条检查:

public class RobustPipelineService {

    public BatchResult<Integer> batchIncrWithRetry(List<String> keys, int delta, int maxRetries) {
        List<String> failedKeys = new ArrayList<>();
        int successCount = 0;

        for (int batch = 0; batch < keys.size(); batch += BATCH_SIZE) {
            int end = Math.min(batch + BATCH_SIZE, keys.size());
            List<String> currentBatch = keys.subList(batch, end);

            List<Object> results = redis.executePipelined((RedisCallback<Object>) conn -> {
                for (String key : currentBatch) {
                    conn.stringCommands().incrBy(key.getBytes(), delta);
                }
                return null;
            });

            // 逐条检查结果
            for (int i = 0; i < results.size(); i++) {
                Object result = results.get(i);
                if (result instanceof Long) {
                    successCount++;
                } else if (result instanceof Exception) {
                    String key = currentBatch.get(i);
                    log.error("Pipeline 命令失败: key={}, error={}", key, result);
                    failedKeys.add(key);
                }
            }
        }

        // 失败重试(单独执行,不放在 Pipeline 中)
        if (!failedKeys.isEmpty() && maxRetries > 0) {
            log.warn("Pipeline 部分失败, 重试 {} 个 key", failedKeys.size());
            BatchResult<Integer> retryResult = batchIncrWithRetry(failedKeys, delta, maxRetries - 1);
            successCount += retryResult.getSuccessCount();
        }

        return new BatchResult<>(keys.size(), successCount, failedKeys.size());
    }
}

3.11 实战九:WATCH 与 Pipeline 不能混用

一个常见误用:想在 WATCH 的同时用 Pipeline 提升性能——这是不兼容的。

// 错误组合!WATCH 后使用 Pipeline 会导致 WATCH 失效
// 因为 Pipeline 中的命令不会实时返回结果,WATCH 无法正确判断

// 正确做法:
// 方案 1: WATCH + 逐条命令 + MULTI/EXEC
// 方案 2: 直接用 Lua(最推荐,简单可靠)

// Lua 实现余额扣减(替代 WATCH 重试)
private static final String DEDUCT_LUA =
    "local balance = tonumber(redis.call('GET', KEYS[1]) or '0'); " +
    "local amount = tonumber(ARGV[1]); " +
    "if balance < amount then return -1 end; " +
    "redis.call('DECRBY', KEYS[1], amount); " +
    "return balance - amount";

public long deductWithLua(String userId, long amount) {
    String key = "mall:account:user:" + userId;
    Long result = redis.eval(DEDUCT_LUA,
        Collections.singletonList(key),
        Collections.singletonList(String.valueOf(amount)));
    return result != null ? result : -1;
}

3.12 实战十:Pipeline 输出缓冲区监控

Pipeline 的一个隐藏风险——如果客户端发送大量命令但 Redis 响应慢,输出缓冲区可能积压:

# 查看输出缓冲区配置
127.0.0.1:6379> CONFIG GET client-output-buffer-limit
# 输出: normal 0 0 60, replica 256mb 64mb 60, pubsub 32mb 8mb 60
# normal: 硬限制0(不限制) 软限制0 超时60秒

# 查看客户端的输出缓冲区状态
127.0.0.1:6379> CLIENT LIST | grep "omem="
# 如果 omem 值很大,说明该客户端的输出缓冲区积压严重

Pipeline 中的客户端如果接收响应慢,响应会堆积在 Redis 的输出缓冲区中,不仅浪费内存,还可能导致 Redis 主动断开连接。最佳实践:Pipeline 批次大小不超过 2000 条;客户端必须及时消费 Pipeline 的响应;为 Pipeline 设置合理的超时。

场景 推荐批次大小 原因
批量写小数据 500-2000 平衡吞吐与内存
批量读小 key 200-500 响应包可能较大
批量写大 value 50-200 避免输出缓冲区溢出
跨机房操作 100-500 网络延迟放大

4. 项目总结

4.1 三者边界一句话总结

方案 一句话
Pipeline "少跑几趟,每条还是各管各的"
MULTI/EXEC "排好队一起执行,但错了不负责"
WATCH+MULTI "乐观锁,谁改了我的数据我就重来"
Lua "全在 Redis 里搞定,谁也别插队"

4.2 常见踩坑经验

案例 表象 根因 解法
Pipeline 包太大 执行到一半报 OOM 一次发 100 万条命令,客户端和服务端内存都爆 分批发送,每批 500-2000 条
WATCH 重试死循环 余额扣减接口一直重试不返回 热点账户高并发,每次 WATCH 都冲突 热点账户改用 Lua;或用队列串行化
事务中混合 WATCH WATCH 在 MULTI 之后执行 WATCH 必须在 MULTI 之前调用 WATCH → GET → MULTI → 入队 → EXEC
误以为 Pipeline 原子 积分和排名不一致 Pipeline 中两个命令之间被其他客户端插入 需要原子性用 Lua

4.3 思考题

  1. 场景题:积分系统需要在"增加积分"的同时"更新排名"和"记录积分流水"(三个操作需要原子性)。请设计 Lua 脚本实现这个组合操作。

  2. 优化题:WATCH 在冲突高的场景下重试次数多、性能差。除了改用 Lua,还有什么方案可以降低 WATCH 的冲突率?

(答案提示将在下一章末尾给出。)


上一章思考题答案提示(第13章):

第1题(热点库存拆分):方案一——水平拆分,将 100 件库存拆成 10 个 key,每个 10 件(stock:sku1001:0 到 stock:sku1001:9)。用户请求时随机/哈希选一个 key,用 Lua 扣减。方案二——令牌桶,不存库存数,而是用 List 预存 100 个令牌,用 LPOP 原子弹出——拿到令牌的算抢到,没拿到的就是抢完。

第2题(Lua 无限循环):脚本会持续占用 Redis 主线程,所有其他客户端全部阻塞。如果脚本没有执行写操作(如纯计算死循环),可以用 SCRIPT KILL 终止。如果脚本已执行了写操作——SCRIPT KILL 无效,只能重启 Redis。这对生产的启示:① 永远不要在 Lua 中写无上限的循环;② 设置 lua-time-limit(默认 5000ms),超时后 Redis 会记录日志但并不自动终止。

延伸阅读与资源

Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
Nacos 3.x 注册配置中心实战修炼:从入门到源码扩展
Elasticsearch从入门到进阶的实战之旅
MySQL Server 9从入门到进阶的实战之旅
RabbitMQ从入门到进阶的实战之旅:从单机到大促高可用架构
Celery 入门到进阶之路:从异步任务到自研调度平台
LangGraph 生产级实战进阶:从零到生产级Agent工作流开发
Dify 从入门到源码:LLM 应用平台实战修炼
从零到生产级:FastAPI 异步高并发、源码与 SRE 实战
实战SQLAlchemy 2.0: 从 CRUD 到生产级架构
从零打造企业级 AI 助手:LangChain RAG、Agent 与生产实战
后端工程师 AI 转型课:Ollama 私有化大模型从入门到生产
MongoDB 实战进阶与内核修炼
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化/性能调优)
Milvus向量数据库实战修炼:从 0 到 1 精通向量检索与生产落地
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经

posted on 2026-11-09 16:07  一天不进步,就是退步  阅读(13)  评论(0)    收藏  举报