Redis Cluster主节点挂了,为什么"高可用"还全员掉线?我把三次kill的记录翻出来了

中午刷掘金,看到一篇《Redis集群切换主节点时,服务竟然全员掉线》,订单系统瘫了8分钟。评论区挺热闹,有人说Redis Cluster是伪高可用,有人说那是客户端没用对。

两种说法我都不太服气,但也没底气反驳------"failover期间到底发生了什么",我自己的知识停留在"从节点会顶上来"这一句,顶多再补个"会有几秒抖动"。几秒是怎么构成的,抖动的时候客户端收到的是什么,这些全是别人嚼过的结论。 正好手头有台1核2G的沙盒闲着,Redis源码编译一份,搭个3主3从,把主节点亲手kill掉,逐秒把记录扒出来看。

环境与第一次kill

Redis是我从官网拉的stable源码包现场编译的------stable包拉下来是什么版本就是什么版本,编译出来8.10.2,可在download.redis.io自行复核。6个节点跑在127.0.0.1的7000-7005端口,redis-cli --cluster create建集群,每个主带一个从。关键配置只有一行值得说:cluster-node-timeout 5000,5秒判定超时,官方教程的示例值,故意选小一点让failover别等太久。

建好之后的拓扑长这样:

makefile 复制代码
127.0.0.1:7000 master --- slots 0-5460      (从节点:7004)
127.0.0.1:7001 master --- slots 5461-10922  (从节点:7005)
127.0.0.1:7002 master --- slots 10923-16383 (从节点:7003)

实验脚本干两件事:一个业务线程用redis-py的RedisCluster每200毫秒做一次set+get,模拟线上读写;一个监控线程每300毫秒抓一次CLUSTER NODES,盯节点flags的变化。跑到第5秒,对7000的进程kill -9------注意是kill -9,模拟进程崩溃,不是优雅退出。

先看业务线程的错误记录,从kill到恢复一共12次报错,全部是这个:

vbnet 复制代码
[   5.86s] BIZ-ERR | order:1027 -> ConnectionError: Error 111 connecting to 127.0.0.1:7000. Connection refused.
[   6.48s] BIZ-ERR | order:1030 -> ConnectionError: Error 111 connecting to 127.0.0.1:7000. Connection refused.
...
[  13.33s] BIZ-ERR | order:1063 -> ConnectionError: Error 111 connecting to 127.0.0.1:7000. Connection refused.

错误窗口7.47秒,12次错误,之后业务自动恢复,没有再错。有个细节比窗口本身更有意思:这12次错误不是连续的。order:1027挂在7000的slot上,order:1028落在7001,正常返回;order:1029落在7002,也正常。所谓"全员掉线",在这7秒里其实是"打到0-5460这5461个slot上的请求全挂,其他slot活得好好的"。

再对照监控线程抓到的flags变化,时间线就完整了:

ini 复制代码
[   5.33s] kill -9(7000进程死亡)
[  12.68s] 节点7000 flags=master,fail?    ← PFAIL:主观下线,开始拉票
[  13.28s] 节点7000 flags=master,fail     ← FAIL:过半主节点确认
[  14.18s] 节点7004 flags=master          ← 选举完成,7004晋升接管0-5460

对着Redis官方的cluster specification拆一下这三个阶段。kill之后,其余节点 ping 7000 不通,但并不会立刻判死刑------要等超过cluster-node-timeout(我配的5秒)才标成PFAIL,也就是那个fail?问号,意思是"可能挂了,待确认"。PFAIL只是单个节点的私下怀疑,要等gossip消息传播开、过半主节点都表示"我也连不上他",才升级成FAIL。然后7004发起选举,向其余主节点要授权票,拿到过半票后晋升新主,把slot映射的新配置广播出去。我记录里fail?到fail只隔了0.6秒,比规范里写的等待上限快得多------多数派ACK来得快时,FAIL确认和选举就是亚秒级的事。

再看客户端在这7秒里干了什么。redis-py的RedisCluster默认行为是:撞上ConnectionError先把请求标记失败,同时触发一次集群拓扑重初始化(reinitialize),刷新完slot映射再重试。所以我的业务线程并不是"每200毫秒必错一次",而是错几次、修一次拓扑、再错几次,12次报错分散在7秒里,这就是为什么错误明细看起来断断续续。如果换成不做拓扑刷新、也不重试的裸客户端,这7秒里的报错会密得多,恢复也得靠应用自己兜底------同一个集群,客户端策略不同,体感完全两样。

所以7.47秒的构成大致是:5秒左右的故障检测(node-timeout)+ 1到2秒的PFAIL升级FAIL + 不到1秒的选举和广播。我的记录里PFAIL出现在kill后7.35秒,比5秒的配置值晚,gossip传播和重连重试都要吃时间,这个延后是正常的。

集群端在这几秒里说了什么

客户端报ConnectionError很好理解------进程都没了,TCP连接自然被拒。但我想知道的是,故障期间去找别的节点 问这些slot,集群自己会怎么回答。于是又跑了一轮:kill掉7000之后,站在7001上反复读一个落在0-5460的key(slot 2117),不带-c参数,看原始返回:

ini 复制代码
t=1s:  MOVED 2117 127.0.0.1:7000        ← 拓扑还没变,仍指旧主
t=2~7s: MOVED 2117 127.0.0.1:7000
t=8s:  CLUSTERDOWN The cluster is down   ← 旧主被标FAIL,slot无主
t=9s:  MOVED 2117 127.0.0.1:7000
(下一轮探测里)MOVED 2117 127.0.0.1:7004  ← 7004晋升,指向新主

注意t=8秒那个CLUSTERDOWN。这是集群在明说:这个slot现在没有主,整个集群处于fail状态。cluster-require-full-coverage默认是yes,只要有一部分slot没有健康节点负责,集群整体就进入fail------这些无主的slot固然不能服务,这是后面第三轮实验的伏笔。

同一时刻我加了带-c的对照:redis-cli -c会自动跟随重定向,于是它拿着MOVED给出的地址去连7000,撞上Connection refused,报错翻倍------一次是集群的重定向,一次是死节点的拒绝。看日志排查时这两类报错交替出现,很容易被误判成两个问题,其实是同一条链路的前后两跳。

至于为什么中间那段是MOVED而不是直接CLUSTERDOWN:7000刚死的时候,别的节点还不知道,路由表里0-5460的主人仍然是7000,所以返回MOVED让你去连7000,连过去就是Connection refused。客户端拿到MOVED重定向再撞墙,这就是很多业务报错日志里"MOVED和ConnectionError交替出现"的由来。

优雅切换为什么可以做到零报错

崩溃讲完了,我加了组对照:不kill,对从节点执行redis-cli -p 7000 cluster failover,让7004计划内接管7000。业务线程全程没停,结果125次请求,0次报错。

这不是运气。CLUSTER FAILOVER的流程官方文档写得很清楚:从节点先告诉主节点别处理写请求,等复制流完全追平,再发起选举晋升,旧主之后对客户端返回重定向。整个切换过程中slot始终有主,客户端最多被重定向一次,感知不到中断。

对比之下就很清楚了:kill -9跳过了"追平复制流"这一步,集群只能靠故障检测+选举流程慢慢把slot重新安排出来,这个流程天然带着node-timeout量级的窗口。掉线不是Redis Cluster的bug,是崩溃场景下这套安全机制的固定成本。

第三次实验:真正的"全员掉线"

三轮实验不是一口气连着跑的,中间沙盒回收过Redis进程,第三轮开始时集群重启归位:0-5460的主又变回7004,7000重新以从节点身份归队,主从配置回到完整状态。这次我做得更狠:先把7004 kill掉,让7000自动晋升接管0-5460,不等集群喘口气,立刻把刚上岗的7000也kill掉------这下0-5460连一个从节点都没有了。

对照组我特意留了一个落在5461-10922的key(k0,slot 8579,健康主7001负责)。结果:

ini 复制代码
[第二次kill后]
t=1s  slot2117(无主):      MOVED 2117 127.0.0.1:7000   ← 指向死节点
t=1s  slot8579(7001负责):  v                            ← 正常读
t=1s  写入测试:            OK                           ← 正常写
...(前7秒一切如常)...
t=8s  slot2117(无主):      CLUSTERDOWN The cluster is down
t=8s  slot8579(7001负责):  CLUSTERDOWN The cluster is down   ← 健康slot也挂了
t=8s  写入测试:            CLUSTERDOWN The cluster is down
...(直到把7000重新拉起来才恢复)

前7秒,健康slot还在正常读写;第8秒开始,整个集群所有slot、包括完全健康的7001负责的部分,读写全部返回CLUSTERDOWN。一个没有从节点的主挂掉,把整个集群拖进了fail状态。

