如何优雅的进行Redis批量操作--增加,删除,模糊查询?这个追求必须要有!

1,226 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

需求背景

虽然说redis是纯内存操作,效率非常高,但是一次插入或者删除千万级或者亿级的操作,如果采用单条处理的api,整体处理效率还是很低的;另外,如果处理的数据量过大,稍有不慎可能就会导致client端的内存溢出或者服务端的负载过高,基于此,下面提供几种优雅的批量操作的jedis api供批量操作场景使用。

另外,本文整理了下redis的相关知识也有另外一层考虑,就是在考虑接下来公司技术架构演进和重构可能涉及到的一些技术层面的调研。这部分内容相关的分析以及说明会在文末和大家讨论,如果对redis批量操作相关的知识比较熟悉的朋友可以直接跳过中间直接到文章的最后来参与讨论。

具体实现

下面将从批量添加,批量查询以及批量删除三个方面提供例子:

批量添加

批量添加有两种方式,即使用jedis自带的api进行操作,以及使用jedis的Pipeline api来进行操作

jedis自带api

可以使用jedis自带的zadd(zset),sadd(set)和hmset(hash)方法来进行批量操作处理,其中sadd方法value可以传入一个可变长度的string,具体实现的时候可以传一个数组,通过List的toArray方法实现,zadd方法传入一个存放Key以及Score方法的map,hmset方法传入一个存放KV的map即可,但是上述方法有一个问题需要注意,需要控制数组或者map的大小,如果是循环插入数据,防止数据过大导致client端OOM

sadd

List<String> list = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
  list.add(i+"");
}
jedis.sadd("testSet",list.toArray(new String[list.size()]));

zadd

Map<String,Double> map = new HashMap<>();
for (int i = 0; i < 100; i++) {
  map.put(i+"",Double.valueOf(i));
}
jedis.zadd("testZSet",map);

hmset

Map<String,String> map = new HashMap<>();
for (int i = 0; i < 100; i++) {
  map.put(i+"",i+"");
}
jedis.hmset("testHash",map);

Pipeline

redis的管道(Pipelining)操作是一种异步的访问模式,一次发送多个指令,不同步等待其返回结果。这样可以取得非常好的执行效率,具体代码如下:

jedis = jedispool.getResource();
Pipeline pip = jedis.pipelined();
long startTime = System.currentTimeMillis();
for (int i = start; i < end; i++) {
  pip.set((10000000 + i) + "", (10000000 + i) + "");
}
pip.sync();

批量删除

批量删除即使用jedis自带的api操作即可(zset按照score来删除使用专用的api,不在此文讨论范围之内),只是value参数传递一个可变长度的字符串来处理,具体实现的时候可以传一个数组,通过List的toArray方法实现,同样需要控制数组或者map的大小,如果是循环插入数据,防止数据过大导致client端OOM,具体代码如下:

//kv
jedis.del(list.toArray(new String[list.size()]));
//hash
jedis.hdel("testHash",list.toArray(new String[list.size()]));
//set
jedis.srem("testSet",list.toArray(new String[list.size()]));
//zset
jedis.zrem("testZSet",list.toArray(new String[list.size()]));

模糊查询

理论上来说有两种方式可以实现模糊查询,一种是jedis的keys方法,另外一种是使用scan方式来迭代处理模糊查询,但是keys方法只是理论上的实现方式,无法在生产环境的大数据量场景下使用,所以scan就成了唯一通用的靠谱选择:

jedis的keys(不建议使用)

该方法返回所有匹配表达式的key:

KEYS * 匹配数据库中所有 key 。
KEYS j?va 匹配单一字符:如 java , jeva 和 juva 等。
KEYS j*va 匹配0-那个字符:如 jva 和 jaaaaava 等。
KEYS j[ae]va 匹配给定的单个字符:如 java 和 jeva ,但不匹配 juva 。

功能看起来比较丰富,但是实际应用中有时候会出现需要遍历redis中的所有键值的需求,比如清理没用的键等等。但是keys这个命令性能真的很差,而且会使相关数据加锁,从而影响其他操作,按照redis官方的说法:

Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout. Don’t use KEYS in your regular application code. If you’re looking for a way to find keys in a subset of your keyspace, consider using SCAN or sets.

