DEV Community

Cover image for Coding redis cluster the hard way
Rangeesh
Rangeesh

Posted on

Coding redis cluster the hard way


Hi readers, after reading this blog I hope you will be having a feeling like…

You can also choose to read the basics or dive right away into coding if you are here just for code reference. way easy than choosing a red pill and a blue pill right…

So let's address the elephant in the room, what is Redis?

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperlogs, geospatial indexes with radius queries, and streams. — Redis Website

Wow! That is too technical, right? It is just a type of DB where data is stored in memory (RAM) instead of the disk with additional out-of-the-box features like hashes, object mapping and whatnot!!.

Now as you already have an idea of what is Redis let's dive into the Redis cluster. Wait what !!!, yes you read that right, as you know what is Redis just add more Redis and more Redis and more of it and make all them talk from different machines from different countries, That is what Redis cluster is. Check yourselves with below image.

Before diving into technical coding please make sure to refresh your idea on below terms or use the below links to brush up on what that term means and continue further reading.

  1. Basic of networks like OSI layer - https://www.imperva.com/learn/application-security/osi-model/ 1.Consistent hashing -https://en.wikipedia.org/wiki/Consistent_hashing
  2. Basic Redis commands - https://www.freecodecamp.org/news/how-to-learn-redis/
  3. Basic of FastApi - https://fastapi.tiangolo.com/

Now Lets dive into some technical as this is a tech blog..

  • Every Redis Cluster node requires two TCP connections. The normal Redis TCP port used to serve clients, i.e. 6379, plus the port obtained by adding 10000 to the data port, so 16379. Make sure you open both ports in your firewall, otherwise Redis cluster nodes will be not able to communicate.

  • Redis Cluster does not use consistent hashing, but a different form of sharding where every key is conceptually part of what we call a hash slot. There are 16384 hash slots in Redis Cluster.

  • Every node in a Redis Cluster is responsible for a subset of the hash slots, so for example you may have a cluster with 3 nodes, where:

    • Node A contains hash slots from 0 to 5500.
    • Node B contains hash slots from 5501 to 11000.
    • Node C contains hash slots from 11001 to 16383.

Now we know what it is how can we get our hands dirty.

There are 3 ways to set up a cluster for testing and experimenting we are not going to explain everything still add useful links which you can refer to and get started.

WAY 1

Extremely Easy: just a few clicks (Go Enterprise)

WAY 2

Tedious way: Binary installs and running:

Well not that tedious, and later you can build a bash script to start the spinning-up process.

Steps:

  • Creating folders:

    • As We'll be building a master-slave cluster aka. HA Cluster (i.e. there will be a pair of Redis instances on a particular node for a cluster)
    • Let's start by creating multiple folders viz. The number of nodes * 2: 3 * 2 = 6 folders (planning to have 3 masters and 3 slaves)
    • You can name these folders as simple as the port numbers they'll be listening to (just a suggestion😉) or any other significant nomenclature your heart desires.
    • In each of these folders you'll need to download, unzip and make Redis. Follow the below commands for every folder.
    $ wget http://download.redis.io/releases/redis-7.0.4.tar.gz
    $ tar xzf redis-7.0.4.tar.gz
    $ cd redis-7.0.4
    $ make
    

    Pro tip: You can also simply download the package in a folder and copy it to other folders. This will save you some download time and bandwidth.

  • Spinning up Redis in cluster mode.

    • create/edit "redis.conf" file in each folder to have these contents to configure redis for cluster mode.
    port <Port number>
    cluster-enabled yes
    cluster-config-file nodes.conf
    cluster-node-timeout 5000
    appendonly yes
    
    • to enable cluster-mode its as easy as enabling the "cluster-enabled" directive.
    • Now go ahead, start 6 terminals and navigate to all these folders, and start redis with command:
    ./src/redis-server ./redis.conf
    
    • Open a 7th terminal to create the cluster:
    ./redis-cli --cluster create 127.0.0.1:<port for 1 redis> \
    127.0.0.1:<port for 2 redis> 127.0.0.1:<port for 3 redis> \
    127.0.0.1:<port for 4 redis> 127.0.0.1:<port for 5 redis> \
    127.0.0.1:<port for 6 redis>
    --cluster-replicas 1
    
    • "--cluster-replicas" flag set to 1 means that every master has one slave when created.
    • After this, you'll see a lot of logs for "performing check", "Adding replica" and "reassigning each epoch"
    • Connect with the cluster using redis-cli by running

    ./src/redis-cli -c -p <port>

    You can try a SET and GET operations to test the cluster.

  • Adding RediSearch and Redis Insight.

  • The best part of redis modules is that they sit on top of cluster nodes and provide all this awesome support with a blazingly fast response. I beg to differ that Redis could be the only database needed for your BE.