这就是"全员掉线"的集群侧真相。两个默认配置联手造成的:cluster-require-full-coverage yes让任何slot无主时集群整体进入fail;cluster-allow-reads-when-down no让fail状态下节点停止一切服务,包括读。设计动机官方也写了------防止你在不知情的时候读到不一致的数据。但代价就是,热文里那种"主节点挂了且failover没能完成"的场景下,故障是全集群扩散的,而且会持续到slot重新有主为止------如果从节点数据太旧被cluster-replica-validity-factor拦下、或者干脆没有从节点,这个窗口就不是几秒,是几分钟到无限长。热文那8分钟,我猜大概率落在这一类(当然只是猜测,人家没贴集群配置)。

能配置的账

三轮实验做完,能给的都是配置层面的账,而且每一笔都对应着代价,没有魔法。 最直接的冲动是把cluster-node-timeout调小------检测快了,选举快了,窗口自然短。但这个值同时也管着"多久怀疑邻居挂了",我试过把数值往1秒压,结果网络稍微抖一下集群就互相怀疑,没完没了触发本不该发生的failover,比慢更伤。生产上5到15秒之间按业务敏感度选,别学我。

第三轮那种局面,唯一的防法是保证每个主节点真的有 健康的从节点。这里有个容易被忽略的细节:cluster-replica-validity-factor默认10,从节点断连超过node-timeout的十倍时长,就失去选举资格。也就是说一个长期断线、勉强连回来的从节点,平时看着像高可用的一部分,真出事那一刻是被挡在投票门外的。监控里除了盯主节点,从节点的复制延迟也得一起盯。

最后是那两个默认值。cluster-require-full-coverage no加cluster-allow-reads-when-down yes,能让第三轮的局面变成"坏slot报错、好slot照常服务"。代价是官方注释里明说的:你得接受可能读到不一致的数据,而且业务侧要真的按key维度处理报错,而不是一刀切告警。我的看法是,缓存类场景这两个开关值得一试,碰交易数据就算了------"部分可用"和"全局一致"哪个值钱,得看你的key背后是什么。

客户端这边,redis-py这类智能客户端拿到MOVED会刷新拓扑,ConnectionError后会重连重试,我第一轮实验里7.47秒后自动恢复靠的就是这个。但如果客户端不做重试、或者连接池把坏连接缓存住不放,掉线时长就会被客户端策略放大------同一个集群,不同的客户端配置,掉线时长可以差出一个数量级。

写到这里基本可以把开头的问题收掉了:Redis Cluster的高可用是slot级别的、靠异步检测和选举实现的最终一致,主节点崩溃必有一个node-timeout量级的窗口;窗口内是"部分slot报错"还是"全集群CLUSTERDOWN",取决于failover能不能完成、以及require-full-coverage这组配置怎么给。四组实验的脚本都留在文末,复制就能复现。信结论不如信日志,有空自己跑一遍。

附录:完整脚本与运行前提

运行前提,三样东西:

  1. Redis 用官网 stable 源码包编译:curl -O https://download.redis.io/redis-stable.tar.gz,解压后 make,产物在 redis-stable/src/ 下。我编译出来是 8.10.2,你拉包时是什么版本就是什么版本,机制部分不受小版本影响;2. pip install redis,redis-py 的 RedisCluster 客户端;3. 脚本里的 OUT_PATH 和 BASE_DATA 写的是我机器上的绝对路径,跑之前改成你自己的目录。 脚本1:搭集群(setup_cluster.sh):
bash 复制代码
#!/bin/bash
# 搭建3主3从Redis Cluster实验环境(端口7000-7005,数据在~/redis-cluster-test)
BASE=~/redis-cluster-test
mkdir -p $BASE
for p in 7000 7001 7002 7003 7004 7005; do
  mkdir -p $BASE/node$p
  cat > $BASE/node$p/redis.conf <<EOF
port $p
bind 127.0.0.1
daemonize yes
pidfile $BASE/node$p/redis.pid
dir $BASE/node$p
save ""
appendonly no
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
logfile $BASE/node$p/redis.log
EOF
  ~/redis-build/redis-stable/src/redis-server $BASE/node$p/redis.conf