由于执行keys命令,redis会锁定数据,如果数据庞大的话可能需要几秒或更长,对于生产服务器上锁定几秒这绝对是灾难了,所以不建议在生产服务器上使用keys命令,尤其是大数据量的场景下。

scan(建议使用)

说完不建议使用的,再来说说建议使用的scan,从redis的官方文档上看,2.8版本之后SCAN命令已经可用,允许使用游标从keyspace中检索键。对比KEYS命令,虽然SCAN无法一次性返回所有匹配结果,但是却规避了阻塞系统这个高风险,从而也让一些操作可以放在server节点上执行。

需要注意的是,SCAN 命令是一个基于游标的迭代器。SCAN 命令每次被调用之后, 都会向用户返回一个新的游标,用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。同时,使用SCAN,用户还可以使用keyname模式和count选项对命令进行调整。SCAN相关命令还包括SSCAN 命令、HSCAN 命令和 ZSCAN 命令,分别用于集合、哈希键及有续集等。

下面是样例代码:

String cursor = ScanParams.SCAN_POINTER_START;
ScanParams scanParams = new ScanParams();
scanParams.match(prefix);
scanParams.count(10000);
while (true) {
  //使用scan命令获取数据,使用cursor游标记录位置,下次循环使用
  ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
  cursor = scanResult.getStringCursor();// 返回0 说明遍历完成
  List<String> list = scanResult.getResult();
  for(String s : list){
    //TODO 业务逻辑
  }
  if ("0".equals(cursor)) {
    break;
  }
}

后记

虽然redis是一个基于内存的缓存神器,操作速度奇快,但是单次操作奇快也架不住数以千万甚至亿级的操作,此时如果还是用迭代的单条操作可能带来灾难性的后果,所以除了单条操作外,建议在实现中尽量使用批量操作。

接下来说说在公司架构演进和重构层面上关于redis的考虑。

之前公司的业务流转强烈依赖于ElasticSearch,但是我们的场景更偏向于实时的OLAP,全文搜索涉及的非常少。这种业务场景在数据量不大的情况下,还是可以玩得转的。如果数据量变大后,ElasticSearch显然没办法很好的hold住纷繁复杂的OLAP场景,往往心有余而力不足,毕竟ElasticSearch的主业是搜索引擎,而数据聚合更像是个“赠品”。

在这种情况下,选择一个纯正的OLAP数据库,如ClickHouse可能是个更好的选择。而事实上ClickHouse确实也能很好的完成这方面的工作。但是针对为数不多的全文搜索场景,ClickHouse同样无能为力。

所以在ClickHouse carry住了我们当前大部分OLAP场景的前提下,需要提供全文检索能力来支持剩余的全文查询场景,也为整个架构提供未来可能的扩展性需求。

虽然在全文搜索引擎的选型中,ElasticSearch和Solr是不二的选择,但是两者的成本相对于我们的使用场景来说基本上算是杀鸡用牛刀,所以不到万不得已我是不会麻烦这两位搜索引擎界的泰斗。

回到Redis的话题,很长一段时间之前,当Redis升级到4.0版本的时候,Redis开始对外提供Modules以支持在Redis基础上的各种插件和扩展。Redis Modules 是 redis 4.0 引入的一种扩展机制。用户可以通过实现 redis module 提供的 C api 接口为 redis 服务添加定制化功能,从此Redis走上了花活迭起的新时代。

而RediSearch是一个高性能的全文搜索引擎,可作为一个Redis Module 运行在Redis上,是由RedisLabs团队开发的,这个组件的出现为数据量适中,内存和存储空间有限的场景下的全文搜索提供了一个新的思路和选择。

初始版的RediSearch如同其他新生事物一样,成长期会面临一堆问题和烦恼,频繁的版本迭代也令很多追求者望而却步。

但是几年过去了,RediSearch也砥砺前行了很久,是时候重新评估下了,希望能带来惊喜,用以解决当前我们面临的困境。如果有成功经验后续会通过文章分享,如果大家已经有了相关的经验也欢迎在后台给我留言,我也学习学习。

文章到这里就结束了,最后路漫漫其修远兮,大数据之路还很漫长。如果想一起大数据的小伙伴,欢迎点赞转发加关注,下次学习不迷路,我们在大数据的路上共同前进!