WAY 3

Slightly easier than going all the way down with manual configs.

Play around with Redis Insight with the simple operations and explore the stack section for tutorials there.

Let's Implement a Backend to see Redis in action

I wish to explore the search features of Redis combined with the Hash/Json module for Redis.

We will create a small microservice for extending and interacting with Redis.

env setup for Redis OM

$ export export REDIS_OM_URL=redis://<username>:<password>@<host>:<port>
Enter fullscreen mode Exit fullscreen mode

For fun lets use some pokemon data:

We will upload this data through the redisOM ODM later.

Create a file called pokemon.csv


Name,Type_1,Type_2,Max_CP,Max_HP
Bulbasaur,Grass,Poison,1079,83
Ivysaur,Grass,Poison,1643,107
Venusaur,Grass,Poison,2598,138
Charmander,Fire,,962,73
Charmeleon,Fire,,1568,103
Charizard,Fire,Flying,2620,135
Squirtle,Water,,1015,81
Wartortle,Water,,1594,105
Blastoise,Water,,2560,137
Caterpie,Bug,,446,83
Metapod,Bug,,481,91
Butterfree,Bug,Flying,1465,107
Weedle,Bug,Poison,452,75
Kakuna,Bug,Poison,488,83
Beedrill,Bug,Poison,1450,115
Pidgey,Normal,Flying,684,75
Pidgeotto,Normal,Flying,1232,111
Pidgeot,Normal,Flying,2106,143
Rattata,Normal,,585,59
Raticate,Normal,,1454,99
Spearow,Normal,Flying,691,75
Fearow,Normal,Flying,1758,115
Ekans,Poison,,830,67
Arbok,Poison,,1779,107
Pikachu,Electric,,894,67
Raichu,Electric,,2042,107
Sandshrew,Ground,,804,91
Sandslash,Ground,,1823,130
Nidoran♀,Poison,,882,99
Nidorina,Poison,,1414,122
Nidoqueen,Poison,Ground,2502,154
Nidoran♂,Poison,,849,84
Nidorino,Poison,,1382,108
Nidoking,Poison,Ground,2492,140
Clefairy,Fairy,,1209,122
Clefable,Fairy,,2414,162
Vulpix,Fire,,837,72
Ninetales,Fire,,2203,127
Jigglypuff,Normal,Fairy,924,194
Wigglytuff,Normal,Fairy,2192,233
Zubat,Poison,Flying,647,75
Golbat,Poison,Flying,1935,130
Oddish,Grass,Poison,1156,83
Gloom,Grass,Poison,1701,107
Vileplume,Grass,Poison,2510,130
Paras,Bug,Grass,923,67
Parasect,Bug,Grass,1759,107
Venonat,Bug,Poison,1036,107
Venomoth,Bug,Poison,1903,122
Diglett,Ground,,460,27
Dugtrio,Ground,,1176,67
Meowth,Normal,,761,75
Persian,Normal,,1643,115
Psyduck,Water,,1117,91
Golduck,Water,,2403,138
Mankey,Fighting,,884,75
Primeape,Fighting,,1877,115
Growlithe,Fire,,1344,99
Arcanine,Fire,,3005,154
Poliwag,Water,,801,75
Poliwhirl,Water,,1350,115
Poliwrath,Water,Fighting,2523,154
Abra,Psychic,,604,51
Kadabra,Psychic,,1140,75
Alakazam,Psychic,,1826,99
Machop,Fighting,,1097,122
Machoke,Fighting,,1773,138
Machamp,Fighting,,2612,154
Bellsprout,Grass,Poison,1125,91
Weepinbell,Grass,Poison,1736,115
Victreebel,Grass,Poison,2548,138
Tentacool,Water,Poison,911,75
Tentacruel,Water,Poison,2236,138
Geodude,Rock,Ground,855,75
Graveler,Rock,Ground,1443,99
Golem,Rock,Ground,2319,138
Ponyta,Fire,,1526,91
Rapidash,Fire,,2215,115
Slowpoke,Water,Psychic,1227,154
Slowbro,Water,Psychic,2615,162
Magnemite,Electric,Steel,897,51
Magneton,Electric,Steel,1893,91
Farfetch'd,Normal,Flying,1272,94
Doduo,Normal,Flying,861,67
Dodrio,Normal,Flying,1849,107
Seel,Water,,1114,115
Dewgong,Water,Ice,2161,154
Grimer,Poison,,1293,138
Muk,Poison,,2621,178
Shellder,Water,,828,59
Cloyster,Water,Ice,2067,91
Gastly,Ghost,Poison,810,59
Haunter,Ghost,Poison,1390,83
Gengar,Ghost,Poison,2093,107
Onix,Rock,Ground,863,67
Drowzee,Psychic,,1082,107
Hypno,Psychic,,2199,146
Krabby,Water,,797,59
Kingler,Water,,1836,99
Voltorb,Electric,,845,75
Electrode,Electric,,1657,107
Exeggcute,Grass,Psychic,1107,107
Exeggutor,Grass,Psychic,2976,162
Cubone,Ground,,1013,91
Marowak,Ground,,1668,107
Hitmonlee,Fighting,,1503,91
Hitmonchan,Fighting,,1527,91
Lickitung,Normal,,1638,154
Koffing,Poison,,1160,75
Weezing,Poison,,2266,115
Rhyhorn,Ground,Rock,1190,138
Rhydon,Ground,Rock,2259,178
Chansey,Normal,,679,408
Tangela,Grass,,1752,115
Kangaskhan,Normal,,2057,178
Horsea,Water,,800,59
Seadra,Water,,1725,99
Goldeen,Water,,972,83
Seaking,Water,,2058,138
Staryu,Water,,944,59
Starmie,Water,Psychic,2197,107
Mr. Mime,Psychic,Fairy,1505,75
Scyther,Bug,Flying,2088,122
Jynx,Ice,Psychic,1728,115
Electabuzz,Electric,,2134,115
Magmar,Fire,,2281,115
Pinsir,Bug,,2137,115
Tauros,Normal,,1857,130
Magikarp,Water,,264,43
Gyarados,Water,Flying,2708,162
Lapras,Water,Ice,3002,218
Ditto,Normal,,926,88
Eevee,Normal,,1084,99
Vaporeon,Water,,2836,218
Jolteon,Electric,,2155,115
Flareon,Fire,,2662,115
Porygon,Normal,,1703,115
Omanyte,Rock,Water,1127,67
Omastar,Rock,Water,2249,122
Kabuto,Rock,Water,1112,59
Kabutops,Rock,Water,2145,107
Aerodactyl,Rock,Flying,2180,138
Snorlax,Normal,,3135,265
Articuno,Ice,Flying,2999,154
Zapdos,Electric,Flying,3136,154
Moltres,Fire,Flying,3263,154
Dratini,Dragon,,990,76
Dragonair,Dragon,,1760,108
Dragonite,Dragon,Flying,3525,156
Mewtwo,Psychic,,4174,180
Mew,Psychic,,3322,170
Enter fullscreen mode Exit fullscreen mode

