一、慢查询到底是什么鬼?

大家应该都有过这样的体验:明明是个简单的查询操作,却莫名其妙卡了好几秒。这种情况在Redis里就叫"慢查询"。Redis默认会把执行时间超过10毫秒的命令记录下来,这个阈值可以通过slowlog-log-slower-than参数调整。

举个例子,假设我们有个用户积分排行榜:

# 技术栈:Redis
# 获取排行榜前100名(正常情况)
ZREVRANGE user:scores 0 99 

# 危险操作:获取全部用户(当用户量很大时)
ZREVRANGE user:scores 0 -1

第二个命令在用户量达到百万级别时,可能会变成慢查询。Redis是单线程的,这种慢查询会阻塞其他命令的执行,就像高速公路上的车祸现场,后面所有车都得等着。

二、如何揪出这些慢查询?

Redis提供了非常方便的慢查询日志功能。我们来看看具体怎么操作:

# 查看当前慢查询配置
CONFIG GET slowlog*

# 设置记录慢查询的阈值(单位微秒,这里设置为5毫秒)
CONFIG SET slowlog-log-slower-than 5000

# 设置最多保存多少条慢查询记录
CONFIG SET slowlog-max-len 1000

# 查看慢查询日志
SLOWLOG GET 10  # 获取最近10条慢查询

慢查询日志会显示这些信息:

  1. 唯一ID
  2. 命令执行时间戳
  3. 执行耗时(微秒)
  4. 命令和参数

我曾经遇到过一个案例:某个O(N)复杂度的命令在大数据量下执行缓慢,通过慢查询日志很快定位到了问题。

三、常见慢查询的优化套路

3.1 大Key问题

大Key是指包含大量元素的复合类型Key,比如:

  • 包含百万字段的Hash
  • 超长List/Sorted Set
# 危险的大Key操作
HGETALL huge:hash  # 获取包含百万字段的Hash所有内容

# 优化方案1:分批获取
HSCAN huge:hash 0 COUNT 100

# 优化方案2:考虑拆分数据结构
# 把 huge:hash 拆分成 hash:part1, hash:part2...

3.2 复杂度过高的命令

有些命令的时间复杂度是O(N),数据量大时就会很慢:

# 危险操作
KEYS *  # O(N)复杂度,会扫描所有key

# 优化方案:使用SCAN命令
SCAN 0 MATCH "user:*" COUNT 100

3.3 不合理的数据结构选择

# 错误示范:用List存储需要频繁查询中间元素的数据
LPUSH mylist item1 item2 item3...  # 插入很快
LINDEX mylist 50000  # 但查询中间元素很慢

# 优化方案:改用Sorted Set
ZADD myset 1 item1 2 item2 3 item3...
ZRANGE myset 50000 50000  # 查询效率更高

四、进阶优化技巧

4.1 使用Lua脚本减少网络开销

-- 技术栈:Redis + Lua
-- 统计某个Hash中值大于100的字段数量
local count = 0
local fields = redis.call('HKEYS', KEYS[1])
for _, field in ipairs(fields) do
    local value = redis.call('HGET', KEYS[1], field)
    if tonumber(value) > 100 then
        count = count + 1
    end
end
return count

这个脚本避免了多次网络往返,但要注意:

  1. 脚本不要太复杂
  2. 执行时间不能太长

4.2 Pipeline批量操作

# 技术栈:Redis
# 普通操作(多次网络往返)
SET key1 value1
SET key2 value2
SET key3 value3

# 使用Pipeline(一次网络往返)
MULTI
SET key1 value1
SET key2 value2
SET key3 value3
EXEC

4.3 合理使用持久化策略

# 如果对性能要求极高,可以考虑关闭持久化
CONFIG SET save ""  # 关闭RDB
CONFIG SET appendonly no  # 关闭AOF

# 但这样会牺牲数据安全性,建议根据业务需求权衡

五、实战案例分析

最近优化过一个电商平台的购物车系统,原始方案是这样的:

# 每个用户的购物车是一个Hash
HSET cart:user1 item1 1 item2 2 item3 5...

# 获取购物车所有商品
HGETALL cart:user1  # 当商品数量很多时变慢

优化方案:

  1. 拆分购物车数据,热门商品单独存储
  2. 使用SCAN代替HGETALL
  3. 对购物车商品数量设置上限

优化后,购物车查询速度从平均200ms降到了5ms以下。

六、监控与预警

除了被动查看慢查询日志,我们还应该主动监控:

# 使用redis-cli监控命令延迟
redis-cli --latency -h 127.0.0.1 -p 6379

# 使用INFO命令获取统计信息
redis-cli INFO stats | grep total_commands_processed

建议设置监控告警,当慢查询数量突增时及时通知。

七、总结与建议

经过上面的分析,我们可以得出几个关键结论:

  1. 预防胜于治疗:设计阶段就要考虑性能问题
  2. 监控不能少:建立完善的监控体系
  3. 优化要循序渐进:不要一次性做太多改动
  4. 测试很重要:任何优化都要经过充分测试

最后记住:Redis优化没有银弹,要根据具体业务场景选择最合适的方案。有时候最简单的解决方案反而是最有效的,比如增加服务器内存或者升级硬件配置。