记一次redisson RMap带来的cpu暴涨

事件的缘由:在一次线上数据访问过多的场景下出发了redis-cpu暴涨,经过排查发现是有大量的HSCAN,再经过一番业务的排查,发现是如下代码引起的

RMap<String, String> map = redissonClient.getMap(key);
map.entrySet().stream().map(x -> Integer.valueOf(x.getKey())).collect(Collectors.toList());

经过上述的源码分析发现Redisson 的 RMap 实现原理如下:
Redisson 是一个用于 Redis 的 Java 客户端,它提供了许多高级数据结构,例如 RMap。在调用 RMap.entrySet() 时,Redisson 需要遍历 Redis 中的 Hash 表(或类似结构)。由于 Redis 的 HGETALL 等命令无法处理非常大的数据集,Redisson 使用 HSCAN 来实现增量遍历。

Redisson RMap 的 entrySet 实现

以下是一个简化的 RMap.entrySet() 实现的示例:

public Set<Map.Entry<String, String>> entrySet() {
    Set<Map.Entry<String, String>> entries = new HashSet<>();
    int cursor = 0;
    do {
        MapScanResult<String, String> scanResult = scan(cursor, SCAN_COUNT);
        entries.addAll(scanResult.getMap());
        cursor = scanResult.getPos();
    } while (cursor != 0);
    return entries;
}

scan(int cursor, int count):一个内部方法,使用 Redis 的 HSCAN 命令来获取部分结果。
SCAN_COUNT:每次扫描的条目数。
scan 方法示例

private MapScanResult<String, String> scan(int cursor, int count) {
    RFuture<MapScanResult<String, String>> future = commandExecutor.readAsync(getName(), codec, RedisCommands.HSCAN, getName(), cursor, "COUNT", count);
    return future.get();
}

commandExecutor.readAsync:执行 Redis 命令的异步方法。
RedisCommands.HSCAN:表示 Redis 的 HSCAN 命令。

产生多次 SCAN 的原因

当调用 RMap.entrySet() 时,Redisson 需要遍历整个 Hash 表。由于 Redis 的 SCAN 命令每次只能返回部分结果,为了获取全部数据,Redisson 必须多次调用 SCAN,每次传入上一次的游标值,直到遍历完所有条目。

因此解决上述问题的关键点是减少 Redis 的 SCAN 操作

解决办法
Map<String, String> valueMap = map.readAllMap();

使用 readAllMap() 可以避免上述问题的主要原因在于它一次性读取所有数据并返回一个 Java Map,而不是逐条扫描和处理。这减少了 Redis 的 SCAN 命令调用次数,降低了 Redis 服务器的压力。

readAllMap() 的工作原理

readAllMap() 方法在内部执行一个单一的 Redis 命令(如 HGETALL)来获取所有数据,而不是使用 SCAN 命令分批次地获取。这意味着它可以一次性获取整个 Hash 表的内容,从而避免了多次 SCAN 的问题。

RMap<String, String> map = redissonClient.getMap("myMap");
Map<String, String> allData = map.readAllMap();

原理对比

  • entrySet() 方法:
- 多次 SCAN:entrySet() 方法在 Redisson 中需要遍历整个 Hash 表,通常会通过多次 HSCAN 命令来获取所有条目。
- 高频次 IO:由于 Redis 的 HSCAN 命令每次只能返回部分结果,为了获取所有数据,需要多次调用,导致高频次 IO 操作。
- 潜在性能问题:在大数据集的情况下,频繁的 SCAN 操作会增加 Redis 服务器的负载,可能导致性能问题。
  • readAllMap() 方法:
- 单次读取:readAllMap() 方法在内部执行一个类似于 HGETALL 的命令,一次性获取所有数据。
- 低频次 IO:由于只需要一次 IO 操作,减少了网络通信的频次,降低了 Redis 服务器的压力。
- 高效:在数据量不大的情况下,HGETALL 可以高效地返回所有数据。

源码分析
以下是 readAllMap() 方法的简化实现示例:

public Map<K, V> readAllMap() {
    RFuture<Map<K, V>> future = commandExecutor.readAsync(getName(), codec, RedisCommands.HGETALL, getName());
    return future.join();
}

commandExecutor.readAsync:执行异步命令的方法。
RedisCommands.HGETALL:表示 Redis 的 HGETALL 命令。
在 readAllMap() 中,Redisson 只需要执行一次 HGETALL 命令即可获取所有键值对。

示例代码

import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.redisson.Redisson;
import org.redisson.config.Config;

import java.util.Map;

public class RedisExample {

    public static void main(String[] args) {
        // 配置和初始化 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redissonClient = Redisson.create(config);

        // 获取 RMap 实例
        RMap<String, String> map = redissonClient.getMap("myMap");

        // 使用 readAllMap() 一次性获取所有数据
        Map<String, String> allData = map.readAllMap();

        // 打印结果
        allData.forEach((key, value) -> System.out.println(key + ": " + value));

        // 关闭 Redisson 客户端
        redissonClient.shutdown();
    }
}

总结
避免多次 SCAN:readAllMap() 方法通过一次性读取所有数据,避免了多次 SCAN 操作,减少了 Redis 服务器的压力。
减少 IO 操作:一次性获取数据的方式减少了网络通信的频次,提高了性能。
适用场景:适用于数据量相对较小的场景。在数据量非常大的情况下,HGETALL 可能会导致内存占用增加,需要谨慎使用。
通过使用 readAllMap() 方法,可以显著提高读取 Redis 数据的效率,避免 Redis 内存和 CPU 过度消耗的问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容