So what is the plan here ?

We will create a simple LookUp DB service for pokemon using redisOM python, Redis search-py & FastAPI

  1. FastApi for building a microservice
  2. redisOM as an ODM
  3. redisearch-py for redissearch operations.

let's create a file called model.py

Here we are going to create a DB Model analyzing the data we have.

Creating auto indexes over a field is as simple as setting index=True, as well as setting up full text search takes enabling full_text_search=True on a field

from aredis_om import HashModel,JsonModel, NotFoundError , Field 
from pydantic import BaseModel


class Pokemons(HashModel, BaseModel):
    Name: str = Field(index=True, full_text_search=True)
    Type_1: str = Field(index=True, full_text_search=True)
    Type_2: str = Field(index=True, full_text_search=True)
    Max_CP:int
    Max_HP:int
Enter fullscreen mode Exit fullscreen mode

Now lets upload the data to DB, create a file called uploader.py

from csv import DictReader

import aioredis
import asyncio
from aredis_om import HashModel, NotFoundError  
from aredis_om import get_redis_connection
from aredis_om import Migrator
from model import Pokemons

async def main():
    with open("pokemonGO.csv", 'r') as f:

        dict_reader = DictReader(f)

        list_of_dict = list(dict_reader)
        for i in list_of_dict:
            print(i)
            d = Pokemons(**i)
            await d.save()


    await Migrator().run()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
