订单总丢?PHP队列的正确打开方式


写在前面

上周有个朋友深夜给我打电话,声音里全是焦虑:"我们的订单系统又出事了------用户下单后支付成功,但订单状态没更新,客服电话被打爆了!"

我一问,原来他们所有订单处理逻辑都写在接口的同步流程里:高并发下一超时,订单就丢了。

这不是个例。我见过太多 PHP 项目在业务增长期被"同步处理"这把钝刀割得遍体鳞伤。今天这篇文章,不讲概念堆砌,只讲为什么你的订单会丢、怎么用队列真正解决,以及那些年我踩过的坑


一、为什么你的订单总丢?------ 同步处理的三大原罪

1.1 超时:接口等不起,业务伤不起

php 复制代码
// 很多项目的订单处理是这样的
public function createOrder($orderData)
{
    // 1. 插入订单
    $orderId = $this->orderModel->create($orderData);

    // 2. 库存扣减
    $this->inventoryService->deduct($orderId);

    // 3. 发送通知
    $this->notifyService->send($orderId);

    // 4. 记录日志
    $this->logService->record($orderId);

    return $orderId;
}

这段代码看起来没问题,但如果第 3 步"发送通知"是个 HTTP 调用且耗时 3 秒,请问:

  • 用户的感受是什么?接口等了 3 秒才返回。
  • 如果第 4 步又花了 2 秒呢?5 秒,用户早就关页面了。
  • 如果_notifyService_ 在第 3 步抛了异常呢?整个事务回滚,订单都没了。

更极端一点:用户支付成功 → 回调接口触发 → 库存服务超时 → 订单回滚 → 用户付了钱但没订单。投诉、客诉、退款,一个不少。

1.2 并发:并发一来,Race Condition 伺候

php 复制代码
// 库存扣减的原始写法
$stock = $this->redis->get("goods:{$goodsId}:stock");
if ($stock > 0) {
    $this->redis->decr("goods:{$goodsId}:stock"); // 库存超卖!
    $this->orderModel->create($orderData);
}

在高并发下,get → 判断 → decr 这三步不是原子的,两个请求同时读到 stock = 1,都通过判断,都扣了,结果库存变成 -1,超卖两个。

你以为这是库存专属的问题?支付回调、优惠券发放、积分扣除......所有涉及"读-改-写"的场景,并发都是定时炸弹

1.3 崩溃:进程挂了,任务也没了

PHP-FPM 工作模式:请求来 → 启动进程 → 处理 → 返回 → 进程销毁

如果处理到一半,PHP-FPM 进程被 kill 了(OOM Kill、超时强制终止),正在执行的任务直接蒸發。用户看到接口返回了 200,但后端业务根本没执行完。

典型的"表面成功,实际失败"。


二、队列核心概念:三分钟搞懂

在说实现方案之前,先把几个核心概念捋清楚,这些概念是所有队列方案的基石。

2.1 生产者(Producer):任务的制造者

php 复制代码
// 生产者:接收用户请求,往队列里扔一个任务
$queue->push("order_process", [
    "order_id" => 10086,
    "user_id" => 9527,
    "amount" => 299.00,
    "created_at" => time()
]);

生产者的职责就一个:把任务安全地送进队列,然后快速返回。它不应该关心任务什么时候被处理、谁来处理。

2.2 消费者(Consumer):任务的执行者

php 复制代码
// 消费者:从队列里取任务,执行它
while (true) {
    $job = $queue->pop("order_process"); // 阻塞式获取
    $this->processOrder($job["order_id"]);
    $queue->ack($job["id"]); // 确认处理完成
}

消费者的职责:从队列取任务、执行任务、确认完成。如果执行失败,要能重试或者放到死信队列。

2.3 消息持久化:不让消息死于内存

这是最容易忽略的一点。

如果没有持久化:队列服务重启(比如 Redis 宕机重启),队列里的消息全部丢失------相当于你往 ATM 里存了钱,但银行说"服务器重启了,存款记录没了"。

正确的做法

  • Redis :开启 AOF 持久化(appendfsync everysec 以上)
  • RabbitMQ:消息落盘 + 队列持久化
  • 数据库:消息存储在表里(最原始但最保险)

三、PHP 队列实现方案对比

3.1 方案一:文件队列(低配版,玩具级)

把任务写到文件里,消费者定时扫描文件。

