1. Overview
1.概述
Collections are an essential building block typically seen in almost all modern applications. So, it’s no surprise that Redis offers a variety of popular data structures such as lists, sets, hashes, and sorted sets for us to use.
集合是几乎所有现代应用程序中的一个基本构建块。因此,Redis 提供了各种流行的数据结构,如列表、集、哈希和排序集,供我们使用,这并不奇怪。
In this tutorial, we’ll learn how we can effectively read all available Redis keys that match a particular pattern.
在本教程中,我们将学习如何有效地读取所有符合特定模式的可用Redis键。
2. Explore Collections
2.探索收藏
Let’s imagine that our application uses Redis to store information about balls used in different sports. We should be able to see information about each ball available from the Redis collection. For simplicity, we’ll limit our data set to only three balls:
让我们设想一下,我们的应用程序使用Redis来存储不同运动中使用的球的信息。我们应该能够从Redis集合中看到关于每个球的信息。为了简单起见,我们将把我们的数据集限制为只有三个球。
- Cricket ball with a weight of 160 g
- Football with a weight of 450 g
- Volleyball with a weight of 270 g
As usual, let’s first clear our basics by working on a naive approach to exploring Redis collections.
像往常一样,让我们首先清除我们的基础知识,用天真的方法来探索Redis集合。
3. Naive Approach Using redis-cli
3.使用redis-cli的天真方法
Before we start writing Java code to explore the collections, we should have a fair idea of how we’ll do it using the redis-cli interface. Let’s assume that our Redis instance is available at 127.0.0.1 on port 6379, for us to explore each collection type with the command-line interface.
在我们开始写Java代码来探索集合之前,我们应该对如何使用redis-cli接口来做这件事有一个合理的想法。让我们假设我们的 Redis 实例在 127.0.0.1 端口 6379 上可用,以便我们用命令行接口探索每个集合类型。
3.1. Linked List
3.1.链接列表
First, let’s store our data set in a Redis linked list named balls in the format of sports-name_ball-weight with the help of the rpush command:
首先,让我们在rpush命令的帮助下,将我们的数据集存储在一个名为balls的Redis链接列表中,格式为sports-name_ball-weight。
% redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> RPUSH balls "cricket_160"
(integer) 1
127.0.0.1:6379> RPUSH balls "football_450"
(integer) 2
127.0.0.1:6379> RPUSH balls "volleyball_270"
(integer) 3
We can notice that a successful insertion into the list outputs the new length of the list. However, in most cases, we’ll be blind to the data insertion activity. As a result, we can find out the length of the linked list using the llen command:
我们可以注意到,成功插入列表后会输出列表的新长度。然而,在大多数情况下,我们会对数据插入活动视而不见。因此,我们可以使用llen命令来找出链表的长度。
127.0.0.1:6379> llen balls
(integer) 3
When we already know the length of the list, it’s convenient to use the lrange command to retrieve the entire data set easily:
当我们已经知道列表的长度时,使用lrange命令可以很方便地检索到整个数据集。
127.0.0.1:6379> lrange balls 0 2
1) "cricket_160"
2) "football_450"
3) "volleyball_270"
3.2. Set
3.2.设置
Next, let’s see how we can explore the data set when we decide to store it in a Redis set. To do so, we first need to populate our data set in a Redis set named balls using the sadd command:
接下来,让我们看看当我们决定将数据集存储在Redis集时,我们如何探索数据集。要做到这一点,我们首先需要使用sadd命令将我们的数据集填充到名为 balls 的 Redis 集中。
127.0.0.1:6379> sadd balls "cricket_160" "football_450" "volleyball_270" "cricket_160"
(integer) 3
Oops! We had a duplicate value in our command. But, since we were adding values to a set, we don’t need to worry about duplicates. Of course, we can see the number of items added from the output response-value.
哎呀!我们的命令里有一个重复的值。但是,由于我们是在向一个集合添加值,我们不需要担心重复的问题。当然,我们可以从输出的响应值中看到添加的项目数量。
Now, we can leverage the smembers command to see all the set members:
现在,我们可以利用smembers命令来查看所有的集合成员。
127.0.0.1:6379> smembers balls
1) "volleyball_270"
2) "cricket_160"
3) "football_450"
3.3. Hash
3.3. 哈希
Now, let’s use Redis’s hash data structure to store our dataset in a hash key named balls such that hash’s field is the sports name and the field value is the weight of the ball. We can do this with the help of hmset command:
现在,让我们使用Redis的哈希数据结构,将我们的数据集存储在一个名为 “球 “的哈希键中,这样,哈希的字段是运动名称,字段值是球的重量。我们可以在hmset命令的帮助下做到这一点。
127.0.0.1:6379> hmset balls cricket 160 football 450 volleyball 270
OK
To see the information stored in our hash, we can use the hgetall command:
要查看存储在我们的哈希值中的信息,我们可以使用hgetall/em>命令。
127.0.0.1:6379> hgetall balls
1) "cricket"
2) "160"
3) "football"
4) "450"
5) "volleyball"
6) "270"
3.4. Sorted Set
3.4.排序的集合
In addition to a unique member-value, sorted-sets allows us to keep a score next to them. Well, in our use case, we can keep the name of the sport as the member value and the weight of the ball as the score. Let’s use the zadd command to store our dataset:
除了唯一的成员值之外,排序集还允许我们在它们旁边保留一个分数。那么,在我们的用例中,我们可以把运动的名称作为成员值,把球的重量作为分数。让我们使用zadd命令来存储我们的数据集。
127.0.0.1:6379> zadd balls 160 cricket 450 football 270 volleyball
(integer) 3
Now, we can first use the zcard command to find the length of the sorted set, followed by the zrange command to explore the complete set:
现在,我们可以首先使用zcard命令来查找排序后的集合的长度,然后使用zrange命令来探索完整集合。
127.0.0.1:6379> zcard balls
(integer) 3
127.0.0.1:6379> zrange balls 0 2
1) "cricket"
2) "volleyball"
3) "football"
3.5. Strings
3.5.字符串
We can also see the usual key-value strings as a superficial collection of items. Let’s first populate our dataset using the mset command:
我们也可以把通常的键值字符串看作是一个表面的项目集合。让我们首先使用mset命令来填充我们的数据集。
127.0.0.1:6379> mset balls:cricket 160 balls:football 450 balls:volleyball 270
OK
We must note that we added the prefix “balls:” so that we can identify these keys from the rest of the keys that may be lying in our Redis database. Moreover, this naming strategy allows us to use the keys command to explore our dataset with the help of prefix pattern matching:
我们必须注意到,我们添加了前缀 “balls:” ,这样我们就可以将这些键从我们Redis数据库中可能存在的其他键中识别出来。此外,这种命名策略允许我们使用keys命令,在前缀模式匹配的帮助下探索我们的数据集。
127.0.0.1:6379> keys balls*
1) "balls:cricket"
2) "balls:volleyball"
3) "balls:football"
4. Naive Java Implementation
4.天真的Java实现
Now that we have developed a basic idea of the relevant Redis commands that we can use to explore collections of different types, it’s time for us to get our hands dirty with code.
现在我们已经对相关的Redis命令有了一个基本的概念,我们可以用这些命令来探索不同类型的集合,现在是时候让我们动手写代码了。
4.1. Maven Dependency
4.1.Maven的依赖性
In this section, we’ll be using the Jedis client library for Redis in our implementation:
在这一节中,我们将在实现中使用Jedis客户端库为Redis。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
4.2. Redis Client
4.2 Redis客户端
The Jedis library comes with the Redis-CLI name-alike methods. However, it’s recommended that we create a wrapper Redis client, which will internally invoke Jedis function calls.
Jedis库自带了Redis-CLI的类似名字的方法。然而,建议我们创建一个封装的Redis客户端,它将在内部调用Jedis的函数调用。
Whenever we’re working with Jedis library, we must keep in mind that a single Jedis instance is not thread-safe. Therefore, to get a Jedis resource in our application, we can make use of JedisPool, which is a threadsafe pool of network connections.
每当我们使用Jedis库时,我们必须牢记,单个Jedis实例不是线程安全的。因此,为了在我们的应用程序中获得Jedis资源,我们可以使用JedisPool,它是一个线程安全的网络连接池。
And, since we don’t want multiple instances of Redis clients floating around at any given time during the life cycle of our application, we should create our RedisClient class on the principle of the singleton design pattern.
而且,由于我们不希望在应用程序的生命周期中的任何时候都有多个Redis客户端实例漂浮在空中,我们应该根据singleton设计模式的原则创建我们的RedisClient类。
First, let’s create a private constructor for our client that’ll internally initialize the JedisPool when an instance of RedisClient class is created:
首先,让我们为我们的客户端创建一个私有构造函数,当RedisClient类的实例被创建时,它将在内部初始化JedisPool。
private static JedisPool jedisPool;
private RedisClient(String ip, int port) {
try {
if (jedisPool == null) {
jedisPool = new JedisPool(new URI("http://" + ip + ":" + port));
}
} catch (URISyntaxException e) {
log.error("Malformed server address", e);
}
}
Next, we need a point of access to our singleton client. So, let’s create a static method getInstance() for this purpose:
接下来,我们需要一个访问我们的单子客户端的点。因此,让我们为这个目的创建一个静态方法getInstance()。
private static volatile RedisClient instance = null;
public static RedisClient getInstance(String ip, final int port) {
if (instance == null) {
synchronized (RedisClient.class) {
if (instance == null) {
instance = new RedisClient(ip, port);
}
}
}
return instance;
}
Finally, let’s see how we can create a wrapper method on top of Jedis’s lrange method:
最后,让我们看看如何在Jedis的lrange方法之上创建一个包装方法。
public List lrange(final String key, final long start, final long stop) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.lrange(key, start, stop);
} catch (Exception ex) {
log.error("Exception caught in lrange", ex);
}
return new LinkedList();
}
Of course, we can follow the same strategy to create the rest of the wrapper methods such as lpush, hmset, hgetall, sadd, smembers, keys, zadd, and zrange.
当然,我们可以按照同样的策略来创建其余的封装方法,如lpush、hmset、hgetall、sadd、smembers、keys、zadd和zrange。
4.3. Analysis
4.3.分析报告
All the Redis commands that we can use to explore a collection in a single go will naturally have an O(n) time complexity in the best case.
我们可以用来一次性探索一个集合的所有Redis命令,在最好的情况下自然会有O(n)的时间复杂性。
We are perhaps a bit liberal, calling this approach as naive. In a real-life production instance of Redis, it’s quite common to have thousands or millions of keys in a single collection. Further, Redis’s single-threaded nature brings more misery, and our approach could catastrophically block other higher-priority operations.
我们也许有点自由,称这种方法为天真。在现实生活中的Redis生产实例中,在一个集合中拥有数千或数百万个键是很常见的。此外,Redis 的单线程特性带来了更多痛苦,我们的方法可能会灾难性地阻塞其他更优先的操作。
So, we should make it a point that we’re limiting our naive approach to be used only for debugging purposes.
所以,我们应该把我们的天真方法限制在只用于调试的范围内。
5. Iterator Basics
5.迭代器基础知识
The major flaw in our naive implementation is that we’re requesting Redis to give us all of the results for our single fetch-query in one go. To overcome this issue, we can break our original fetch query into multiple sequential fetch queries that operate on smaller chunks of the entire dataset.
我们的天真实现的主要缺陷是,我们要求Redis一次性给我们的单个fetch-query的所有结果。为了克服这个问题,我们可以把原来的获取查询分成多个连续的获取查询,对整个数据集的小块进行操作。
Let’s assume that we have a 1,000-page book that we’re supposed to read. If we follow our naive approach, we’ll have to read this large book in a single sitting without any breaks. That’ll be fatal to our well-being as it’ll drain our energy and prevent us from doing any other higher-priority activity.
让我们假设我们有一本1000页的书要读。如果我们按照我们天真的方法,我们将不得不在没有任何休息的情况下一次性阅读这本大书。这对我们的健康是致命的,因为它会耗尽我们的精力,使我们无法进行任何其他更优先的活动。
Of course, the right way is to finish the book over multiple reading sessions. In each session, we resume from where we left off in the previous session — we can track our progress by using a page bookmark.
当然,正确的方法是在多个阅读时段完成这本书。在每一个环节中,我们从上一个环节离开的地方继续阅读–我们可以通过使用页面书签跟踪我们的进度。
Although the total reading time in both cases will be of comparable value, nonetheless, the second approach is better as it gives us room to breathe.
尽管两种情况下的总阅读时间将具有可比性,但尽管如此,第二种方法更好,因为它给了我们喘息的空间。
Let’s see how we can use an iterator-based approach for exploring Redis collections.
让我们看看我们如何使用基于迭代器的方法来探索Redis集合。
6. Redis Scan
6.Redis扫描
Redis offers several scanning strategies to read keys from collections using a cursor-based approach, which is, in principle, similar to a page bookmark.
Redis提供了几种扫描策略,使用基于游标的方法从集合中读取键,原则上类似于页面书签。
6.1. Scan Strategies
6.1.扫描策略
We can scan through the entire key-value collection store using the Scan command. However, if we want to limit our dataset by collection types, then we can use one of the variants:
我们可以使用扫描命令来扫描整个键值集合存储。然而,如果我们想通过集合类型来限制我们的数据集,那么我们可以使用其中一个变体。
- Sscan can be used for iterating through sets
- Hscan helps us iterate through pairs of field-value in a hash
- Zscan allows an iteration through members stored in a sorted set
We must note that we don’t really need a server-side scan strategy specifically designed for the linked lists. That’s because we can access members of the linked list through indexes using the lindex or lrange command. Plus, we can find out the number of elements and use lrange in a simple loop to iterate the entire list in small chunks.
我们必须注意,我们并不真的需要一个专门为链表设计的服务器端扫描策略。这是因为我们可以使用lindex或lrange命令通过索引访问链表的成员。另外,我们可以找出元素的数量,并在一个简单的循环中使用lrange,以小块的方式遍历整个列表。
Let’s use the SCAN command to scan over keys of string type. To start the scan, we need to use the cursor value as “0”, matching pattern string as “ball*”:
让我们使用SCAN命令来扫描字符串类型的键。为了开始扫描,我们需要使用光标值为 “0”,匹配模式字符串为 “ball*”。
127.0.0.1:6379> mset balls:cricket 160 balls:football 450 balls:volleyball 270
OK
127.0.0.1:6379> SCAN 0 MATCH ball* COUNT 1
1) "2"
2) 1) "balls:cricket"
127.0.0.1:6379> SCAN 2 MATCH ball* COUNT 1
1) "3"
2) 1) "balls:volleyball"
127.0.0.1:6379> SCAN 3 MATCH ball* COUNT 1
1) "0"
2) 1) "balls:football"
With each completed scan, we get the next value of cursor to be used in the subsequent iteration. Eventually, we know that we’ve scanned through the entire collection when the next cursor value is “0”.
每完成一次扫描,我们就会得到游标的下一个值,在随后的迭代中使用。最终,当下一个游标值为 “0 “时,我们知道我们已经扫描了整个集合。
7. Scanning With Java
7.用Java进行扫描
By now, we have enough understanding of our approach that we can start implementing it in Java.
现在,我们已经对我们的方法有了足够的了解,我们可以开始在Java中实现它。
7.1. Scanning Strategies
7.1.扫描策略
If we peek into the core scanning functionality offered by the Jedis class, we’ll find strategies to scan different collection types:
如果我们偷看一下Jedis类提供的核心扫描功能,我们会发现扫描不同集合类型的策略。
public ScanResult<String> scan(final String cursor, final ScanParams params);
public ScanResult<String> sscan(final String key, final String cursor, final ScanParams params);
public ScanResult<Map.Entry<String, String>> hscan(final String key, final String cursor,
final ScanParams params);
public ScanResult<Tuple> zscan(final String key, final String cursor, final ScanParams params);
Jedis requires two optional parameters, search-pattern and result-size, to effectively control the scanning – ScanParams makes this happen. For this purpose, it relies on the match() and count() methods, which are loosely based on the builder design pattern:
Jedis需要两个可选的参数,搜索模式和结果大小,以有效地控制扫描 – ScanParams使之发生。为此,它依赖于match()和count()方法,它们松散地基于builder设计模式。
public ScanParams match(final String pattern);
public ScanParams count(final Integer count);
Now that we’ve soaked in the basic knowledge about Jedis’s scanning approach, let’s model these strategies through a ScanStrategy interface:
现在我们已经吸收了关于Jedis的扫描方法的基本知识,让我们通过ScanStrategy接口来模拟这些策略。
public interface ScanStrategy<T> {
ScanResult<T> scan(Jedis jedis, String cursor, ScanParams scanParams);
}
First, let’s work on the simplest scan strategy, which is independent of the collection-type and reads the keys, but not the value of the keys:
首先,让我们研究最简单的scan策略,它与集合类型无关,读取键,但不读取键的值。
public class Scan implements ScanStrategy<String> {
public ScanResult<String> scan(Jedis jedis, String cursor, ScanParams scanParams) {
return jedis.scan(cursor, scanParams);
}
}
Next, let’s pick up the hscan strategy, which is tailored to read all the field keys and field values of a particular hash key:
接下来,让我们拿起hscan策略,它是为读取特定哈希键的所有字段键和字段值而定制的。
public class Hscan implements ScanStrategy<Map.Entry<String, String>> {
private String key;
@Override
public ScanResult<Entry<String, String>> scan(Jedis jedis, String cursor, ScanParams scanParams) {
return jedis.hscan(key, cursor, scanParams);
}
}
Finally, let’s build the strategies for sets and sorted sets. The sscan strategy can read all the members of a set, whereas the zscan strategy can read the members along with their scores in the form of Tuples:
最后,让我们为集合和排序后的集合建立策略。sscan策略可以读取一个集合的所有成员,而zscan策略可以以Tuples的形式读取成员及其分数。
public class Sscan implements ScanStrategy<String> {
private String key;
public ScanResult<String> scan(Jedis jedis, String cursor, ScanParams scanParams) {
return jedis.sscan(key, cursor, scanParams);
}
}
public class Zscan implements ScanStrategy<Tuple> {
private String key;
@Override
public ScanResult<Tuple> scan(Jedis jedis, String cursor, ScanParams scanParams) {
return jedis.zscan(key, cursor, scanParams);
}
}
7.2. Redis Iterator
7.2. Redis迭代器
Next, let’s sketch out the building blocks needed to build our RedisIterator class:
接下来,让我们勾勒出构建我们的RedisIterator类所需的构建块。
- String-based cursor
- Scanning strategy such as scan, sscan, hscan, zscan
- Placeholder for scanning parameters
- Access to JedisPool to get a Jedis resource
We can now go ahead and define these members in our RedisIterator class:
现在我们可以继续前进,在我们的RedisIterator类中定义这些成员。
private final JedisPool jedisPool;
private ScanParams scanParams;
private String cursor;
private ScanStrategy<T> strategy;
Our stage is all set to define the iterator-specific functionality for our iterator. For that, our RedisIterator class must implement the Iterator interface:
我们的阶段是为我们的迭代器定义特定的迭代器功能。为此,我们的RedisIterator类必须实现Iterator接口。
public class RedisIterator<T> implements Iterator<List<T>> {
}
Naturally, we are required to override the hasNext() and next() methods inherited from the Iterator interface.
当然,我们需要覆盖从Iterator接口继承的hasNext()和next()方法。
First, let’s pick the low-hanging fruit – the hasNext() method – as the underlying logic is straight-forward. As soon as the cursor value becomes “0”, we know that we’re done with the scan. So, let’s see how we can implement this in just one-line:
首先,让我们摘取低垂的果实–hasNext()方法–因为其底层逻辑是直截了当的。一旦光标值变成 “0”,我们就知道我们已经完成了扫描。那么,让我们看看如何用一行就能实现这个目标。
@Override
public boolean hasNext() {
return !"0".equals(cursor);
}
Next, let’s work on the next() method that does the heavy lifting of scanning:
接下来,让我们研究一下next()方法,该方法完成了扫描的重任。
@Override
public List next() {
if (cursor == null) {
cursor = "0";
}
try (Jedis jedis = jedisPool.getResource()) {
ScanResult scanResult = strategy.scan(jedis, cursor, scanParams);
cursor = scanResult.getCursor();
return scanResult.getResult();
} catch (Exception ex) {
log.error("Exception caught in next()", ex);
}
return new LinkedList();
}
We must note that ScanResult not only gives the scanned results but also the next cursor-value needed for the subsequent scan.
我们必须注意,ScanResult不仅给出了扫描的结果,还给出了后续扫描所需的下一个游标值。
Finally, we can enable the functionality to create our RedisIterator in the RedisClient class:
最后,我们可以启用功能,在RedisClient类中创建我们的RedisIterator。
public RedisIterator iterator(int initialScanCount, String pattern, ScanStrategy strategy) {
return new RedisIterator(jedisPool, initialScanCount, pattern, strategy);
}
7.3. Read With Redis Iterator
7.3.用Redis迭代器读取
As we’ve designed our Redis iterator with the help of the Iterator interface, it’s quite intuitive to read the collection values with the help of the next() method as long as hasNext() returns true.
由于我们是在Iterator接口的帮助下设计的Redis迭代器,只要hasNext()返回true,就可以很直观地借助next()方法读取集合的值。
For the sake of completeness and simplicity, we’ll first store the dataset related to the sports-balls in a Redis hash. After that, we’ll use our RedisClient to create an iterator using Hscan scanning strategy. Let’s test our implementation by seeing this in action:
为了完整和简单起见,我们首先将与运动球相关的数据集存储在Redis哈希中。之后,我们将使用我们的RedisClient来创建一个使用Hscan扫描策略的迭代器。让我们通过实际操作来测试我们的实现。
@Test
public void testHscanStrategy() {
HashMap<String, String> hash = new HashMap<String, String>();
hash.put("cricket", "160");
hash.put("football", "450");
hash.put("volleyball", "270");
redisClient.hmset("balls", hash);
Hscan scanStrategy = new Hscan("balls");
int iterationCount = 2;
RedisIterator iterator = redisClient.iterator(iterationCount, "*", scanStrategy);
List<Map.Entry<String, String>> results = new LinkedList<Map.Entry<String, String>>();
while (iterator.hasNext()) {
results.addAll(iterator.next());
}
Assert.assertEquals(hash.size(), results.size());
}
We can follow the same thought process with little modification to test and implement the remaining strategies to scan and read the keys available in different types of collections.
我们可以按照同样的思维过程,稍作修改,测试和实现其余的策略,以扫描和读取不同类型的集合中的密钥。
8. Conclusion
8.结语
We started this tutorial with an intention to learn about how we can read all the matching keys in Redis.
我们开始这个教程的目的是为了学习如何在Redis中读取所有匹配的键。
We found out that there is a simple way offered by Redis to read keys in one go. Although simple, we discussed how this puts a strain on the resources and is therefore not suitable for production systems. On digging deeper, we came to know that there’s an iterator-based approach for scanning through matching Redis keys for our read-query.
我们发现Redis提供了一个简单的方法来一次性读取密钥。虽然简单,但我们讨论了这是如何对资源造成压力的,因此不适合用于生产系统。深入研究后,我们知道有一种基于iterator的方法来扫描匹配的Redis键,以便我们进行读取查询。
As always, the complete source code for the Java implementation used in this article is available over on GitHub.
一如既往,本文中所使用的Java实现的完整源代码可在GitHub上获取。