Liste de toutes les clés Redis disponibles

Haut Java

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Vue d'ensemble

Les collections sont un élément essentiel que l'on retrouve généralement dans presque toutes les applications modernes. Il n'est donc pas surprenant que Redis propose une variété de structures de données populaires telles que des listes, des ensembles, des hachages et des ensembles triés que nous pouvons utiliser.

Dans ce didacticiel, nous allons apprendre comment lire efficacement toutes les clés Redis disponibles qui correspondent à un modèle particulier.

2. Explorez les collections

Imaginons que notre application utilise Redis pour stocker des informations sur les balles utilisées dans différents sports. Nous devrions pouvoir voir des informations sur chaque balle disponible dans la collection Redis. Pour plus de simplicité, nous limiterons notre ensemble de données à seulement trois boules:

  • Balle de cricket d'un poids de 160 g
  • Football avec un poids de 450 g
  • Volleyball d'un poids de 270 g

Comme d'habitude, clarifions d'abord nos bases en travaillant sur une approche naïve pour explorer les collections Redis.

3. Approche naïve utilisant redis-cli

Avant de commencer à écrire du code Java pour explorer les collections, nous devrions avoir une bonne idée de la façon dont nous le ferons en utilisant l' interface redis-cli . Supposons que notre instance Redis soit disponible à 127.0.0.1 sur le port 6379 , pour que nous puissions explorer chaque type de collection avec l'interface de ligne de commande.

3.1. Liste liée

Tout d'abord, stockons notre ensemble de données dans une liste chaînée Redis nommée balles au format sport-name _ ball-weight à l'aide de la commande rpush :

% 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

Nous pouvons remarquer qu'une insertion réussie dans la liste génère la nouvelle longueur de la liste . Cependant, dans la plupart des cas, nous serons aveugles à l'activité d'insertion de données. En conséquence, nous pouvons connaître la longueur de la liste chaînée à l'aide de la commande llen :

127.0.0.1:6379> llen balls (integer) 3

Lorsque nous connaissons déjà la longueur de la liste, il est pratique d' utiliser la commande lrange pour récupérer facilement l'ensemble de données complet:

127.0.0.1:6379> lrange balls 0 2 1) "cricket_160" 2) "football_450" 3) "volleyball_270"

3.2. Ensemble

Ensuite, voyons comment nous pouvons explorer l'ensemble de données lorsque nous décidons de le stocker dans un ensemble Redis. Pour ce faire, nous devons d'abord remplir notre ensemble de données dans un ensemble Redis nommé boules à l'aide de la commande sadd :

127.0.0.1:6379> sadd balls "cricket_160" "football_450" "volleyball_270" "cricket_160" (integer) 3

Oops! Nous avions une valeur en double dans notre commande. Mais, puisque nous ajoutions des valeurs à un ensemble, nous n'avons pas à nous soucier des doublons. Bien sûr, nous pouvons voir le nombre d'éléments ajoutés à partir de la valeur de réponse en sortie.

Maintenant, nous pouvons tirer parti de la smembers commande pour voir tous les membres de l' ensemble :

127.0.0.1:6379> smembers balls 1) "volleyball_270" 2) "cricket_160" 3) "football_450"

3.3. Hacher

Maintenant, utilisons la structure de données de hachage de Redis pour stocker notre ensemble de données dans une clé de hachage nommée boules de telle sorte que le champ de hachage est le nom du sport et la valeur du champ est le poids de la balle. Nous pouvons le faire à l'aide de la commande hmset :

127.0.0.1:6379> hmset balls cricket 160 football 450 volleyball 270 OK

Pour voir les informations stockées dans notre hachage, nous pouvons utiliser la commande hgetall :

127.0.0.1:6379> hgetall balls 1) "cricket" 2) "160" 3) "football" 4) "450" 5) "volleyball" 6) "270"

3.4. Ensemble trié

En plus d'une valeur de membre unique, les ensembles triés nous permettent de conserver un score à côté d'eux. Eh bien, dans notre cas d'utilisation, nous pouvons garder le nom du sport comme valeur de membre et le poids de la balle comme score. Utilisons la commande zadd pour stocker notre ensemble de données:

127.0.0.1:6379> zadd balls 160 cricket 450 football 270 volleyball (integer) 3

Maintenant, nous pouvons d'abord utiliser la commande zcard pour trouver la longueur de l'ensemble trié, suivie de la commande zrange pour explorer l'ensemble complet :

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. Cordes