php 复制代码
// 生产者:写文件
file_put_contents("/tmp/queue.txt", json_encode($job) . "\n", FILE_APPEND);

// 消费者:读文件
$lines = file("/tmp/queue.txt");
foreach ($lines as $line) {
    $job = json_decode($line, true);
    $this->process($job);
    // 处理完删除行?不好意思,文件不支持原子删除......
}

优点:零依赖,代码简单。

致命缺点

  • 无并发支持(文件锁性能差)
  • 无持久化保证(服务器重启文件可能损坏)
  • 无重试机制
  • 无监控
  • 生产环境用这个,迟早出事。

3.2 方案二:Redis 队列(主流,简单易用)

Redis 的 List 结构天生就是队列:LPUSH 入队,BRPOP 出队。配合 RPOPLPUSH(或 BRPOPLPUSH)还能实现安全的队列转移(从原队列取出的同时备份到处理队列,防止消息丢失)。

php 复制代码
// Redis 队列核心操作
// 入队
$redis->lpush("queue:order", json_encode($job));

// 出队(阻塞,最常用)
$job = $redis->brpop("queue:order", 10); // 阻塞10秒

优点:部署简单(大多数 PHP 项目本来就用 Redis),性能极高,持久化可配置,支持原子操作。

缺点:不支持多消费者共享同一个队列(需要自己做广播),不支持消息确认(需手动实现),cluster 模式下部分命令有坑。

适用场景:中小型项目,单个应用内的异步任务处理。

3.3 方案三:RabbitMQ / Redis Stream(企业级)

特性 Redis 队列 RabbitMQ Redis Stream
消息持久化 AOF/RDB 磁盘 AOF
消息确认 手动实现 ACK 原生支持 XACK 原生支持
多消费者 需 BRPOPLPUSH Exchange 路由原生支持 Consumer Group 原生支持
延迟消息 需 Sorted Set 延迟插件 延迟消息
集群 Redis Cluster 镜像集群 Redis Cluster
运维成本
适用规模 中小 中大

RabbitMQ:功能最全,支持各种路由规则、死信队列、优先级队列。但配置复杂,运维成本高。

Redis Stream (推荐):Redis 5.0 引入,弥补了 List 的不足,支持 Consumer Group(多消费者分片消费)、消息持久化、ACK 确认。我目前项目中的首选。


四、Redis 队列实战代码

下面给出一个基于原生 PHP + Redis 的完整队列实现,包含生产者和消费者两部分。建议收藏,直接抄。

4.1 生产者(投递任务)

php 复制代码
<?php
/**
 * 订单队列 - 生产者
 * 负责将订单任务投递到 Redis 队列
 */

class OrderProducer
{
    private Redis $redis;
    private string $queueName = "queue:order";

    public function __construct()
    {
        $this->redis = new Redis();
        $this->redis->connect("127.0.0.1", 6379);
        $this->redis->auth("your_password"); // 生产环境务必设置密码
    }

    /**
     * 投递订单处理任务
     * @param array $orderData 订单数据
     * @return bool 是否投递成功
     */
    public function dispatch(array $orderData): bool
    {
        $job = [
            "id" => uniqid("order_", true),       // 唯一任务ID,用于去重
            "data" => $orderData,
            "attempts" => 0,                       // 初始重试次数
            "created_at" => date("Y-m-d H:i:s"),
        ];

        try {
            // LPUSH 入队,JSON 序列化存储
            $result = $this->redis->lpush(
                $this->queueName,
                json_encode($job, JSON_UNESCAPED_UNICODE)
            );

            if ($result === false) {
                // 记录投递失败日志
                $this->log("ERROR", "订单投递失败: " . json_encode($orderData, JSON_UNESCAPED_UNICODE));
                return false;
            }

            $this->log("INFO", "订单已投递队列: {$job['id']}");
            return true;

        } catch (RedisException $e) {
            $this->log("ERROR", "Redis异常: " . $e->getMessage());
            return false;
        }
    }

