批量操作减少网络往返:RTT 优化实战指南

批量操作减少网络往返:RTT 优化实战指南

RTT 问题的本质

RTT(Round-Trip Time,往返时间)是网络请求从客户端发出到收到回复的总时间。对于 Redis 这样的内存数据库,命令本身的执行时间在微秒级,而网络延迟通常在毫秒级。

一次数据操作耗时 = 网络 RTT + Redis 执行时间 + 客户端处理时间
                  ~1ms           ~0.01ms      ~0.01ms

网络耗时占了 99%!

RTT 的影响有多大

数值对比

场景 100 次操作 1000 次操作 10000 次操作
顺序单次(内网 0.5ms RTT) 50ms 500ms 5s
顺序单次(公网 20ms RTT) 2s 20s 200s
批量操作(一次 RTT) 0.5ms 0.5ms 0.5ms

公网场景更加明显

如果 Redis 部署在云服务商的不同可用区,甚至不同地域,RTT 可能达到 10-50ms。这时每条命令 1 次 RTT 的开销是灾难性的。

四种批量操作方案

方案一:Pipeline

# 适用:无依赖的批量操作
def batch_set_user(redis, users):
    """批量写入用户数据"""
    pipe = redis.pipeline()
    for uid, data in users.items():
        pipe.set(f"user:{uid}:name", data['name'])
        pipe.set(f"user:{uid}:age", data['age'])
        pipe.set(f"user:{uid}:city", data['city'])
    pipe.execute()  # 3*N 次 SET = 1 次网络往返

方案二:MGET / MSET

# 适用:多条 GET/SET 操作
# 不用 Pipeline,直接使用原生命令
r.mset({
    'user:1:name': 'Alice',
    'user:1:age': '30',
    'user:1:city': 'Beijing'
})

# 批量读取
names = r.mget('user:1:name', 'user:2:name', 'user:3:name')

MSET/MGET 是 Redis 内建的批量操作命令,效率比 Pipeline 更高。

方案三:Lua 脚本

# 适用:有逻辑依赖的批量操作
script = """
for i, key in ipairs(KEYS) do
    redis.call('SET', key, ARGV[i])
end
return #KEYS
"""
r.eval(script, 3, 'k1', 'k2', 'k3', 'v1', 'v2', 'v3')

方案四:Unix Socket

# 适用:Redis 在本地部署
r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')

Unix Socket 避免了 TCP 协议栈开销,延迟可以从 0.5ms 降到 0.05ms,RTT 减少 10 倍。

实战:批量导入 100 万条数据

import redis

r = redis.Redis(host='localhost', port=6379)

# ❌ 错误做法:逐条导入
import time
start = time.time()
for i in range(1000000):
    r.set(f"data:{i}", f"value:{i}")
print(f"逐条导入: {time.time() - start:.2f}s")
# 约 280 秒 (内网环境)

# ✅ 正确做法:Pipeline 分批导入
start = time.time()
pipe = r.pipeline()
BATCH_SIZE = 1000
for i in range(1000000):
    pipe.set(f"data:{i}", f"value:{i}")
    if (i + 1) % BATCH_SIZE == 0:
        pipe.execute()
        pipe = r.pipeline()
# 最后一轮
pipe.execute()
print(f"Pipeline 导入: {time.time() - start:.2f}s")
# 约 8 秒 (内网环境)

速度提升 35 倍!

减少网络往返的其他技巧

1. 合并相似操作

# ❌ 分开操作
for uid in [1, 2, 3]:
    r.hset(f"user:{uid}", "online", "1")

# ✅ 合并操作
pipe = r.pipeline()
for uid in [1, 2, 3]:
    pipe.hset(f"user:{uid}", "online", "1")
pipe.execute()

2. 使用字符串代替事务

# ❌ 多个命令
r.incr('views:article:1')
r.expire('views:article:1', 3600)

# ✅ Lua 脚本合并为一个发送
script = """
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
"""
r.eval(script, 1, 'views:article:1', 3600)

3. 连接复用

# ❸ 使用连接池复用连接
pool = redis.ConnectionPool(host='localhost', port=6379, max_connections=50)
r = redis.Redis(connection_pool=pool)
# 每次操作都复用已有连接,减少 TCP 握手

优化效果量度表

优化手段 RTT 减少幅度 适用场景
Pipeline 80-95% 无依赖批量操作
MSET/MGET 90-98% 简单批量读写
Lua 脚本 80-99% 有依赖的批处理
Unix Socket 90% 本地部署
连接池 减少握手 高频短连接
数据合并 50-90% 结构化数据

面试要点

  • RTT 是 Redis 的主要延迟来源(命令执行只有微秒级)
  • Pipeline 是最常用的批量优化手段
  • MSET/MGET 比 Pipeline 更高效(内建命令,协议更紧凑)
  • Lua 脚本适合有逻辑依赖的场景
  • 批量操作要合理控制每批大小(建议 500-2000 条)
© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容