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
Product Catalog:
- Create a Redis Hash for a product (e.g.,
product:id:123). Store fields likename,description,price,category, andquantity. - Initialize the product with some values.
- Reduce the
quantityby 5 when an “order” occurs. - Update the
priceto a new value. - Retrieve all product details.
- Challenge: Implement logic to prevent
quantityfrom going below zero during a decrement operation using a simpleHGETandHSETconditional check (non-atomic for now, we’ll cover atomic checks later).
- Create a Redis Hash for a product (e.g.,
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 (
themeandnotifications_enabled). - Challenge: Imagine
notifications_enabledis 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?
- For a given
Real-time Stock Ticker:
- Create a hash for a stock symbol, e.g.,
stock:GOOG. Storelast_price,open_price,high_price,low_price,volume. - Simulate a trade: update
last_price. If thelast_priceis higher thanhigh_price, updatehigh_price. If lower thanlow_price, updatelow_price. Incrementvolumeby 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_priceandvolume. - Challenge: Set an expiration on the entire stock hash, perhaps 5 minutes, assuming historical data is saved elsewhere.
- Create a hash for a stock symbol, e.g.,
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.