Redis Data Structures

Redis Data Structures

Learn the most common data structures that Redis provides out of the box

Β·

16 min read

Redis is an in-memory database which has a very powerful set of data types and data structures. These data structures are used across a variety of applications like message queues, game leaderboards, etc. Redis has been widely popular because of its performance efficiency and simplicity of using it.

In this article, I have explained the most commonly used data types in Redis with examples.

I would also recommend installing Redis on your local machine and following this article by actually executing the commands hands-on. Once you install Redis(instructions here), run redis-cli command from your CLI, this will open the command line interface of redis where you can run the commands which are mentioned below in this article.

Before starting with Redis data types, let's understand what are the keys in Redis

Keys

Keys are the primary way to access data in Redis. Almost all the Redis data structures and Redis commands operate using keys.

Usually, it is suggested to have a meaningful domain-specific key name, with a separator if required; for example: product:12 may be used for storing details about a product which has a primary id 12.

Keys are used in almost all data types of Redis like string, list, hashes, sets, etc.

We will now start with a String data type to also get acquainted with keys.

Strings

We will be exploring the below commands which are applied for string data type -

SET <key> <value>  
GET <key>
KEYS <regex pattern>
SCAN <cursor>
DEL <key> 
UNLINK <key>
EXISTS <key>
  • SET <key> <value> - sets the value of the given key

    • You can also provide the expiry of this key using EX or EXAT argument.

    • By default, if the key is not present, it will create the key, else it will update the value of the key. If you want to SET the key only if the key does not already exist, then you can pass the NX argument.

  • GET <key> - returns the value of the given key name. ex: GET product:12

  • MGET <key> [<key>...] - returns the value of multiple keys passed in the argument

    Below is the demonstration in redis-cli :

      # Sets the value as product-12-value with expity of 2 mins
      127.0.0.1:6379> SET product:12 product-12-value EX 120 NX
      # OUTPUT
      OK
    
      127.0.0.1:6379> SET product:13 p13
      # OUTPUT
      OK
    
      127.0.0.1:6379> get product:12
      # OUTPUT
      "product-12-value"
    
      127.0.0.1:6379> GET product:12
      # OUTPUT
      "product-12-value"
    
      127.0.0.1:6379> MGET product:12 product:13
      # OUTPUT
      1) "product-12-value"
      2) "p13"
    

Redis also provides commands to retrieve all the keys in the database matching a regex pattern

  • KEYS <regex pattern> - returns all the keys matching the specified pattern.

    • This command is not recommended in production applications since it's a blocking command, and it may cause performance issues in the case of large datasets.

        127.0.0.1:6379> KEYS product:*
        # OUTPUT
        1) "product:12"
        2) "product:13"
      
  • SCAN <cursor> [MATCH pattern] [COUNT count]

    • This command also blocks the thread but it returns only a few keys at a time, along with a cursor. You need to call SCAN 0(0 is the cursor here) for the first time and then iteratively call the SCAN command with the updated cursor which was returned in the response.

    • When the server response has the cursor value as 0, it signifies no other keys left to iterate, so you can stop the iteration at this point.

    • The user can also specify an optional MATCH argument to retrieve based on a specified regex pattern, and a COUNT argument signifying what number of keys to be retrieved per SCAN, though this can block the thread longer if the COUNT value is too large.

To delete the key, Redis provides 2 commands

  • DEL <key> - deletes the key in a blocking manner

  • UNLINK <key> - deletes the key in a non-blocking way, where the memory associated with the key is reclaimed by an asynchronous process.

You can also check if the key exists or not using EXISTS <key> command.

Apart from keys and scan command, all other commands have a time complexity of O(1). Every scan command is O(1), but it can become O(N) for complete iteration.


Strings are fundamental data types in Redis and can be used to store numerical values, serialized JSON or anything that case which can be represented in binary. All the commands we saw above are used for strings.

🚨 Even if you run SET int-key 12, and then try to GET the value, it will return value 12 as a string.

127.0.0.1:6379> SET int-key 12
OK

127.0.0.1:6379> GET int-key
"12"

You can check the type of key using type <key> command. For the above key, it will return a string.

