Redis Core Concepts: Hashes

While Redis Strings are great for simple key-value pairs, what if you need to store more complex, structured data, similar to a JavaScript object or a Python dictionary? That’s where Redis Hashes come in.

A Redis Hash is a map between string fields and string values. It’s ideal for representing objects, like a user profile, a product, or a configuration set, where each object has multiple attributes (fields) and their corresponding values.

In this chapter, we will explore:

  • The structure and benefits of Redis Hashes.
  • Commands for adding, retrieving, updating, and deleting individual fields within a hash.
  • Commands for working with multiple fields at once.
  • Advanced hash operations.

Understanding Redis Hashes

Think of a Redis Hash as a single Redis key that points to a collection of field-value pairs.

key -> { field1: value1, field2: value2, field3: value3, ... }

Key characteristics:

  • Space-efficient: For storing a small number of fields (e.g., less than a few thousand), Hashes are very memory-efficient compared to storing each field as a separate Redis String key.
  • Atomic operations per field: You can update individual fields within a hash atomically.
  • Organized data: Keeps related data together under a single logical key.

Basic Hash Commands (CRUD)

1. HSET key field value [field value ...] (Create/Update Fields)

Sets the value of field in the hash stored at key. If key does not exist, a new hash is created. If field already exists in the hash, its value is overwritten. You can set multiple field-value pairs in a single command.

Node.js Example:

// redis-hashes.js
const Redis = require('ioredis');
const redis = new Redis();

async function hsetExample() {
  try {
    // Set individual fields
    let response = await redis.hset('user:100', 'name', 'Alice', 'email', 'alice@example.com');
    console.log(`HSET user:100 -> ${response}`); // Number of fields added (2 if new, 0 if updated)

    // Overwrite a field
    response = await redis.hset('user:100', 'name', 'Alice Wonderland');
    console.log(`HSET user:100 (overwrite name) -> ${response}`); // 0 if updated

    // Add another field
    response = await redis.hset('user:100', 'age', '30');
    console.log(`HSET user:100 (add age) -> ${response}`); // 1 if new field added

    // Setting multiple fields at once
    response = await redis.hset('product:sku:A123', {
      name: 'Wireless Mouse',
      brand: 'Logitech',
      price: '25.99',
      stock: '500'
    });
    console.log(`HSET product:sku:A123 with object -> ${response}`); // Number of fields added (4 if new, 0 if updated)

  } catch (err) {
    console.error('Error in hsetExample:', err);
  }
}

// hsetExample().then(() => redis.quit());

Python Example:

# redis_hashes.py
import redis

r = redis.Redis(host='localhost', port=6379, db=0)

def hset_example():
    try:
        # Set individual fields
        response = r.hset('user:101', 'name', 'Bob')
        print(f"HSET user:101 (name) -> {response}") # Number of fields added (1 if new, 0 if updated)

        response = r.hset('user:101', 'email', 'bob@example.com')
        print(f"HSET user:101 (email) -> {response}") # Number of fields added

        # Overwrite a field
        response = r.hset('user:101', 'name', 'Bob The Builder')
        print(f"HSET user:101 (overwrite name) -> {response}") # 0 if updated

        # Setting multiple fields at once
        # Note: redis-py's hset with mapping directly corresponds to HSET field value field value ...
        response = r.hset('product:sku:B456', mapping={
            'name': 'Mechanical Keyboard',
            'brand': 'Keychron',
            'price': '120.00',
            'layout': 'US ANSI'
        })
        print(f"HSET product:sku:B456 with mapping -> {response}") # Number of fields added

    except Exception as e:
        print(f"Error in hset_example: {e}")

# hset_example()
# r.close()

2. HGET key field (Read Field)

Retrieves the value associated with field in the hash stored at key. If key or field does not exist, it returns null (Node.js) or None (Python).

Node.js Example:

// ... (previous setup)
async function hgetExample() {
  try {
    // Ensure the hash exists
    await redis.hset('user:100', 'name', 'Alice', 'email', 'alice@example.com', 'age', '30');

    let name = await redis.hget('user:100', 'name');
    console.log(`HGET user:100 name -> ${name}`); // Output: Alice

    let email = await redis.hget('user:100', 'email');
    console.log(`HGET user:100 email -> ${email}`); // Output: alice@example.com

    let nonExistentField = await redis.hget('user:100', 'phone');
    console.log(`HGET user:100 phone -> ${nonExistentField}`); // Output: null

    let nonExistentKey = await redis.hget('user:999', 'name');
    console.log(`HGET user:999 name -> ${nonExistentKey}`); // Output: null

  } catch (err) {
    console.error('Error in hgetExample:', err);
  }
}

// hgetExample().then(() => redis.quit());

Python Example:

# ... (previous setup)
def hget_example():
    try:
        # Ensure the hash exists
        r.hset('user:101', mapping={'name': 'Bob', 'email': 'bob@example.com', 'age': '25'})

        name = r.hget('user:101', 'name')
        print(f"HGET user:101 name -> {name.decode('utf-8') if name else None}") # Output: Bob

        email = r.hget('user:101', 'email')
        print(f"HGET user:101 email -> {email.decode('utf-8') if email else None}") # Output: bob@example.com

        non_existent_field = r.hget('user:101', 'phone')
        print(f"HGET user:101 phone -> {non_existent_field.decode('utf-8') if non_existent_field else None}") # Output: None

        non_existent_key = r.hget('user:999', 'name')
        print(f"HGET user:999 name -> {non_existent_key.decode('utf-8') if non_existent_key else None}") # Output: None

    except Exception as e:
        print(f"Error in hget_example: {e}")

# hget_example()
# r.close()

3. HDEL key field [field ...] (Delete Fields)

Removes the specified fields from the hash stored at key. Returns the number of fields that were removed.

Node.js Example:

// ... (previous setup)
async function hdelExample() {
  try {
    await redis.hset('user:100', 'name', 'Alice', 'email', 'alice@example.com', 'age', '30');
    console.log(`User 100 before HDEL: ${JSON.stringify(await redis.hgetall('user:100'))}`);

    let deletedCount = await redis.hdel('user:100', 'age', 'phone');
    console.log(`HDEL user:100 age phone -> ${deletedCount}`); // Output: 1 (only 'age' was removed)

    console.log(`User 100 after HDEL: ${JSON.stringify(await redis.hgetall('user:100'))}`);

  } catch (err) {
    console.error('Error in hdelExample:', err);
  } finally {
    await redis.del('user:100');
  }
}

// hdelExample().then(() => redis.quit());

Python Example:

# ... (previous setup)
def hdel_example():
    try:
        r.hset('user:101', mapping={'name': 'Bob', 'email': 'bob@example.com', 'age': '25'})
        print(f"User 101 before HDEL: {r.hgetall('user:101')}")

        deleted_count = r.hdel('user:101', 'email', 'address')
        print(f"HDEL user:101 email address -> {deleted_count}") # Output: 1 (only 'email' was removed)

        print(f"User 101 after HDEL: {r.hgetall('user:101')}")

    except Exception as e:
        print(f"Error in hdel_example: {e}")
    finally:
        r.delete('user:101')

# hdel_example()
# r.close()

Working with Multiple Fields

1. HGETALL key

Returns all fields and values of the hash stored at key.

Node.js Example:

// ... (previous setup)
async function hgetallExample() {
  try {
    await redis.hset('user:100', 'name', 'Alice', 'email', 'alice@example.com', 'age', '30');
    let userData = await redis.hgetall('user:100');
    console.log(`HGETALL user:100 -> ${JSON.stringify(userData)}`);
    // Output: {"name":"Alice","email":"alice@example.com","age":"30"}

    let productData = await redis.hgetall('product:sku:A123');
    console.log(`HGETALL product:sku:A123 -> ${JSON.stringify(productData)}`);
    // Output: {"name":"Wireless Mouse","brand":"Logitech","price":"25.99","stock":"500"}

  } catch (err) {
    console.error('Error in hgetallExample:', err);
  } finally {
    await redis.del('user:100', 'product:sku:A123');
  }
}

// hgetallExample().then(() => redis.quit());

Python Example:

# ... (previous setup)
def hgetall_example():
    try:
        r.hset('user:101', mapping={'name': 'Bob', 'email': 'bob@example.com', 'age': '25'})
        user_data = r.hgetall('user:101')
        # user_data will be bytes, decode fields and values
        decoded_user_data = {k.decode('utf-8'): v.decode('utf-8') for k, v in user_data.items()}
        print(f"HGETALL user:101 -> {decoded_user_data}")
        # Output: {'name': 'Bob', 'email': 'bob@example.com', 'age': '25'}

        r.hset('product:sku:B456', mapping={
            'name': 'Mechanical Keyboard', 'brand': 'Keychron', 'price': '120.00', 'layout': 'US ANSI'
        })
        product_data = r.hgetall('product:sku:B456')
        decoded_product_data = {k.decode('utf-8'): v.decode('utf-8') for k, v in product_data.items()}
        print(f"HGETALL product:sku:B456 -> {decoded_product_data}")

    except Exception as e:
        print(f"Error in hgetall_example: {e}")
    finally:
        r.delete('user:101', 'product:sku:B456')

# hgetall_example()
# r.close()

Note: HGETALL can be a blocking command if the hash contains a very large number of fields, as it retrieves all of them. For very large hashes (e.g., millions of fields), consider using HSCAN (covered in advanced topics) for iterative scanning.

2. HMGET key field [field ...]

Returns the values associated with the specified fields in the hash stored at key.

Node.js Example:

// ... (previous setup)
async function hmgetExample() {
  try {
    await redis.hset('user:100', 'name', 'Alice', 'email', 'alice@example.com', 'age', '30');
    let values = await redis.hmget('user:100', 'name', 'age', 'city');
    console.log(`HMGET user:100 name age city -> ${JSON.stringify(values)}`);
    // Output: ["Alice", "30", null] (city does not exist)
  } catch (err) {
    console.error('Error in hmgetExample:', err);
  } finally {
    await redis.del('user:100');
  }
}

// hmgetExample().then(() => redis.quit());

Python Example:

# ... (previous setup)
def hmget_example():
    try:
        r.hset('user:101', mapping={'name': 'Bob', 'email': 'bob@example.com', 'age': '25'})
        values = r.hmget('user:101', 'name', 'age', 'city')
        decoded_values = [v.decode('utf-8') if v else None for v in values]
        print(f"HMGET user:101 name age city -> {decoded_values}")
        # Output: ['Bob', '25', None]
    except Exception as e:
        print(f"Error in hmget_example: {e}")
    finally:
        r.delete('user:101')

# hmget_example()
# r.close()

Advanced Hash Operations

1. HINCRBY key field increment

Atomically increments the number stored at field in the hash stored at key by increment. If key does not exist, a new hash is created. If field does not exist, it’s initialized to 0 before the operation.

2. HINCRBYFLOAT key field increment

Similar to HINCRBY, but for floating-point numbers.

Node.js Example:

// ... (previous setup)
async function hincrbyExample() {
  try {
    await redis.hset('product:A123', 'stock', '100', 'rating', '4.5');
    console.log(`Product A123 initial stock: ${await redis.hget('product:A123', 'stock')}`);

    let newStock = await redis.hincrby('product:A123', 'stock', -10); // Sell 10 items
    console.log(`Product A123 stock after sale: ${newStock}`); // Output: 90

    let newRating = await redis.hincrbyfloat('product:A123', 'rating', 0.2); // New review
    console.log(`Product A123 rating after new review: ${newRating}`); // Output: 4.7

    // Incrementing a non-existent field
    let newSalesCount = await redis.hincrby('product:A123', 'salesCount', 1);
    console.log(`Product A123 sales count: ${newSalesCount}`); // Output: 1

  } catch (err) {
    console.error('Error in hincrbyExample:', err);
  } finally {
    await redis.del('product:A123');
  }
}

// hincrbyExample().then(() => redis.quit());

Python Example:

# ... (previous setup)
def hincrby_example():
    try:
        r.hset('product:B456', mapping={'stock': '200', 'rating': '3.8'})
        print(f"Product B456 initial stock: {r.hget('product:B456', 'stock').decode('utf-8')}")

        new_stock = r.hincrby('product:B456', 'stock', -20) # Ship 20 items
        print(f"Product B456 stock after shipment: {new_stock}") # Output: 180

        new_rating = r.hincrbyfloat('product:B456', 'rating', 0.5) # Positive reviews
        print(f"Product B456 rating after new reviews: {new_rating}") # Output: 4.3

        # Incrementing a non-existent field
        new_returnCount = r.hincrby('product:B456', 'returnCount', 1)
        print(f"Product B456 return count: {new_returnCount}") # Output: 1

    except Exception as e:
        print(f"Error in hincrby_example: {e}")
    finally:
        r.delete('product:B456')

# hincrby_example()
# r.close()

Full Node.js Example with Hashes

// full_hash_operations.js
const Redis = require('ioredis');
const redis = new Redis();

async function runAllHashExamples() {
  console.log('--- Running Hash Examples ---');

  const userKey = 'user:profile:789';

  // 1. Initial user profile creation
  await redis.hset(userKey, {
    username: 'john_doe',
    email: 'john@example.com',
    status: 'active',
    karma: '100',
    last_login: Date.now().toString()
  });
  console.log(`\nCreated user profile for ${userKey}:`);
  console.log(await redis.hgetall(userKey));

  // 2. Update user status and karma
  await redis.hset(userKey, 'status', 'inactive');
  await redis.hincrby(userKey, 'karma', 20); // User earned 20 karma
  console.log(`\nUpdated user profile after status change and karma gain:`);
  console.log(await redis.hgetall(userKey));

  // 3. Retrieve specific fields
  const [username, email] = await redis.hmget(userKey, 'username', 'email');
  console.log(`\nRetrieved username: ${username}, email: ${email}`);

  // 4. Check for existence of a field
  const emailExists = await redis.hexists(userKey, 'email');
  const phoneExists = await redis.hexists(userKey, 'phone');
  console.log(`Does 'email' field exist? ${emailExists ? 'Yes' : 'No'}`);
  console.log(`Does 'phone' field exist? ${phoneExists ? 'Yes' : 'No'}`);

  // 5. Delete a field
  await redis.hdel(userKey, 'last_login');
  console.log(`\nUser profile after deleting 'last_login':`);
  console.log(await redis.hgetall(userKey));

  // 6. Set expiration for the entire hash key (e.g., for caching)
  await redis.expire(userKey, 60); // Cache user profile for 60 seconds
  console.log(`\nUser profile key '${userKey}' set to expire in ${await redis.ttl(userKey)} seconds.`);

  console.log('--- Hash Examples Complete ---');
  await redis.del(userKey); // Clean up
  await redis.quit();
}

// runAllHashExamples();

Exercises / Mini-Challenges

  1. Product Catalog:

    • Create a Redis Hash for a product (e.g., product:id:123). Store fields like name, description, price, category, and quantity.
    • Initialize the product with some values.
    • Reduce the quantity by 5 when an “order” occurs.
    • Update the price to a new value.
    • Retrieve all product details.
    • Challenge: Implement logic to prevent quantity from going below zero during a decrement operation using a simple HGET and HSET conditional check (non-atomic for now, we’ll cover atomic checks later).
  2. User Preferences:

    • For a given userId (e.g., user:prefs:500), store user preferences in a hash: theme (e.g., ‘dark’), notifications_enabled (e.g., ’true’), language (e.g., ’en-US’).
    • Retrieve a specific preference (e.g., language).
    • Update two preferences at once (theme and notifications_enabled).
    • Challenge: Imagine notifications_enabled is a boolean. How would you store it (as a string) and how would you retrieve and convert it back to a boolean in your Node.js/Python code?
  3. Real-time Stock Ticker:

    • Create a hash for a stock symbol, e.g., stock:GOOG. Store last_price, open_price, high_price, low_price, volume.
    • Simulate a trade: update last_price. If the last_price is higher than high_price, update high_price. If lower than low_price, update low_price. Increment volume by the trade quantity. (This is complex to do atomically with simple hash commands, but try to do it step-by-step).
    • Retrieve the current last_price and volume.
    • Challenge: Set an expiration on the entire stock hash, perhaps 5 minutes, assuming historical data is saved elsewhere.

By completing these exercises, you’ll gain practical experience using Redis Hashes to manage structured data efficiently. Next, we’ll explore Lists, which are excellent for ordered collections and implementing queues.