Nous pouvons également voir les chaînes de valeurs-clés habituelles comme une collection superficielle d'éléments . Commençons par remplir notre ensemble de données à l'aide de la commande mset :

127.0.0.1:6379> mset balls:cricket 160 balls:football 450 balls:volleyball 270 OK

Nous devons noter que nous avons ajouté le préfixe «boules: » afin que nous puissions identifier ces clés à partir du reste des clés qui peuvent se trouver dans notre base de données Redis. De plus, cette stratégie de dénomination nous permet d'utiliser la commande keys pour explorer notre jeu de données à l'aide de la correspondance de modèle de préfixe:

127.0.0.1:6379> keys balls* 1) "balls:cricket" 2) "balls:volleyball" 3) "balls:football"

4. Implémentation Naive Java

Maintenant que nous avons développé une idée de base des commandes Redis pertinentes que nous pouvons utiliser pour explorer des collections de différents types, il est temps pour nous de mettre la main à la pâte avec du code.

4.1. Dépendance de Maven

Dans cette section, nous utiliserons la bibliothèque cliente Jedis pour Redis dans notre implémentation:

 redis.clients jedis 3.2.0 

4.2. Client 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.

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.

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.

First, let's create a private constructor for our client that'll internally initialize the JedisPool when an instance of RedisClient class is created:

private static JedisPool jedisPool; private RedisClient(String ip, int port) { try { if (jedisPool == null) { jedisPool = new JedisPool(new URI("//" + 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:

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:

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.

4.3. Analysis

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.

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.

So, we should make it a point that we're limiting our naive approach to be used only for debugging purposes.

5. Iterator Basics

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.

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.

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.

6. Redis Scan

Redis offers several scanning strategies to read keys from collections using a cursor-based approach, which is, in principle, similar to a page bookmark.

6.1. Scan Strategies

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.

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*”:

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”.

7. Scanning With Java

By now, we have enough understanding of our approach that we can start implementing it in Java.

7.1. Scanning Strategies

If we peek into the core scanning functionality offered by the Jedis class, we'll find strategies to scan different collection types:

public ScanResult scan(final String cursor, final ScanParams params); public ScanResult sscan(final String key, final String cursor, final ScanParams params); public ScanResult
     
       hscan(final String key, final String cursor, final ScanParams params); public ScanResult 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:

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:

public interface ScanStrategy { ScanResult 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:

public class Scan implements ScanStrategy { public ScanResult 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:

public class Hscan implements ScanStrategy
     
       { private String key; @Override public ScanResult
      
        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:

public class Sscan implements ScanStrategy { private String key; public ScanResult scan(Jedis jedis, String cursor, ScanParams scanParams) { return jedis.sscan(key, cursor, scanParams); } } public class Zscan implements ScanStrategy { private String key; @Override public ScanResult scan(Jedis jedis, String cursor, ScanParams scanParams) { return jedis.zscan(key, cursor, scanParams); } }

7.2. Redis Iterator

Next, let's sketch out the building blocks needed to build our RedisIterator class:

  • 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:

private final JedisPool jedisPool; private ScanParams scanParams; private String cursor; private ScanStrategy 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:

public class RedisIterator implements Iterator
     
       { }
     

Naturally, we are required to override the hasNext() and next() methods inherited from the Iterator interface.

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:

@Override public boolean hasNext() { return !"0".equals(cursor); }

Next, let's work on the next() method that does the heavy lifting of scanning:

@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.

Finally, we can enable the functionality to create our RedisIterator in the RedisClient class:

public RedisIterator iterator(int initialScanCount, String pattern, ScanStrategy strategy) { return new RedisIterator(jedisPool, initialScanCount, pattern, strategy); }

7.3. Read With Redis Iterator

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.

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:

@Test public void testHscanStrategy() { HashMap hash = new HashMap(); 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
     
       results = new LinkedList
      
       (); 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

Nous avons commencé ce didacticiel avec l'intention d'apprendre comment nous pouvons lire toutes les clés correspondantes dans Redis.

Nous avons découvert qu'il existe un moyen simple offert par Redis pour lire les clés en une seule fois. Bien que simple, nous avons discuté de la façon dont cela met une pression sur les ressources et n'est donc pas adapté aux systèmes de production. En creusant plus profondément, nous avons appris qu'il existe une approche basée sur des itérateurs pour analyser les clés Redis correspondantes pour notre requête de lecture.

Comme toujours, le code source complet de l'implémentation Java utilisée dans cet article est disponible à l'adresse over sur GitHub.

Fond Java

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS