Redis SCAN批量大小对性能的影响
Redis SCAN批量大小对性能的影响
1. 背景
项目中会有一些功能,需要去遍历 Redis 中的一批 Key 的信息,然后通过这一批 Key 去查询对应的数据。
通常就会有 2 种做法:
- 使用 KEYS 命令一次性返回所有匹配的 Key。因为 Redis 的 IO 线程是单线程,所以如果 KEYS 处理大数据量时,其他客户端请求需要排队等待处理。
- 使用 SCAN 命令分批去匹配符合条件的 Key 。 SCAN 命令是非阻塞的,因为在每次调用时只返回一部分结果。这样就可以避免服务器阻塞,可以快速处理其他客户端的请求。
在实际生产环境中,大批 Key 遍历时会优先选择 SCAN 命令,这样不会影响其他业务功能的正常执行。
SCAN 命令是 Redis 中用于遍历集合的一种非阻塞迭代器,用法:
SCAN cursor [MATCH pattern] [COUNT count]
- cursor: 游标。遍历开始时会设置为 0,每次调用后返回新的游标值,直到返回 0 时表示遍历结束。
- MATCH pattern: 这是个可选参数,用于匹配特定模式的 Key。
- COUNT count: 也是个可选参数。 参数是建议每次返回的元素数量,并不保证返回的数量。
举个用法小例子:
127.0.0.1:6379> SCAN 0 MATCH user:* COUNT 10
上面的这个例子就是遍历所有以 【user: 】开头的 Key,每次返回的元素数量控制的是 10。
到了实际的项目环境中, SCAN 命令每一批遍历多少 KEY 比较合适,这个值需要进行测试后才能得到比较准确的数据。
不过测试的目标就是:
- 确保 Redis 的 CPU 和内存使用稳定,不会出现异常高的资源使用。
- 访问速度快。
- 减少通信次数。
2.测试环境准备
- 客户端: Ubuntu + 百兆带宽 + Java应用
- 服务端: 云 Linux 云服务器 + 下行20M带宽
- 地址位置:客户端跟服务端地理位置接近,平均延时 9ms 左右。
- Redis: Redis5 + 20 万左右的 key数据。
- 测试代码: JDK8 + Spring Redis Template 。
注意: 生产环境下应用跟 Redis 通常位于同一区域进行内网通信,不会出现跨公网通信情况。这里只是为了演示 Scan count 对性能的影响
2. 测试步骤
- 往 Redis 中写入 20万条数据,Key 的名称前缀为 TEST_KEY:xxxxxxxxxxx,这里的 xxx 可以用自增的ID代替即可。
- 通过编写 Java 代码,连接 Redis 进行 Scan 操作读取 20 万条数据 Key。
- 尝试切换不同的 Scan count 数值,记录耗时时间。
- 每个 Scan count 重复测试 5 次,读取时间取 5 次时间的平均值。 这样可以避免单次时间过大或者过小引起的数据误差。
3. 测试代码
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.util.StopWatch;
import java.util.ArrayList;
import java.util.List;
public class ScanKeyPerformanceTest {
// Redis key 的前缀
private static final String REDIS_KEY_PREFIX = "TEST_KEY";
// Redis scan 每一批的大小
private static final int SCAN_BATCH_SIZE = 5000;
private final RedisTemplate<String, Object> redisTemplate;
public ScanKeyPerformanceTest(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public static void main(String[] args) {
final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);
final RedisTemplate<String, Object> redisTemplate = context.getBean(RedisTemplate.class);
// 执行测试方法
final ScanKeyPerformanceTest test = new ScanKeyPerformanceTest(redisTemplate);
test.testScanRedisKey();
}
public void testScanRedisKey() {
redisTemplate.execute((RedisConnection connection) -> {
ScanOptions scanOptions = ScanOptions.scanOptions().match(REDIS_KEY_PREFIX + "*").count(SCAN_BATCH_SIZE).build();
Cursor<byte[]> cursor = connection.scan(scanOptions);
// 分配一个大数组存放结果
final List<String> resultKeyList = new ArrayList<>(20 * 10000);
// 循环遍历,拿到符合条件的 Redis key 列表
while (cursor.hasNext()) {
resultKeyList.add(new String(cursor.next()));
}
return null;
});
}
}
4. 测试结果
发送以及请求大小来源于 Wireshark 抓包数据中的统计,统计的是 IPV4层的包大小。
批量条数 (Batch Count) | 平均读取时间 (ms) | 发送请求大小 (kB) | 接收请求大小 (MB) |
---|---|---|---|
500 | 16921 | 192 | 10 |
1000 | 11566 | 138 | 10 |
2000 | 6814 | 78 | 10 |
3000 | 5634 | 63 | 10 |
4000 | 6638 | 51 | 10 |
5000 | 4619 | 48 | 10 |
6000 | 4452 | 43 | 10 |
7000 | 3805 | 45 | 10 |
8000 | 5601 | 43 | 10 |
9000 | 4648 | 39 | 10 |
10000 | 4080 | 37 | 10 |
15000 | 3148 | 32 | 10 |
20000 | 2570 | 30 | 10 |
30000 | 2498 | 37 | 10 |
50000 | 2187 | 36 | 10 |
100000 | 2251 | 35 | 10 |
150000 | 2058 | 44 | 10 |
200000 | 1818 | 40 | 10 |
从上图中可以看出,随着 batch count 的增加,总体读取时间是在呈现下降的趋势,最后直到某个点后趋于稳定。
- 500 到 2000: 这个范围内批处理大小的增加很明显就降低了从 Redis 读取数据的时间,平均时间从 16921ms 降到 6814 ms。
- 2000 到 3000: 读取时间继续降低,但下降幅度没那么大了。
- 3000 到 5000: 读取时间继续降低,显示出批处理大小继续增加的好处。
- 5000 到 6000: 读取时间还在降低。
- 6000 到 7000: 读取时间已经到了 1 万以内的最低时间 3805 ms。
- 7000 到 8000: 读取时间上升到平均 5601 ms,表明增加批处理大小不一定总是能继续降低时间。
- 8000 到 100000: 总体趋势显示读取时间趋于稳定,波动在一个小范围内,基本跟 7000条一批差不多耗时。
- 150000 到 200000: 读取时间看起来是进一步降低,到达最小值 1818 ms 。
可以从上面数据得到的几点小结论:
- 持续增加批处理大小,并不一定会让读取时间减少,反而有时候会变的更慢。
- 批处理大小需要取一个比较平衡的值,过大收益不明显,而且会让 Redis 服务器负载过高。
5. 注意事项
- Scan 命令返回的结果可能包含重复 Key, 调用方必须做去重处理。
- SCAN 命令是多次扫描,这个过程中数据可能会发生变化。所以导致结果集可能不一定完全一致,这个在业务上可以考虑下是否会受影响。