    /**
     * 投递延迟订单处理任务(使用 Redis Sorted Set 实现延迟队列)
     * @param array $orderData 订单数据
     * @param int $delaySeconds 延迟秒数
     * @return bool
     */
    public function dispatchDelay(array $orderData, int $delaySeconds = 30): bool
    {
        $job = [
            "id" => uniqid("order_", true), "data" => $orderData,
            "attempts" => 0, "created_at" => date("Y-m-d H:i:s"),
        ];
        $delayQueueName = "queue:order:delay";
        $executeTime = time() + $delaySeconds; // 延迟到指定时间才执行

        try {
            // ZADD 的正确调用方式:zadd(key, score, member)
            $result = $this->redis->zadd(
                $delayQueueName, $executeTime,
                json_encode($job, JSON_UNESCAPED_UNICODE)
            );

            if ($result === false) {
                $this->log("ERROR", "延迟订单投递失败: " . json_encode($orderData, JSON_UNESCAPED_UNICODE));
                return false;
            }

            $this->log("INFO", "延迟订单已投递: {$job['id']}, 将在 {$delaySeconds} 秒后执行");
            return true;
        } catch (RedisException $e) {
            $this->log("ERROR", "延迟投递失败: " . $e->getMessage());
            return false;
        }
    }

    private function log(string $level, string $message): void
    {
        $timestamp = date("Y-m-d H:i:s");
        echo "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
    }
}

// ============ 使用示例 ============
$producer = new OrderProducer();

// 模拟用户下单接口中调用
$orderData = [
    "order_id" => 10086,
    "user_id" => 9527,
    "goods" => [
        ["id" => 1, "name" => "iPhone 16", "num" => 1, "price" => 8999.00],
        ["id" => 2, "name" => "AirPods Pro", "num" => 1, "price" => 1999.00],
    ],
    "total_amount" => 10998.00,
    "pay_method" => "wechat",
];

$producer->dispatch($orderData);
// 立即返回,用户无需等待库存、通知、日志等操作

4.2 消费者(处理任务)

php 复制代码
<?php
/**
 * 订单队列 - 消费者
 * 持续从 Redis 队列获取任务并执行
 * 建议使用 nohup 或 supervisor 在后台运行
 */

class OrderConsumer
{
    private Redis $redis;
    private string $queueName = "queue:order";
    private string $processingQueueName = "queue:order:processing"; // 处理中队列(安全备份)
    private int $maxAttempts = 3; // 最大重试次数
    private int $popTimeout = 10;  // BRPOPLPUSH 阻塞时间(秒)

    public function __construct()
    {
        $this->redis = new Redis();
        $this->redis->connect("127.0.0.1", 6379);
        $this->redis->auth("your_password");
    }

    /**
     * 启动消费者(阻塞循环)
     */
    public function run(): void
    {
        $this->log("INFO", "订单消费者已启动,监听队列: {$this->queueName}");

        while (true) {
            try {
                // BRPOPLPUSH:从 order 队列取任务,同时放到 processing 队列做备份
                // 这样即使进程崩溃,任务也不会丢失(从 processing 队列恢复)
                $jobJson = $this->redis->brpoplpush(
                    $this->queueName,
                    $this->processingQueueName,
                    $this->popTimeout
                );

                if ($jobJson === null) {
                    // 超时,继续等待
                    continue;
                }

                $job = json_decode($jobJson, true);

                if ($job === null) {
                    $this->log("WARNING", "任务JSON解析失败: " . $jobJson);
                    $this->redis->lrem($this->processingQueueName, 1, $jobJson);
                    continue;
                }

                $this->processJob($job);

            } catch (RedisException $e) {
                $this->log("ERROR", "Redis异常: " . $e->getMessage());
                sleep(5); // 异常时短暂休眠,避免疯狂重试
            } catch (Exception $e) {
                $this->log("ERROR", "处理异常: " . $e->getMessage());
                sleep(1);
            }

            // 处理延迟队列中的到期任务(可以放到定时任务中)
            $this->processDelayQueue();
        }
    }

    /**
     * 处理单个任务
     */
    private function processJob(array $job): void
    {
        $jobId = $job["id"] ?? "unknown";
        $orderData = $job["data"] ?? [];
        $attempts = ($job["attempts"] ?? 0) + 1;

        $this->log("INFO", "开始处理任务: {$jobId} (第{$attempts}次尝试)");

        try {
            // ========== 这里是具体业务逻辑 ==========

            // 1. 校验订单合法性
            $this->validateOrder($orderData);

            // 2. 库存扣减(使用 Lua 脚本保证原子性)
            $this->deductInventory($orderData);

            // 3. 创建订单记录
            $orderId = $this->createOrderRecord($orderData);

            // 4. 发送通知
            $this->sendNotification($orderId, $orderData);

            // 5. 记录操作日志
            $this->logOrder($orderId, $orderData);

            // ========== 业务逻辑结束 ==========

            // 成功:从 processing 队列中移除(确认完成)
            $this->redis->lrem($this->processingQueueName, 1, json_encode($job, JSON_UNESCAPED_UNICODE));
            $this->log("INFO", "任务处理成功: {$jobId}");

        } catch (Exception $e) {
            $this->handleFailure($job, $attempts, $e);
        }
    }