This is because it returns the string representation of the value. The different types that can be returned are: string, list, set, zset, hash and stream (We will look into these types below)

Internally, Redis stores the encoding of the values, and stores the knowledge whether it's a string, integer, floating number, etc. To check the kind of internal representation used to store the value, you need to use object encoding <key>. This command will return "int" as a value as shown below.

127.0.0.1:6379> TYPE int-key
string

127.0.0.1:6379> OBJECT ENCODING int-key
"int

For numeric values, we can also increment/decrement the value directly using the below Redis commands:

  • INCR/DECR <key> - increments/decrements the key value by 1

  • INCRBY/DECRBY <key> <increment value> - increments/decrements by the specified increment value

      127.0.0.1:6379> INCR int-key
      (integer) 13
    
      127.0.0.1:6379> INCR int-key
      (integer) 14
    
      127.0.0.1:6379> GET int-key
      "14"
    

Use cases of Strings:

  • Caching the API response in JSON string format. for example: SET product:12 "{\"name\":\"P1\",\"price\":\"121\"}"

  • Storing session cache.

Hashes

Hashes are a collection of field-value pairs, similar to hashmaps or maps in other languages like Java, Go, etc. They are mutable as well, so we can update or delete the hash and even add/update the field-value pairs.

Hashes store field values as string type, which means we can store flat string and numeric values but not embedded JSON or objects.

Similar to Key and string, we have below commands used for hashes -

  • HSET <key> <field> <value> [<field> <value> ...] - sets/creates or updates the hash with given field-values stored at key

    • HSET hash-example:product:12 name p1 price 22 will create the hash with key hash-example:product:12 for the first time with given fields name and price with their values p1 and 22 respectively.

    • HSET hash-example:product:12 name p12 will update the above hash's "name" field's value to p12

  • HGET <key> <field> - returns provided field's value from the given hash name

    • Example: HGET hash-example:product:12 name will return p1
  • HMGET <key> <field> [<field>...] - allows retrieving multiple fields values

      127.0.0.1:6379> HSET hash-example:product:12 name p1 price 22
      (integer) 0
    
      127.0.0.1:6379> HMGET hash-example:product:12 name price
      1) "p1"
      2) "22"
    
      127.0.0.1:6379> HSET hash-example:product:12 name p2
      (integer) 0
    
      127.0.0.1:6379> HMGET hash-example:product:12 name price
      1) "p2"
      2) "22"
    
  • HGETALL <key> - will retrieve all the fields with their values for provided key

      127.0.0.1:6379> HGETALL hash-example:product:12
      1) "price"
      2) "22"
      3) "name"
      4) "p2"
    
  • HKEYS <key> - will only return the field names for a given hash

  • HLEN <key> - returns the total length(number of fields) of hash

  • HDEL <key>- deletes the hash stored at the given key

  • HINCRBY <key> <field> <incr-value> - will increment the value of the field by incr-value provided in the argument.

      127.0.0.1:6379> HINCRBY hash-example:product:12 price 2
      (integer) 24
    
      127.0.0.1:6379> HMGET hash-example:product:12 name price
      1) "p2"
      2) "24"
    
  • HSCAN <key> <cursor> [MATCH pattern] [COUNT count] - Similar to SCAN command we saw above in the key section, but HSCAN is for scanning hashes.

      127.0.0.1:6379> hscan hash-example:product:12 0
      1) "0"
      2) 1) "price"
         2) "24"
         3) "name"
         4) "p2"
    

Apart from HSCAN, HKEYS and HGETALL all other commands have a time complexity of O(1).

Hashes use cases:

  • Caching in case the object is too large and we want to only retrieve certain fields instead of all.

  • Storing User profiles, product information, etc.

List

The list is the sequence of strings, similar to Array or ArrayList in Java, Go, etc. Lists are used for storing arrays of strings, but they can also be used as a stack and queue.

Think of it as a list having a head as left(1st element)and a tail as right(last element). The beauty of this data type is that Redis provide commands to insert and remove the element from both the left and right of the list out of the box, making it ideal for stack and queue use-case implementations.