done
sleep 1
~/redis-build/redis-stable/src/redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1 --cluster-yes
echo "=== 集群状态 ==="
~/redis-build/redis-stable/src/redis-cli -p 7000 cluster nodes | sort -t: -k2 -n

脚本2:kill主节点+客户端/监控双线程(第一轮) (failover_experiment.py):

python 复制代码
# -*- coding: utf-8 -*-
"""failover实验:kill主节点,逐0.2s探测客户端表现,逐0.3s记录集群flags变化"""
import subprocess, threading, time, sys
from redis import Redis
from redis.cluster import RedisCluster, ClusterNode

BASE_DATA = "/root/redis-cluster-test"
OUT_PATH = "/Coze/Drive/AI助理/所有对话/主对话/文章创作/文章初稿/20261003/实验产物/failover_experiment.txt"
OUT = open(OUT_PATH, "w", buffering=1)

T0 = time.time()
def ts():
    return round(time.time() - T0, 2)

def log(kind, detail):
    line = f"[{ts():7.2f}s] {kind:5s} | {detail}"
    OUT.write(line + "\n")
    print(line)

def build_cluster_client(**kw):
    nodes = [ClusterNode("127.0.0.1", p) for p in range(7000, 7006)]
    return RedisCluster(startup_nodes=nodes, **kw)

# ---------- 阶段1:预写数据 ----------
rc = build_cluster_client()
for i in range(200):
    rc.set(f"order:{i}", i)
log("INIT", "预写200个key完成")

# ---------- 监控线程:记录7000及其从节点的flags ----------
def monitor(stop_evt):
    admin = Redis(host="127.0.0.1", port=7001, decode_responses=True)
    while not stop_evt.is_set():
        try:
            raw = admin.execute_command("CLUSTER", "NODES")
            for line in raw.splitlines():
                parts = line.split()
                if ":7000" in parts[1] or ":7004" in parts[1]:
                    port = parts[1].split(":")[1].split("@")[0]
                    flags = parts[2]
                    log("MON", f"节点{port} flags={flags}")
        except Exception as e:
            log("MON-ERR", f"{type(e).__name__}: {str(e)[:60]}")
        time.sleep(0.3)

# ---------- 业务线程:持续set/get ----------
rc_biz = build_cluster_client()  # 默认配置(带重试/拓扑刷新)
stop = threading.Event()

def biz(stop_evt, label):
    i = 1000
    while not stop_evt.is_set():
        try:
            rc_biz.set(f"order:{i}", str(i))
            v = rc_biz.get(f"order:{i}")
            log("BIZ-OK", f"{label} set/get order:{i} -> {v}")
        except Exception as e:
            log("BIZ-ERR", f"{label} order:{i} -> {type(e).__name__}: {str(e)[:90]}")
        i += 1
        time.sleep(0.2)

tm = threading.Thread(target=monitor, args=(stop,), daemon=True)
tb = threading.Thread(target=biz, args=(stop, "默认客户端"), daemon=True)
tm.start(); tb.start()

time.sleep(5)
# ---------- kill 7000 主节点 ----------
pid = open(BASE_DATA + "/node7000/redis.pid").read().strip()
log("KILL", f"kill -9 {pid} (端口7000主节点)")
subprocess.run(["kill", "-9", pid])

time.sleep(55)  # 观察failover全过程
stop.set()
time.sleep(1)

# ---------- 输出统计 ----------
lines = open(OUT_PATH).read().splitlines()
biz_errs = [l for l in lines if "BIZ-ERR" in l]
biz_oks = [l for l in lines if "BIZ-OK" in l]
if biz_errs:
    t_first_err = float(biz_errs[0].split("]")[0].strip("[ s"))
    t_last_err = float(biz_errs[-1].split("]")[0].strip("[ s"))
    first_ok_after = None
    for l in biz_oks:
        t = float(l.split("]")[0].strip("[ s"))
        if t > t_first_err:
            first_ok_after = t
            break
    log("STAT", f"首个业务错误: {t_first_err:.2f}s (kill于5.00s)")
    log("STAT", f"最后错误: {t_last_err:.2f}s, 错误后首个成功: {first_ok_after}")
    log("STAT", f"错误窗口: {t_last_err - t_first_err:.2f}s, 错误请求数: {len(biz_errs)}")
    from collections import Counter
    cnt = Counter(l.split("-> ")[-1].split(":")[0].strip() for l in biz_errs)
    for k, v in cnt.items():
        log("STAT", f"  错误类型 {k}: {v}次")
OUT.close()
print("DONE")

脚本3:故障期间集群侧返回探测(第二轮) (clusterdown_probe.sh):

bash 复制代码
#!/bin/bash
# 补充实验:kill 7000后,从7001视角逐秒观察slot(0-5460)请求返回什么
RB=~/redis-build/redis-stable/src; BASE=~/redis-cluster-test
for p in 7000 7001 7002 7003 7004 7005; do $RB/redis-server $BASE/node$p/redis.conf 2>/dev/null; done
sleep 25
echo "cluster_state: $($RB/redis-cli -p 7001 cluster info | grep cluster_state | tr -d '\r')"
# 找一个slot落在0-5460的key
KEY=""
for i in $(seq 0 30); do
  s=$($RB/redis-cli -p 7001 cluster keyslot "order:$i" | tr -d '\r')
  if [ "$s" -lt 5461 ] 2>/dev/null; then KEY="order:$i"; echo "选用key=$KEY slot=$s"; break; fi
done
[ -z "$KEY" ] && KEY="order:0" && echo "fallback order:0 slot=$($RB/redis-cli -p 7001 cluster keyslot order:0)"
PID=$(cat $BASE/node7000/redis.pid)
echo "kill -9 $PID at $(date +%H:%M:%S.%N | cut -c1-11)"
kill -9 $PID
for t in $(seq 1 14); do
  echo "--- t=${t}s plain(7001): $($RB/redis-cli -p 7001 GET $KEY 2>&1 | head -1)"
  echo "    t=${t}s -c模式(7001): $($RB/redis-cli -p 7001 -c GET $KEY 2>&1 | head -1)"
  sleep 1
done
echo "=== 7004晋升验证 ==="
$RB/redis-cli -p 7001 cluster nodes | grep -E ':700[04]'

脚本4:优雅切换对照,125请求0错误(manual_failover_experiment.py):

python 复制代码
# -*- coding: utf-8 -*-
"""第二轮:优雅切换(CLUSTER FAILOVER)对比实验,BIZ线程持续观察是否报错"""
import subprocess, threading, time
from redis import Redis
from redis.cluster import RedisCluster, ClusterNode

BASE_DATA = "/root/redis-cluster-test"
OUT_PATH = "/Coze/Drive/AI助理/所有对话/主对话/文章创作/文章初稿/20261003/实验产物/manual_failover_experiment.txt"
OUT = open(OUT_PATH, "w", buffering=1)
T0 = time.time()

def log(kind, detail):
    line = f"[{time.time()-T0:7.2f}s] {kind:5s} | {detail}"
    OUT.write(line + "\n"); print(line)

def build_cluster_client(**kw):
    nodes = [ClusterNode("127.0.0.1", p) for p in range(7000, 7006)]
    return RedisCluster(startup_nodes=nodes, **kw)

rc = build_cluster_client()
for i in range(200):
    rc.set(f"order:{i}", i)
log("INIT", "预写200个key完成")

rc_biz = build_cluster_client()
stop = threading.Event()
def biz():
    i = 5000
    while not stop.is_set():
        try:
            rc_biz.set(f"order:{i}", str(i))
            v = rc_biz.get(f"order:{i}")
            log("BIZ-OK", f"set/get order:{i} -> {v}")
        except Exception as e:
            log("BIZ-ERR", f"order:{i} -> {type(e).__name__}: {str(e)[:90]}")
        i += 1
        time.sleep(0.2)

tb = threading.Thread(target=biz, daemon=True); tb.start()
time.sleep(5)

# 当前0-5460的主是7004(第一轮failover后),它的从节点是重新归队的7000
log("CMD", "redis-cli -p 7000 cluster failover (对7004的从节点发起优雅切换)")
r = subprocess.run(["/root/redis-build/redis-stable/src/redis-cli", "-p", "7000", "cluster", "failover"],
                   capture_output=True, text=True)
log("CMD", f"cluster failover 返回: {r.stdout.strip()} {r.stderr.strip()}")

time.sleep(20)
stop.set(); time.sleep(1)

lines = open(OUT_PATH).read().splitlines()
errs = [l for l in lines if "BIZ-ERR" in l]
oks = [l for l in lines if "BIZ-OK" in l]
log("STAT", f"总请求数(成功): {len(oks)}, 错误数: {len(errs)}")
for e in errs[:10]:
    log("STAT-ERR", e)
OUT.close()
print("DONE")

脚本5:双杀实验,健康slot也CLUSTERDOWN(第四轮) (double_kill_probe.sh):

bash 复制代码
#!/bin/bash
# 双杀实验:kill master(有slave)→正常failover;立即kill新master(无slave)→全集群CLUSTERDOWN?
RB=~/redis-build/redis-stable/src; BASE=~/redis-cluster-test
for p in 7000 7001 7002 7003 7004 7005; do $RB/redis-server $BASE/node$p/redis.conf 2>/dev/null; done
echo "等待收敛让7000回归为slave..."
sleep 22
echo "收敛后拓扑: "; $RB/redis-cli -p 7001 cluster nodes | grep -E ':700[014]' | awk '{print " ", $2, $3, $9}'
echo "cluster_state: $($RB/redis-cli -p 7001 cluster info | grep cluster_state | tr -d '\r')"
KEY=$(echo "order:2")  # slot 2117, 属于0-5460 (7004负责)
K2=""
for i in $(seq 0 30); do
  s=$($RB/redis-cli -p 7001 cluster keyslot "k$i" | tr -d '\r')
  if [ "$s" -ge 5461 ] && [ "$s" -le 10922 ] 2>/dev/null; then K2="k$i"; $RB/redis-cli -p 7001 set $K2 v >/dev/null; echo "健康slot对照组key=$K2 slot=$s (7001负责)"; break; fi
done
P4=$(cat $BASE/node7004/redis.pid)
echo "[第一次kill] kill -9 7004(master,0-5460,有slave 7000) at $(date +%H:%M:%S)"
kill -9 $P4
for t in $(seq 1 12); do
  echo "  t=${t}s slot2117(7004原负责): $($RB/redis-cli -p 7001 GET $KEY 2>&1 | head -1)"
  sleep 1
done
echo "failover后: $($RB/redis-cli -p 7001 cluster nodes | grep -E ':7000@|:7004@' | awk '{print $2, $3}')"
P0=$(cat $BASE/node7000/redis.pid)
echo "[第二次kill] kill -9 7000(新master,0-5460,无slave) at $(date +%H:%M:%S)"
kill -9 $P0
for t in $(seq 1 13); do
  echo "  t=${t}s slot2117(无主): $($RB/redis-cli -p 7001 GET $KEY 2>&1 | head -1)"
  echo "  t=${t}s slot$($RB/redis-cli -p 7001 cluster keyslot $K2)(7001健康slot): $($RB/redis-cli -p 7001 GET $K2 2>&1 | head -1)"
  echo "  t=${t}s 写入测试(7001健康slot): $($RB/redis-cli -p 7001 SET $K2 v$t 2>&1 | head -1)"
  sleep 1
done
echo "=== 恢复:重启7000 ==="
$RB/redis-server $BASE/node7000/redis.conf; sleep 6
$RB/redis-cli -p 7001 cluster info | grep -E 'cluster_state'
$RB/redis-cli -p 7001 GET $K2 | head -1
相关推荐
笃行3501 小时前
KingbaseES 数据加密全解:从 SSL 到全密态
数据库
Ivanqhz1 小时前
激活函数在 Transformer 中的作用及各种变体简述
java·linux·数据库·人工智能·深度学习
java1234_小锋2 小时前
【技术专题】Mysql8 数据库 - Mysql8 数据类型简介
数据库·mysql
知行EDI3 小时前
知行之桥 MaBang 端口使用指南——Create Order 订单创建篇
java·服务器·数据库
成旭先生3 小时前
【2026】企业信息模糊查询 API 实战:名称、注册号、统一社会信用代码、企业类型与法人一次查全
服务器·数据库·数据服务
JosieBook3 小时前
【数据库】MySQL 实战精通系列 · 第10篇:分库分表与分布式事务实战
数据库·分布式·mysql
写后端的胖头鱼3 小时前
一文详解Cache Aside(旁路缓存模式)
redis·缓存·延迟双删·缓存一致
我叫洋洋4 小时前
Cadence CIS 元器件库合并实战:3000+ 焊盘、156 个符号零冲突并入自有库
数据库·单片机·嵌入式硬件·oracle·电路
AI 编程助手GPT4 小时前
GPT-6 Luna (Batch) 批量处理性能与质量深度评测
数据库·gpt·batch