Enter fullscreen mode Exit fullscreen mode

Now create a fastapi server file server.py

import asyncio

from fastapi import FastAPI, HTTPException
from starlette.requests import Request
from starlette.responses import Response
from aredis_om import HashModel, NotFoundError , Migrator 
from aredis_om import get_redis_connectio
from model import Pokemons
from redis import ResponseError
from redisearch import Client, IndexDefinition, TextField, NumericField, TagField, GeoField
from redisearch import reducers
from redisearch.aggregation import AggregateRequest, Asc, Desc
Enter fullscreen mode Exit fullscreen mode
  • Redissearch-py initializing client.
def initializeClient():
    SCHEMA = (
        TextField("Name"),
        TextField("Type_1"),
        TextField("Type_2"),
        NumericField("Max_CP"),
        NumericField("Max_HP")
    )
    client =  Client("pokedex", host=<host>, port=12830, conn=None, password=<password>, decode_responses=True)
    definition = IndexDefinition(prefix=[':model.Pokemons:'])
    try:
        client.info()
    except ResponseError:
        # Index does not exist. We need to create it!
        client.create_index(SCHEMA, definition=definition)
    return client


client = initializeClient()
Enter fullscreen mode Exit fullscreen mode

Creating endpoints

Getting/Setting endpoints for our lovely pokemon.

Lets Go

This endpoint creates a pokemon, try sending it a raw JSON and you'll see the pokemon inserted in our DB.

app = FastAPI()

@app.post("/pokemon")
async def save_pkemon(pokemon: Pokemons):
    return await pokemon.save() 
Enter fullscreen mode Exit fullscreen mode
  • Performing a search using redisearch-py client The Redis client can be used to search across the DB for a specific attribute. Here we're creating endpoints for searching based on name, type 1 & 2. (In reality, you'll combine these endpoints for complete searching.)
@app.get("/search/{keyword}")
async def search_pokemon(keyword:str):
    return client.search(f"@Name:{keyword}") 

@app.get("/search-type1/{keyword}")
async def search_pokemon(keyword:str):
    return client.search(f"@Type_1:{keyword}*") 

@app.get("/search-type2/{keyword}")
async def search_pokemon(keyword:str):
    return client.search(f"@Type_2:{keyword}*")
Enter fullscreen mode Exit fullscreen mode
  • Finding them using RedisOM Lets do the same with the RedisOM which has a simpler syntax (according to me 😉)
@app.get("/fuzzy-search/{keyword}")
async def fuzzy_pokemons(keyword:str):
    return await Pokemons.find(Pokemons.Name % keyword).all()
Enter fullscreen mode Exit fullscreen mode
  • Listing all pokemon

Here Come the last endpoint where we'll just list all our cutemon's


@app.get("/pokemon/")
async def list_pokemons():
    return await Pokemons.find().all()

Enter fullscreen mode Exit fullscreen mode
  • Finally, let's see this server performing for us.

This will initialize the server and generate our documentation for interacting with the endpoints (Swagger got the swag!!! 😎)

import uvicorn
if (__name__ == "__main__"):
    uvicorn.run("main:app", port=8000, reload=True, access_log=False)

Enter fullscreen mode Exit fullscreen mode

Run the main.py file

python main.py
Enter fullscreen mode Exit fullscreen mode

In your browser visit http://localhost:8000/docs. You’ll be greeted with the Swagger/Open API docs with all the endpoints created above.

Conclusion:

The setup is easy if you know what's to be done and how things fit together, but then the responsibility of having to look up for scaling and failure goes up. Redis cloud is better suited for a developer who needs to move fast without worrying about infrastructure concerns. The ODM is easy to get going and can be used with JSON to model any complex data structure.
Redis in all is 🔥 with its capabilities!

Shout out to redis team and My friend who helped me validate this post.

Learn more:

  1. Try Redis Cloud for free
  2. Redis Developer Hub - tools, guides, and tutorials about Redis
  3. RedisInsight Desktop GUI

Top comments (1)

Collapse
 
astron17 profile image
Aravinth Raaj

Good explanation