    /**
     * 处理失败逻辑:重试 or 死信
     */
    private function handleFailure(array $job, int $attempts, Exception $e): void
    {
        $jobId = $job["id"] ?? "unknown";
        $this->log("WARNING", "任务处理失败: {$jobId}, 错误: {$e->getMessage()}");

        if ($attempts < $this->maxAttempts) {
            // 还没到最大重试次数,需要指数退避避免无限重试循环
            $backoff = (int) pow(2, $attempts - 1); // 1s, 2s, 4s...
            $this->log("INFO", "任务将在 {$backoff} 秒后重试 ({$attempts}/{$this->maxAttempts}): {$jobId}");
            sleep($backoff);

            // 更新重试计数和最后重试时间
            $job["attempts"] = $attempts;
            $job["last_retry_at"] = date("Y-m-d H:i:s");

            // 从处理队列移除后重新放入主队列
            $this->redis->lrem($this->processingQueueName, 1, json_encode($job, JSON_UNESCAPED_UNICODE));
            $this->redis->lpush($this->queueName, json_encode($job, JSON_UNESCAPED_UNICODE));
        } else {
            // 超过最大重试次数,进入死信队列
            $this->redis->lrem($this->processingQueueName, 1, json_encode($job, JSON_UNESCAPED_UNICODE));
            $this->redis->lpush("queue:order:dead", json_encode([
                "job" => $job,
                "failed_at" => date("Y-m-d H:i:s"),
                "reason" => $e->getMessage(),
                "attempts" => $attempts,
            ], JSON_UNESCAPED_UNICODE));
            $this->log("ERROR", "任务进入死信队列: {$jobId}");
        }
    }

    /**
     * 处理延迟队列中已到期的任务
     * 使用 Sorted Set 实现延迟队列,score 存储到期时间戳
     */
    private function processDelayQueue(): void
    {
        $delayQueueName = "queue:order:delay";
        $now = time();

        // 获取所有到期任务(score <= 当前时间),每次最多取10个
        $jobs = $this->redis->zrangebyscore($delayQueueName, 0, $now, ["limit" => [0, 10]]);

        foreach ($jobs as $jobJson) {
            // 先从延迟队列中移除(原子操作,防止重复消费)
            $removed = $this->redis->zrem($delayQueueName, $jobJson);
            if ($removed > 0) {
                // 成功移除后再投放到主队列
                $this->redis->lpush($this->queueName, $jobJson);
            }
        }
    }

    // ========== 业务方法(示例)==========

    private function validateOrder(array $orderData): void
    {
        if (empty($orderData["order_id"]) || empty($orderData["user_id"])) {
            throw new InvalidArgumentException("订单数据不完整");
        }
    }

    private function deductInventory(array $orderData): void
    {
        // 使用 Lua 脚本保证库存扣减的原子性,防止超卖
        // Lua脚本返回值意义:
        // 大于0:扣减成功(返回剩余库存)
        // 0:库存不足
        // -1:key不存在(商品不存在)
        $luaScript = <<<'LUA'
local stock = redis.call('GET', KEYS[1])
if stock == false then
    return -1  -- 库存 key 不存在
end
stock = tonumber(stock)
if stock < tonumber(ARGV[1]) then
    return 0  -- 库存不足
end
local remaining = redis.call('DECRBY', KEYS[1], ARGV[1])
return remaining  -- 返回剩余库存
LUA;

        foreach ($orderData["goods"] ?? [] as $goods) {
            $goodsId = $goods["id"];
            $num = $goods["num"];
            $key = "goods:{$goodsId}:stock";

            $remaining = $this->redis->eval($luaScript, [$key], 1, $num);

            // PHP判断逻辑:大于0才是成功
            if ($remaining === false || $remaining < 0) {
                throw new RuntimeException("商品 {$goodsId} 库存扣减失败");
            }
            if ((int)$remaining === 0) {
                throw new RuntimeException("商品 {$goodsId} 库存不足");
            }
            // 只有 remaining > 0 才是成功
        }
    }

