批量操作减少网络往返: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


暂无评论内容