The list has below list of commands-

  • LPUSH <key> <element> [<element>...] - is used to insert an element at the head of the list (left push) stored at the key. Multiple elements can also be passed.

    • LPUSH demo-list 1 2 3 will result in a list with value [3,2,1]
  • LPOP <key> <count> - is used to remove the data from the head of the list (left pop). Multiple elements can also be removed at once by providing an optional count value

    • LPOP demo-list 1 will remove the 3 from the above list; updated list - [2,1]
  • RPUSH <key> <element> [<element>...] - same as LPUSH, but is used to insert the element or multiple elements at the tail of the list (right push)

    • RPUSH demo-list 4 5 will add to the above list from the right; updated list - [2,1,4,5]
  • RPOP <key> <count> - same as LPOP, but is used to remove the data from the tail of the list (right pop)

    • RPOP demo-list 1 will remove the 5 from the above list; updated list - [2,1,4]
  • LRANGE <key> <start> <stop> - returns the elements of the list stored at the key. Start and stop are zero-based indexes, where the start 0 is the 1st element(head) and so on.

    • LRANGE demo-list 0 -1 will retrieve all elements of the list

    • LRANGE demo-list 0 -2 will retrieve all elements from the 1st element till 2nd last element of the list

127.0.0.1:6379> LPUSH demo-list 1 2 3
(integer) 3

127.0.0.1:6379> LRANGE demo-list 0 -1
1) "3"
2) "2"
3) "1"

127.0.0.1:6379> LPOP demo-list 1
1) "3"

127.0.0.1:6379> LRANGE demo-list 0 -1
1) "2"
2) "1"

127.0.0.1:6379> RPUSH demo-list 4 5
(integer) 4

127.0.0.1:6379> LRANGE demo-list 0 -1
1) "2"
2) "1"
3) "4"
4) "5"

127.0.0.1:6379> RPOP demo-list 1
1) "5"

127.0.0.1:6379> LRANGE demo-list 0 -1
1) "2"
2) "1"
3) "4"
  • LLEN <key> - returns the length of the list

  • LTRIM <key> <start> <stop> - trims the element and keeps only elements from the specified start index to the stop index in the list.

πŸ’« You can use a combination of the above commands for stack and queue implementations

  • For queues, use RPUSH and LPOP

  • For stack, use LPUSH and LPOP

List use cases:

  • Stack or queue implementations like a queue-based worker.

  • Social networking sites display only the top 5 trending results, etc.

Set

Set are similar to lists, i.e. an unordered sequence of strings, but the difference is the set does not have duplicate elements, even if you try to insert the same element thousand times. Another difference is that the set does not allow insertions or deletions from the head or tail of the array, since it's not an array, it's just a set!

Redis Set also supports standard mathematical set operations like Union, Intersection and Difference πŸ’«.

The set has below list of commands-

  • SADD <key> <member> [<member>...] - Adds the member or members passed in the argument to the set stored at key

    • SADD demo-set a b - adds a and b members to the set
  • SCARD <key> - returns the cardinality(total number of members) of the set. i.e. 2

  • SISMEMBER <key> <member> - tells whether the given member is part of the set stored at key or not; return 0 if it is not part of the set

    • SISMEMBER demo-set bxya - returns 0 as bxya member is not present in set
  • SINTER <key> [<key>...] - returns the intersection, i.e. common members between all the sets stored at the provided keys. The time complexity of this command will be O(N*M) where N is the cardinality of the smallest set and M is the total number of sets to compare with.

      127.0.0.1:6379> SADD demo-set a b
      (integer) 0
    
      127.0.0.1:6379>  SCARD demo-set
      (integer) 2
    
      127.0.0.1:6379> SISMEMBER demo-set x
      (integer) 0
    
      127.0.0.1:6379> SISMEMBER demo-set a
      (integer) 1
    
      127.0.0.1:6379> SADD demo-set1 a c d
      (integer) 0
    
      127.0.0.1:6379> SINTER demo-set demo-set1
      1) "a"
    
  • SUNION <key> [<key>...] - - returns the union, i.e. all members in all the given sets stored at the provided keys

    • SUNION demo-set demo-set1 will return b, c, d, a as the output
  • SDIFF <key> [<key>...] - returns the members of the set resulting from the difference between the first set and all the successive sets.

      127.0.0.1:6379> SUNION demo-set demo-set1
      1) "b"
      2) "c"
      3) "d"
      4) "a"
    
      127.0.0.1:6379> SDIFF demo-set demo-set1
      1) "b"
    
  • SMEMBERS key - returns all the members of the set stored at key

      127.0.0.1:6379> SMEMBERS demo-set
      1) "b"
      2) "a"
    
  • SSCAN <cursor> [MATCH pattern] [COUNT count] - Similar to SCAN and HSCAN we saw in the above sections, but SSCAN is used for scanning through sets, and is preferred over SMEMBERS for performance reasons.

  • SREM <key> <member> - removes the specified member from the set

      127.0.0.1:6379> sscan demo-set 0
      1) "0"
      2) 1) "b"
         2) "a"
    
      127.0.0.1:6379> srem demo-set a
      (integer) 1
    
      127.0.0.1:6379> sscan demo-set 0
      1) "0"
      2) 1) "b"
    

