业务场景
碰到一个业务场景,一个预约系统,前面大的访问量都被大佬的组件拦住,然后暂存到redis中,然后我再找个时间段去将redis中的数据取出,持久化到数据库中
思路分析
对以上问题进行初步简化,即从redis中获取大批量数据,引申出来的问题就是,如何保障大批量数据稳定取出并保存,如果一次性取出,有可能内存溢出,用时太长时遇到网络抖动会丢失数据等等。
首先想到的当然是分治,就是取一批数据异步存入数据库的同时,再去执行下次相同操作,即使某批数据出错,影响也可在可控范围内。
其实需求一有,我大佬就告诉我用redis
中的scan
命令来实现,该命令借用runoob菜鸟教程
中的说法就是:
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
SCAN 返回一个包含两个元素的数组, 第一个元素是用于进行下一次迭代的新游标, 而第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回 0 表示迭代已结束。
简而言之,scan
命令就一个游标迭代器,下次迭代会根据上次得到的游标值继续进行。 借助sacn
该特性,就可以做到分批在redis
中拉取数据了。
代码实现
public void getDataFromRedisAndSaveToDb()
{
// 根据机器能力开启线程池
ThreadFactory springThreadFactory = new CustomizableThreadFactory("redis pull n save pool-Thread-");
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CPU_NUM, CPU_NUM * 2, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(CPU_NUM * 3), springThreadFactory);
List execList;
String count = "1000";
String cursorId = "0";
// redis命令
RedisScript<List> redisScript = RedisScript.of("return redis.call('scan',KEYS[1],'count',ARGV[1])", List.class);
RedisSerializer serializer = redisTemplate.getStringSerializer();
List<Object> valueList;
do {
//执行
execList = redisTemplate.execute(redisScript, serializer, serializer, Collections.singletonList(cursorId), count);
// 返回值的1)表示下次要开始的游标位置,该游标仅在redis内部有参考价值,返回值2)表示满足正则表达式的key值集合
assert execList != null;
cursorId = String.valueOf(execList.get(0));
//id的集合
List<String> keyList = (List<String>) execList.get(1);
// 游标值返回0,表示 整个数据集(collection)已经被完整遍历过了,称这个过程为一次完整遍历(full iteration)
//scan 命令有重复风险,借助set去重,获取到key后再批量获取value
valueList = redisTemplate.opsForValue().multiGet(new HashSet<>(keyList));
//todo 此处开线程,多次将valueList存入数据库
}
//该处执行次数与分块大小有关
while (!"0".equals(cursorId));
}
注意事项
- scan命令返回的游标值
cursorId
,并不像在有序数组中的某个有意义地址,它只在redis内部有参考价值 - 每次遍历回来的
key
值数据,有可能存在重复
思路拓展
未完待续
参考资料
对scan
命令的深入了解:scan-redis命令参考
更多样的Java对scan
命令的实现:在RedisTemplate中使用scan代替keys指令