韶关市网站建设_网站建设公司_SEO优化_seo优化
2026/1/12 16:02:50 网站建设 项目流程

好多小伙伴在制作简历时常常带上几个关键词——“能抗住千,百万级流量”“三高架构实战”等。

但只要面试官稍微往深了问:“同学,现在Redis CPU 飙到 90%,作为负责的工程师,你会怎么排查和解决?”十个有八个都得挂。本文将为你梳理一套从容应对、直击要害的“金牌”排查方案。

1.准确定位,找到痛点。

slowlog get [n]:定位慢查询元凶:

Redis的slowlog是一个内存中的日志,记录了执行时间超过指定阈值的命令。这是排查CPU问题的首选利器,因为它能直接告诉你哪些命令是慢的。

# 连接到Redis
redis-cli
# 查看最近的100条慢查询记录
127.0.0.1:6379> slowlog get 100

通常,CPU飙升的罪魁祸首就隐藏在这里。执行结果会告诉你命令的执行时长、具体命令以及参数。你需要重点关注那些执行时间异常长的命令。

127.0.0.1:6379> slowlog get 10 1) 1) (integer) 123 # 慢日志ID:123 2) (integer) 1735689000000 # 执行完成时间戳:2025-01-01 12:30:00 3) (integer) 500000 # 执行耗时:500000微秒 = 500毫秒(超过10毫秒阈值) 4) 1) "KEYS" # 执行的命令:KEYS * 2) "*" 5) "192.168.1.200:65432" # 客户端IP+端口 6) "" # 客户端用户名(空) 2) 1) (integer) 122 2) (integer) 1735688990000 3) (integer) 200000 # 耗时200毫秒 4) 1) "HGETALL" # 执行的命令:HGETALL user_10000(大Hash) 2) "user_10000" 5) "10.0.0.5:43210" 6) "" 。。。。

redis-cli --hotkeys:揪出热点Key:

如果slowlog没有发现明显的慢命令,那问题可能出在访问模式上——某个Key被过于频繁地访问,即“热Key”问题。高QPS会把CPU打满。

较新版本的Redis客户端提供了一个非常方便的工具来发现热点Key:

# --hotkeys 选项会持续扫描,直到你手动停止(Ctrl+C)
redis-cli --hotkeys

这个命令会给你一个实时的热点Key列表,让你对访问热点一目了然。

3.MONITOR:实时监控(谨慎使用!)

MONITOR命令可以实时打印出Redis服务器接收到的所有命令。这是一个非常强大的“照妖镜”,但也是一把双刃剑。

# 实时打印所有流经Redis的命令

【严重警告】:在生产环境,MONITOR会严重影响Redis性能(官方数据是会降低50%以上)。因此,它只应该作为其他方法都无效时的最后手段,并且只能短时间使用,抓取到足够样本后应立即停止。

第二步:根因分析与解决方案

通过第一步的诊断,我们通常会定位到以下三类问题的其中一种。

场景一:高复杂度命令 O(N)

问题表现slowlog中出现大量KEYSHGETALLSMEMBERSSORTSINTER等命令。

这些命令的时间复杂度是O(N)或更高,当操作的数据量巨大时,它们会长时间占用单线程的CPU,阻塞其他所有请求。

解决方案:拆分与替代

  • KEYS *->SCAN 0KEYS是臭名昭著的性能杀手(遍历 Redis 中所有的键(包括过期但未清理的键),返回匹配模式的结果。Redis 单线程的特性决定了:KEYS *执行期间,Redis 主线程被完全占用,无法处理任何其他请求) 必须用SCAN命令来替代。SCAN通过游标进行增量式迭代,不会阻塞线程。虽然需要多次调用,但对整体性能影响极小。
public class ScanReplaceKeys { public static void main(String[] args) { // 连接Redis Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.auth("your_password"); // 初始化游标 String cursor = "0"; // 设置SCAN参数:匹配所有键,每次遍历100个 ScanParams params = new ScanParams().match("*").count(100); // 循环遍历 do { // 执行SCAN ScanResult<String> result = jedis.scan(cursor, params); // 获取本次遍历的键列表 List<String> keys = result.getResult(); // 处理键(比如打印、业务逻辑) for (String key : keys) { System.out.println("遍历到键:" + key); } // 更新游标 cursor = result.getStringCursor(); } while (!"0".equals(cursor)); // 游标为0时结束 // 关闭连接 jedis.close(); } }
  • HGETALL/SMEMBERS->HSCAN/SSCAN:与SCAN同理,用HSCANSSCAN来分批次获取一个大Hash或Set中的元素。
  • 集合操作:对于SINTERSUNION等复杂集合运算,如果数据量巨大,应考虑在客户端分批获取数据后进行计算,而不是让Redis来承担这个压力。

场景二:大Key问题

问题表现slowlog中可能没有执行时间特别长的命令,但通过redis-cli --bigkeys分析或业务逻辑排查,发现某些Key的体积异常巨大(例如,一个包含数百万个元素的Hash,或一个几十MB的String)。

大Key的危害是隐性的,它在序列化/反序列化、网络传输、内存分配和回收(尤其是删除时)都会消耗大量CPU。

解决方案:拆!

  • 大Hash拆分:将一个大的Hash Key,拆分为多个小的Hash Key。例如,将user:1中包含的100万个字段,拆分为user:1:field_group1,user:1:field_group2...
  • 大String/List拆分:将一个巨大的JSON字符串,根据其内部结构拆分到不同的Key中。一个巨大的List可以按ID范围或时间范围进行分段存储。

核心思想是,将一次对大Key的操作,变成多次对小Key的操作。

场景三:热Key问题

问题表现redis-cli --hotkeys发现少数几个Key的访问QPS远超其他Key。

单个Key的请求必须由CPU的一个核来处理,当流量过度集中时,就会造成单点瓶颈。

解决方案:分摊流量

  • 读写分离:这是最直接的思路。通过增加副本(Replica)节点,让读请求被分摊到多个从库上,减轻主库的压力。
  • 多级缓存:在应用层增加本地缓存(如Guava Cache, Caffeine)。对于热点数据,第一次从Redis读取后,会同时写入本地缓存。在后续的极短时间内,绝大部分的读请求都会被本地缓存命中并返回,流量根本到不了Redis。这是应对读热点的绝佳方案。
  • 热Key复制:对于写热点,可以将一个热Key复制成多个副本,例如hotkey->hotkey_1,hotkey_2... 并通过哈希等方式将请求随机打到不同的副本上,从而分摊压力。读取时需要聚合多个副本的数据。
// 总库存是100的话每个副本的库存要变成对应的1/n。 @Component public class HotKeyReplicaService { // 注入StringRedisTemplate(Spring自动装配) @Autowired private StringRedisTemplate stringRedisTemplate; // 副本数量(可配置化,建议放到application.yml) private static final int REPLICA_COUNT = 4; // 库存Key前缀(示例:商品1001的库存) private static final String STOCK_KEY_PREFIX = "stock:1001"; /** * 初始化热Key副本:将总库存均分到多个副本Key中 * @param totalStock 商品总库存 */ public void initStockReplica(int totalStock) { // 计算每个副本的初始库存(均分) int perReplicaStock = totalStock / REPLICA_COUNT; // 批量初始化(减少网络请求) for (int i = 1; i <= REPLICA_COUNT; i++) { String replicaKey = STOCK_KEY_PREFIX + "_" + i; // StringRedisTemplate的set方法:设置键值对 stringRedisTemplate.opsForValue().set(replicaKey, String.valueOf(perReplicaStock)); } } /** * 扣减库存(写操作:路由到随机副本) * @param routeFactor 路由因子(如用户ID、请求ID,保证随机性) * @return 扣减后的副本库存值 */ public Long decrStockReplica(String routeFactor) { // 1. 计算路由索引(0~3 → 转1~4) int index = Math.abs(routeFactor.hashCode()) % REPLICA_COUNT + 1; // 2. 拼接副本Key String replicaKey = STOCK_KEY_PREFIX + "_" + index; // 3. 扣减副本库存(StringRedisTemplate的decrement方法) // 注意:decrement返回扣减后的值,原生Jedis返回的是操作后的值,逻辑一致 return stringRedisTemplate.opsForValue().decrement(replicaKey); } /** * 读取总库存(读操作:聚合所有副本数据) * @return 商品总库存 */ public int getTotalStock() { // 1. 构建所有副本Key的列表 List<String> replicaKeys = new ArrayList<>(); for (int i = 1; i <= REPLICA_COUNT; i++) { replicaKeys.add(STOCK_KEY_PREFIX + "_" + i); } // 2. 批量读取所有副本值(StringRedisTemplate的multiGet,对应原生Jedis的mget) List<String> replicaValues = stringRedisTemplate.opsForValue().multiGet(replicaKeys); // 3. 聚合求和 int totalStock = 0; for (String value : replicaValues) { totalStock += Integer.parseInt(Objects.requireNonNullElse(value, "0")); } return totalStock; } /** * (进阶)原子性扣减库存(防止超卖,用Lua脚本) * @param routeFactor 路由因子 * @param decrement 扣减数量 * @return true-扣减成功,false-库存不足 */ public boolean decrStockAtomically(String routeFactor, int decrement) { // 1. 计算路由索引 int index = Math.abs(routeFactor.hashCode()) % REPLICA_COUNT + 1; String replicaKey = STOCK_KEY_PREFIX + "_" + index; // 2. Lua脚本:保证扣减的原子性(库存≥decrement才扣减) String luaScript = """ local current = tonumber(redis.call('get', KEYS[1])) if current and current >= tonumber(ARGV[1]) then redis.call('decrby', KEYS[1], ARGV[1]) return true else return false end """; // 3. 执行Lua脚本 DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(luaScript); redisScript.setResultType(Boolean.class); // 4. 传入参数:KEYS[1] = 副本Key,ARGV[1] = 扣减数量 return stringRedisTemplate.execute( redisScript, List.of(replicaKey), String.valueOf(decrement) ); } }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询