Set use cases:

  • Track unique website visitors based on IP, since sets have deduplication.

  • The union, intersection and difference make it ideal for use cases where we want to find the commonality or combination of sets between multiple domain data which might have the same members, like for example: to find out all the customers who purchased X as well as Y book. We can have two sets purchase-order:book:X:customers and purchase-order:book:Y:customers whose values are set of customer ids, and we can simply do SINTER to find out users who bought both books.

Sorted Set

Sorted sets are similar to the sets we discussed above, with an additional property - a score is associated with each member in the set. This is a floating point number by which we can specify a sorting order of the members in a sorted set(like sort ascending by score or sort descending by score)

There can be two elements with the same score, in such cases, precedence will be given to a higher member key with a lexical value. example: a sorted set with key ab will be given higher precedence over sorted set with key name bc if both have the same score.

Sorted sets also support union and intersection commands similar to sets.

The sorted set has a similar list of commands as the set, with an additional score value.

Use case: Let's take a use case of players playing in a tennis tournament and earning some scores. We will add 4 players (player1, player2, player3 and player4) along with their total scores in the tournament

  • ZADD <key> <score> <member> [<score> <member>...] - adds one more member to the sorted set along with its associated score.

      127.0.0.1:6379> zadd game:tennis NX 2 player1
      (integer) 1
    
      127.0.0.1:6379> zadd game:tennis NX 4 player2
      (integer) 1
    
      127.0.0.1:6379> zadd game:tennis NX 1 player3
      (integer) 1
    
      127.0.0.1:6379> zadd game:tennis NX 7 player4
      (integer) 1
    
  • ZINCRBY <key> <increment> <member> - increments the score of the member by the values provided in the argument

      127.0.0.1:6379> zincrby game:tennis 2 player3
      "3"
    
  • ZRANGE <key> <min> <max> [LIMIT offset count] [WITHSCORES]- retrieves the set members from specified min and max index. You can also pass an optional limit argument to limit the number of returned set members.
    WITHSCORES argument will also return the scores of the returned members

      127.0.0.1:6379> zrange game:tennis 0 -1 withscores
      1) "player3"
      2) "1"
      3) "player1"
      4) "2"
      5) "player2"
      6) "4"
      7) "player4"
      8) "7"
    
  • ZRANGEBYSCORE <key> <min> <max> [WITHSCORES] [LIMIT offset count] - returns the members of the set in the order sorted ascending based on score. Do note here that the min and max are not zero-based indexes, instead, it starts with 1.

    • This can be used to show the leaderboards of players, for example: to show the bottom 3 players(which have the lowest score) ZRANGEBYSCORE game:tennis 1 4

        127.0.0.1:6379> ZRANGEBYSCORE game:tennis 1 4 WITHSCORES
        1) "player3"
        2) "1"
        3) "player1"
        4) "2"
        5) "player2"
        6) "4"
      
  • ZSCORE <key> <member> - returns the score of the specified member

  • ZREVRANGEBYSCORE <key> <min> <max> [WITHSCORES] [LIMIT offset count] - similar to ZRANGEBYSCORE mentioned above but this command will return based on descending order of the score

    • This can be used to show the tournament leaderboard, for example: to show the top 3 players(which has the highest score)

        127.0.0.1:6379> ZREVRANGEBYSCORE game:tennis +inf -inf WITHSCORES LIMIT 0 3
        1) "player4"
        2) "7"
        3) "player2"
        4) "4"
        5) "player1"
        6) "2"
      
  • ZRANK <key> <member> - returns the rank of the user based on ascending order of scores. Note that rank is zero-based, which means that the member with the lowest score has rank 0.

  • ZREVRANK <key> <member> - returns the rank of the user based on descending order of scores. The member with the highest score will have the lowest rank(i.e. rank 0)

    • This can be used to display the rank of individual players
  • ZREMRANGEBYRANK <key> <start> <stop> - removes the members from the set with the rank range specified in the argument. For example: to retain only the top 3 players in the leaderboard and remove others, we can run -

      # Since the ZREMRANGEBYRANK will evaluate based on scores sorted in ascending order, we will want to retain only bottom 3 elements(which are actually top 3 players by rank, so we will remove from 1st element(index 0) till fourth last element(index -4), hence retaining the top 3 players)
      127.0.0.1:6379> ZREMRANGEBYRANK game:tennis 0 -4
      (integer) 1 
    
      127.0.0.1:6379> ZREVRANGEBYSCORE game:tennis +inf -inf WITHSCORES LIMIT 0 10
      1) "player4"
      2) "7"
      3) "player2"
      4) "4"
      5) "player1"
      6) "2"
    
  • ZUNION <numkeys> <key> [<key>..] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]

    • This performs a union on total N(numkeys) keys specified in the argument and returns the result. WITHSCORES is an optional argument by which scores will also be passed in the result.

    • Note: By default, if a key is present in two sets, the ZUNION will add the scores in both sets and return the new score. If we don't want to add up the score, we can also pass AGGREGATE function arguments like MIN or MAX

  • ZINTER <numkeys> <key> [<key>..] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]

  • ZSCAN <cursor> [MATCH pattern] [COUNT count] - Similar to HSCAN and SSCAN mentioned above, but ZSCAN is used to scan over sorted sets.

Sorted sets use cases:

Since sorted sets have a score associated with each member and an extensive set of commands to play on this score, it makes it ideal for use cases like -

  • Priority queues / task scheduling service

  • Game Leaderboards

  • Geo hashing


There are other Redis data structures as well like Bitmaps, Geospatial, and HyperLogLog which I will try to cover in some other article. The ones mentioned in this article are the most basic and commonly used Redis data types and data structures.

Few more facts about Redis πŸ’‘

Redis being an in-memory database leverages several efficient low-level data structures without worrying about how to persist them effectively.

Redis is single threaded hence it does not have to deal with locks and other synchronization mechanisms, making it simpler to use and think of for your system design.

Redis is known for its fast performance, although it is single-threaded. It is because Redis supports I/O multiplexing where the operating system allows a single thread to wait on multiple socket connections. Because of this, Redis still accepts a large number of requests concurrently and evaluates them one by one. This evaluation/execution phase is extremely fast because it is an in-memory data store. Applications using Redis are usually I/O heavy and I/O takes time whereas in-memory operations are extremely fast, so with I/O multiplexing, redis can support a large number of TCP connections while being extremely fast at executing the memory operations.

Redis also supports transactions and an optimistic locking mechanism. I will be making a separate article for the same. πŸ˜‰

What to do after reading this article

Well, since now you know the basics of Redis data structures, you can build a small project using Java, Go, Ruby or any other language and use Redis as a data store.

Few projects ideas where you can use the above data structures -

  1. Sports event

  2. Product Catalog

  3. Library management

Do comment down if you have more project ideas to work on.

If you found this article helpful, please comment and share this article πŸ˜ƒ.

Let's connect πŸ’«. You can follow me on

You can also subscribe to my newsletter below to get an email notification on my latest posts. πŸ’»

Β