    private function createOrderRecord(array $orderData): int
    {
        // 实际项目中应该是数据库插入
        // 这里用日志模拟
        $this->log("INFO", "订单记录已创建: order_id={$orderData['order_id']}");
        return $orderData["order_id"];
    }

    private function sendNotification(int $orderId, array $orderData): void
    {
        // 实际项目中是调用短信/推送服务
        // 这里用 sleep 模拟耗时操作
        $this->log("INFO", "通知已发送: order_id={$orderId}");
    }

    private function logOrder(int $orderId, array $orderData): void
    {
        $this->log("INFO", "操作日志已记录: order_id={$orderId}");
    }

    private function log(string $level, string $message): void
    {
        $timestamp = date("Y-m-d H:i:s");
        echo "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
    }
}

// ============ 启动消费者 ============
// 推荐使用 supervisor 管理进程
// 命令行运行:php order_consumer.php
// 守护进程运行:nohup php order_consumer.php > /var/log/order_consumer.log 2>&1 &

$consumer = new OrderConsumer();
$consumer->run();

4.3 ThinkPHP6 队列用法(补充)

如果你使用 ThinkPHP6,可以使用官方 topthink/think-queue 组件,封装度更高:

php 复制代码
<?php
// TP6 生产者
use think\facade\Queue;

public function createOrder()
{
    $orderData = [
        "order_id" => 10086,
        "user_id" => 9527,
        "total_amount" => 10998.00,
    ];

    // 1. 立即执行
    Queue::push(\app\job\OrderJob::class, $orderData, "order");

    // 2. 延迟 30 秒执行(订单超时取消场景)
    Queue::later(30, \app\job\OrderJob::class, $orderData, "order");

    return "订单已提交";
}
php 复制代码
<?php
// TP6 消费者 Job 类
namespace app\job;

use think\queue\Job;

class OrderJob
{
    public function fire(Job $job, array $data): void
    {
        $orderId = $data["order_id"] ?? 0;

        try {
            // 处理订单逻辑
            $this->processOrder($data);

            // 删除任务
            $job->delete();

        } catch (\Exception $e) {
            // 失败后最多重试3次
            if ($job->attempts() > 3) {
                $job->delete(); // 放弃
            } else {
                $job->release(10); // 10秒后重试
            }
        }
    }

    private function processOrder(array $data): void
    {
        // 业务逻辑
    }
}
bash 复制代码
# 命令行启动消费者(TP6)
php think queue:work --queue order --tries 3

五、常见坑及解决方案

坑一:消息丢失------没 ACK,挂了就没了

问题:消费者处理任务时进程崩溃,任务没处理完,但已经从队列里删了。

解决

  1. 使用 BRPOPLPUSH (或 BRPOP)将任务转移到处理中队列,处理完成后再手动删除。
  2. 开启 Redis AOF 持久化appendfsync everysecalways
  3. 重要任务用数据库做消息表 (最保险),状态机:待处理 → 处理中 → 已完成/失败

坑二:消息重复消费------幂等没做,同一个订单扣了两次钱

问题:消费者处理超时,触发了重试,同一个任务被执行了两遍。

解决幂等设计,每个任务带唯一 ID,处理前先查重:

php 复制代码
private function processJob(array $job): void
{
    $jobId = $job["id"];

    // 用 Redis SETNX 做分布式锁,防止并发处理
    $lockKey = "lock:job:{$jobId}";
    $lock = $this->redis->set($lockKey, 1, ["NX", "EX" => 60]);

    if (!$lock) {
        $this->log("WARNING", "任务正在被其他进程处理: {$jobId}");
        return; // 幂等:重复任务直接跳过
    }

    try {
        // 检查是否已处理过(用 Redis 或数据库记录)
        $processedKey = "processed:job:{$jobId}";
        if ($this->redis->exists($processedKey)) {
            $this->log("INFO", "任务已处理过,跳过: {$jobId}");
            return;
        }

        // 执行业务逻辑......
        $this->doRealWork($job["data"]);

        // 标记已处理(设置过期时间,防止内存泄漏)
        $this->redis->setex($processedKey, 86400 * 7, 1); // 7天后自动清除

    } finally {
        $this->redis->del($lockKey);
    }
}

坑三:队列积压------消费者太少,任务越堆越多

问题:高峰期任务大量涌入,消费者处理不过来,队列越来越长,用户感受到明显延迟。

解决

  1. 增加消费者数量 :部署多个消费者实例(使用 supervisor 或 K8s HPA 自动扩缩容)。
  2. 分区(Stream Consumer Group):多个消费者分工处理不同消息。
  3. 监控告警:设置队列长度阈值,超过阈值触发报警。
  4. 限流保护:在入口处做流量控制,避免瞬时洪峰。
php 复制代码
// 监控脚本示例
$queueLength = $redis->llen("queue:order");
if ($queueLength > 10000) {
    // 触发告警(接入钉钉/飞书/Sentry)
    $this->sendAlert("队列积压警告!当前长度: {$queueLength}");
}

坑四:顺序问题------先下单后发货,结果先发了货

问题 :Redis List 是 FIFO 队列,但多消费者并发处理时,先入队的消息不一定先处理完

解决

  1. 单消费者:严格的 FIFO,但吞吐量受限。
  2. 按订单 ID 哈希分区:相同订单的消息始终路由到同一个消费者。
  3. 依赖关系设计 :发货依赖下单完成,用延迟队列状态机控制顺序。
php 复制代码
// 按订单ID哈希确保同一订单消息到同一消费者
$consumerGroup = "order_group_" . (crc32($orderId) % $consumerCount);

六、生产环境 Checklist

上线队列功能前,逐项检查:

  • Redis 开启持久化 (AOF + RDB 双保险,appendfsync everysec
  • Redis 设置密码,禁止公网访问**
  • 消费者进程使用 Supervisor 管理,配置自动重启
  • 消息 ACK 机制已实现(BRPOPLPUSH 方案或手动 ACK)
  • 幂等去重已实现(防重复消费)
  • 死信队列已建立(失败任务有归宿)
  • 队列长度监控告警(超过阈值报警)
  • 消费者处理耗时监控(Prometheus + Grafana)
  • 重试次数限制(避免无限重试)
  • 延迟队列方案已设计(超时取消、延时发货等场景)
  • 消费者支持水平扩展(多个实例部署)
  • 环境配置文件分离(生产密码不通用于代码仓库)

总结

回到开头那个问题:订单总丢,怎么办?

答案不是"多加点日志",也不是"换个更强的服务器"。

答案是在架构层面做异步化:用队列把"用户下单"和"业务处理"解耦。

  • 同步处理:用户等得起,业务等不起。
  • 异步队列:用户秒响应,业务慢慢来。

队列不是什么高大上的技术,但它解决的是最实在的问题:让你的系统在高峰期不崩溃、在故障后不丢数据、在扩容时不多花冤枉钱

当然,队列不是银弹。它引入了复杂性:需要监控、运维、重试机制、幂等设计。但这些都是值得付出的代价------因为相比用户丢单、赔偿纠纷,队列的运维成本低太多了。


行动项清单

  1. 今晚:review 现有代码,找出所有同步处理的高风险点(库存、支付回调、通知)
  2. 本周:引入 Redis,为核心流程配置异步队列(参考文中的生产者/消费者代码)
  3. 本周:实现消息 ACK + 幂等去重机制(参考"坑二"解决方案)
  4. 两周内:配置 Supervisor 管理消费者进程,设置队列长度监控告警
  5. 上线前:完成 Checklist 逐项检查,确认 Redis 持久化和安全配置

相关阅读推荐


💡 有问题?评论区见! 关注作者,第一时间获取 PHP 工程化实战系列文章。

相关推荐
元俭2 小时前
【Eino 框架入门】Backend 是怎么变成工具的
后端
小谢小哥2 小时前
08-Java语言核心-JVM原理-垃圾收集详解
后端
超捻2 小时前
09 python 数据类型 | 列表
后端
SimonKing2 小时前
紧急自查!Apifox被投毒,使用者速看:你的Git、SSH、云密钥可能已泄露
java·后端·程序员
AskHarries2 小时前
他用20年拿下WSBK冠军,而你还没开始做第一个产品
后端
￰meteor3 小时前
23种设计模式 -【抽象工厂】
后端·设计模式
武子康3 小时前
大数据-257 离线数仓 - 数据质量监控详解:从理论到Apache Griffin实践
大数据·hadoop·后端
liangblog3 小时前
Spring Boot中手动实例化 `JdbcTemplate` 并指定 数据源
java·spring boot·后端
羊小猪~~3 小时前
算法/力扣--栈与队列经典题目
开发语言·c++·后端·考研·算法·leetcode·职场和发展