Redis Core Concepts: Strings and Keys

Welcome to the heart of Redis! At its most fundamental level, Redis is a key-value store, and the most basic value you can store is a String. Understanding how to work with Strings and manage keys is crucial for building any application with Redis.

In this chapter, we’ll cover:

  • What Redis Strings are and their capabilities.
  • Basic commands for creating, reading, updating, and deleting (CRUD) string keys.
  • Advanced string operations like increments, decrements, and appending.
  • Key management strategies, including checking existence, renaming, and deleting.
  • The critical concept of key expiration (TTL).

What are Redis Strings?

A Redis String is the simplest type of value you can associate with a key. Despite the name “string,” it’s binary-safe, meaning it can store anything from text (like “Hello World!”) to integers, floating-point numbers, or even binary data like JPEG images or serialized objects, up to 512MB in size.

Key characteristics:

  • Binary-safe: No special handling for different character encodings.
  • Atomic operations: Operations on strings are atomic, ensuring consistency.
  • Versatile: Can be used for simple caching, counters, or flags.

Basic String Commands (CRUD)

Let’s explore the fundamental commands to interact with string keys.

1. SET key value (Create/Update)

Sets the value associated with key. If key already exists, its value is overwritten.

Node.js Example:

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

async function setStringExample() {
  try {
    // Set a new key-value pair
    let response = await redis.set('username:1', 'Alice');
    console.log(`SET username:1 -> ${response}`); // Output: OK

    // Overwrite an existing key
    response = await redis.set('username:1', 'Alice Smith');
    console.log(`SET username:1 (overwrite) -> ${response}`); // Output: OK

    // Set a key that holds an integer (Redis treats it as a string)
    response = await redis.set('user:visits:1', '100');
    console.log(`SET user:visits:1 -> ${response}`); // Output: OK

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

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

Python Example:

# redis_strings.py
import redis

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

def set_string_example():
    try:
        # Set a new key-value pair
        response = r.set('username:2', 'Bob')
        print(f"SET username:2 -> {response}") # Output: True (or 1)

        # Overwrite an existing key
        response = r.set('username:2', 'Bob Johnson')
        print(f"SET username:2 (overwrite) -> {response}") # Output: True (or 1)

        # Set a key that holds an integer (Redis treats it as a string)
        response = r.set('user:visits:2', '250')
        print(f"SET user:visits:2 -> {response}") # Output: True (or 1)

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

# set_string_example()
# r.close()

2. GET key (Read)

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

Node.js Example:

// ... (previous setup)
async function getStringExample() {
  try {
    // Retrieve an existing key
    let value = await redis.get('username:1');
    console.log(`GET username:1 -> ${value}`); // Output: Alice Smith

    // Retrieve a non-existent key
    value = await redis.get('nonexistentkey');
    console.log(`GET nonexistentkey -> ${value}`); // Output: null

    // Retrieve a key storing a number
    value = await redis.get('user:visits:1');
    console.log(`GET user:visits:1 -> ${value}`); // Output: 100

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

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

Python Example:

# ... (previous setup)
def get_string_example():
    try:
        # Retrieve an existing key
        value = r.get('username:2')
        print(f"GET username:2 -> {value.decode('utf-8') if value else None}") # Output: Bob Johnson

        # Retrieve a non-existent key
        value = r.get('nonexistentkey')
        print(f"GET nonexistentkey -> {value.decode('utf-8') if value else None}") # Output: None

        # Retrieve a key storing a number
        value = r.get('user:visits:2')
        print(f"GET user:visits:2 -> {value.decode('utf-8') if value else None}") # Output: 250

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

# get_string_example()
# r.close()

3. DEL key [key ...] (Delete)

Removes the specified keys. Returns the number of keys that were removed.

Node.js Example:

// ... (previous setup)
async function deleteStringExample() {
  try {
    await redis.set('tempkey1', 'value1');
    await redis.set('tempkey2', 'value2');

    let deletedCount = await redis.del('tempkey1', 'tempkey2', 'nonexistent');
    console.log(`DEL tempkey1 tempkey2 nonexistent -> ${deletedCount}`); // Output: 2

    let value = await redis.get('tempkey1');
    console.log(`GET tempkey1 after deletion -> ${value}`); // Output: null

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

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

Python Example:

# ... (previous setup)
def delete_string_example():
    try:
        r.set('tempkey3', 'value3')
        r.set('tempkey4', 'value4')

        deleted_count = r.delete('tempkey3', 'tempkey4', 'nonexistent')
        print(f"DEL tempkey3 tempkey4 nonexistent -> {deleted_count}") # Output: 2

        value = r.get('tempkey3')
        print(f"GET tempkey3 after deletion -> {value.decode('utf-8') if value else None}") # Output: None

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

# delete_string_example()
# r.close()

Advanced String Operations

Redis offers specialized commands for numerical string values and appending.

1. INCR key, DECR key, INCRBY key increment, DECRBY key decrement (Atomic Counters)

Atomically increments/decrements the number stored at key. If the key does not exist, it’s initialized to 0 before the operation.

Node.js Example:

// ... (previous setup)
async function counterExample() {
  try {
    // Initialize a counter
    await redis.set('page:views', '0');

    // Increment
    let views = await redis.incr('page:views');
    console.log(`INCR page:views -> ${views}`); // Output: 1

    views = await redis.incr('page:views');
    console.log(`INCR page:views -> ${views}`); // Output: 2

    // Increment by a specific amount
    views = await redis.incrby('page:views', 5);
    console.log(`INCRBY page:views 5 -> ${views}`); // Output: 7

    // Decrement
    views = await redis.decr('page:views');
    console.log(`DECR page:views -> ${views}`); // Output: 6

    // Decrement by a specific amount
    views = await redis.decrby('page:views', 3);
    console.log(`DECRBY page:views 3 -> ${views}`); // Output: 3

    // INCR on a non-existent key
    views = await redis.incr('unique:ids');
    console.log(`INCR unique:ids (new key) -> ${views}`); // Output: 1

  } catch (err) {
    console.error('Error in counterExample:', err);
  } finally {
    await redis.del('page:views', 'unique:ids');
  }
}

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

Python Example:

# ... (previous setup)
def counter_example():
    try:
        r.set('page:likes', '0')

        # Increment
        likes = r.incr('page:likes')
        print(f"INCR page:likes -> {likes}") # Output: 1

        likes = r.incr('page:likes')
        print(f"INCR page:likes -> {likes}") # Output: 2

        # Increment by a specific amount
        likes = r.incrby('page:likes', 10)
        print(f"INCRBY page:likes 10 -> {likes}") # Output: 12

        # Decrement
        likes = r.decr('page:likes')
        print(f"DECR page:likes -> {likes}") # Output: 11

        # Decrement by a specific amount
        likes = r.decrby('page:likes', 5)
        print(f"DECRBY page:likes 5 -> {likes}") # Output: 6

        # INCR on a non-existent key
        users = r.incr('total:users')
        print(f"INCR total:users (new key) -> {users}") # Output: 1

    except Exception as e:
        print(f"Error in counter_example: {e}")
    finally:
        r.delete('page:likes', 'total:users')

# counter_example()
# r.close()

2. APPEND key value

Appends value to the value of key. If key does not exist, it is created with an empty string, then value is appended. Returns the new length of the string.

Node.js Example:

// ... (previous setup)
async function appendExample() {
  try {
    await redis.set('greeting', 'Hello');
    let newLength = await redis.append('greeting', ' World');
    console.log(`APPEND greeting " World" -> new length: ${newLength}`); // Output: new length: 11
    console.log(`GET greeting -> ${await redis.get('greeting')}`); // Output: Hello World

    newLength = await redis.append('new_message', 'First part.');
    console.log(`APPEND new_message "First part." -> new length: ${newLength}`); // Output: new length: 11
    newLength = await redis.append('new_message', ' Second part.');
    console.log(`APPEND new_message " Second part." -> new length: ${newLength}`); // Output: new length: 24
    console.log(`GET new_message -> ${await redis.get('new_message')}`); // Output: First part. Second part.

  } catch (err) {
    console.error('Error in appendExample:', err);
  } finally {
    await redis.del('greeting', 'new_message');
  }
}

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

Python Example:

# ... (previous setup)
def append_example():
    try:
        r.set('message', 'Good')
        new_length = r.append('message', ' Morning')
        print(f"APPEND message ' Morning' -> new length: {new_length}") # Output: new length: 11
        print(f"GET message -> {r.get('message').decode('utf-8')}") # Output: Good Morning

        new_length = r.append('log_entry', 'Event 1 recorded.')
        print(f"APPEND log_entry 'Event 1 recorded.' -> new length: {new_length}") # Output: new length: 17
        new_length = r.append('log_entry', ' Event 2 followed.')
        print(f"APPEND log_entry ' Event 2 followed.' -> new length: {new_length}") # Output: new length: 35
        print(f"GET log_entry -> {r.get('log_entry').decode('utf-8')}") # Output: Event 1 recorded. Event 2 followed.

    except Exception as e:
        print(f"Error in append_example: {e}")
    finally:
        r.delete('message', 'log_entry')

# append_example()
# r.close()

Key Management

Beyond simple CRUD, Redis offers commands to manage keys themselves.

1. EXISTS key [key ...]

Checks if one or more keys exist. Returns the number of keys that exist.

Node.js Example:

// ... (previous setup)
async function existsExample() {
  try {
    await redis.set('product:101', 'laptop');
    let count = await redis.exists('product:101', 'product:102');
    console.log(`EXISTS product:101 product:102 -> ${count}`); // Output: 1 (only product:101 exists)
  } catch (err) {
    console.error('Error in existsExample:', err);
  } finally {
    await redis.del('product:101');
  }
}

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

2. RENAME oldkey newkey

Renames oldkey to newkey. If oldkey does not exist, an error is returned. If newkey already exists, it is overwritten.

Node.js Example:

// ... (previous setup)
async function renameExample() {
  try {
    await redis.set('old_name', 'some data');
    let response = await redis.rename('old_name', 'new_name');
    console.log(`RENAME old_name new_name -> ${response}`); // Output: OK
    console.log(`GET old_name -> ${await redis.get('old_name')}`); // Output: null
    console.log(`GET new_name -> ${await redis.get('new_name')}`); // Output: some data
  } catch (err) {
    console.error('Error in renameExample:', err);
  } finally {
    await redis.del('new_name');
  }
}

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

3. TYPE key

Returns the data type of the value stored at key.

Node.js Example:

// ... (previous setup)
async function typeExample() {
  try {
    await redis.set('my_string', 'hello');
    let type = await redis.type('my_string');
    console.log(`TYPE my_string -> ${type}`); // Output: string

    let nonExistentType = await redis.type('non_existent_key');
    console.log(`TYPE non_existent_key -> ${nonExistentType}`); // Output: none
  } catch (err) {
    console.error('Error in typeExample:', err);
  } finally {
    await redis.del('my_string');
  }
}

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

Key Expiration (TTL - Time To Live)

One of Redis’s most powerful features for caching and temporary data is key expiration. You can set a Time To Live (TTL) for a key, after which Redis will automatically delete it.

1. EXPIRE key seconds

Sets a timeout on key. After seconds, the key will be automatically deleted.

2. PEXPIRE key milliseconds

Sets a timeout on key in milliseconds.

3. TTL key

Returns the remaining time to live of a key in seconds. Returns -2 if the key does not exist, and -1 if the key exists but has no associated expire.

4. PTTL key

Returns the remaining time to live of a key in milliseconds.

5. SETEX key seconds value (Set with Expiration)

A convenience command to set a key’s value and its expiration time in one atomic operation.

Node.js Example:

// ... (previous setup)
async function expirationExample() {
  try {
    // Using SETEX
    await redis.setex('ephemeral_data', 5, 'this will disappear'); // Expires in 5 seconds
    console.log(`SETEX ephemeral_data (5s TTL) -> Value: ${await redis.get('ephemeral_data')}`);

    let ttl = await redis.ttl('ephemeral_data');
    console.log(`TTL ephemeral_data -> ${ttl} seconds remaining`);

    // Wait a bit
    await new Promise(resolve => setTimeout(resolve, 3000));
    ttl = await redis.ttl('ephemeral_data');
    console.log(`TTL ephemeral_data after 3 seconds -> ${ttl} seconds remaining`);

    // Wait for expiration
    await new Promise(resolve => setTimeout(resolve, 3000)); // Total 6 seconds
    console.log(`GET ephemeral_data after expiration -> ${await redis.get('ephemeral_data')}`); // Output: null

    // Setting explicit expire
    await redis.set('another_temp_key', 'some temporary value');
    await redis.expire('another_temp_key', 10); // Expires in 10 seconds
    console.log(`SET and EXPIRE another_temp_key -> TTL: ${await redis.ttl('another_temp_key')}`);

  } catch (err) {
    console.error('Error in expirationExample:', err);
  } finally {
    await redis.del('ephemeral_data', 'another_temp_key'); // Just in case
  }
}

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

Python Example:

# ... (previous setup)
import time

def expiration_example():
    try:
        # Using SETEX
        r.setex('session:user:123', 5, 'logged_in') # Expires in 5 seconds
        print(f"SETEX session:user:123 (5s TTL) -> Value: {r.get('session:user:123').decode('utf-8')}")

        ttl = r.ttl('session:user:123')
        print(f"TTL session:user:123 -> {ttl} seconds remaining")

        # Wait a bit
        time.sleep(3)
        ttl = r.ttl('session:user:123')
        print(f"TTL session:user:123 after 3 seconds -> {ttl} seconds remaining")

        # Wait for expiration
        time.sleep(3) # Total 6 seconds
        print(f"GET session:user:123 after expiration -> {r.get('session:user:123')}") # Output: None

        # Setting explicit expire
        r.set('cache:item:456', 'cached content')
        r.expire('cache:item:456', 10) # Expires in 10 seconds
        print(f"SET and EXPIRE cache:item:456 -> TTL: {r.ttl('cache:item:456')}")

    except Exception as e:
        print(f"Error in expiration_example: {e}")
    finally:
        r.delete('session:user:123', 'cache:item:456') # Just in case

# expiration_example()
# r.close()

Combining Everything

Let’s put some of these concepts together in a single Node.js script.

// full_string_operations.js
const Redis = require('ioredis');
const redis = new Redis(); // Connects to localhost:6379

async function runAllStringExamples() {
  console.log('--- Running String Examples ---');

  // SET, GET, EXISTS
  await redis.set('app:version', '1.0.0');
  console.log(`Current App Version: ${await redis.get('app:version')}`);
  console.log(`Does 'app:version' exist? ${await redis.exists('app:version') ? 'Yes' : 'No'}`);

  // INCR/DECR - User daily hits counter
  const userId = 'user:123';
  const dailyHitsKey = `daily:hits:${userId}`;
  await redis.set(dailyHitsKey, '0'); // Initialize
  await redis.expire(dailyHitsKey, 86400); // Expires in 24 hours

  console.log(`\nUser ${userId} hits: ${await redis.get(dailyHitsKey)}`);
  await redis.incr(dailyHitsKey);
  await redis.incr(dailyHitsKey);
  console.log(`User ${userId} hits after INCR: ${await redis.get(dailyHitsKey)}`);
  await redis.incrby(dailyHitsKey, 10);
  console.log(`User ${userId} hits after INCRBY 10: ${await redis.get(dailyHitsKey)}`);
  console.log(`Time left for daily hits counter: ${await redis.ttl(dailyHitsKey)}s`);

  // APPEND - Log aggregation
  const logKey = 'application:logs';
  await redis.set(logKey, 'Startup complete. ');
  await redis.append(logKey, 'Processing user requests. ');
  await redis.append(logKey, 'Database connection established.');
  console.log(`\nApplication Log: ${await redis.get(logKey)}`);

  // RENAME and DEL
  await redis.set('oldKey', 'some sensitive data');
  console.log(`\nBefore rename: GET oldKey -> ${await redis.get('oldKey')}`);
  await redis.rename('oldKey', 'newKey');
  console.log(`After rename: GET oldKey -> ${await redis.get('oldKey')}`);
  console.log(`After rename: GET newKey -> ${await redis.get('newKey')}`);
  await redis.del('newKey');
  console.log(`After DEL newKey: GET newKey -> ${await redis.get('newKey')}`);

  console.log('--- String Examples Complete ---');
  await redis.quit();
}

// Call the main function to execute all examples
runAllStringExamples();

Exercises / Mini-Challenges

  1. User Last Seen:

    • Create a Redis key for a user (e.g., user:status:456) and set its value to the current timestamp (you can use Date.now() in Node.js or time.time() in Python for milliseconds since epoch).
    • Set this key to expire after 60 seconds.
    • Retrieve the value and print the remaining TTL.
    • Wait for 30 seconds, then retrieve the TTL again.
    • Challenge: Modify the logic so that every time a user is “seen,” their timestamp is updated, and their expiration is reset to 60 seconds. (Hint: look into SET key value EX or EXPIRE after SET).
  2. Website Visitor Counter with Daily Reset:

    • Implement a global visitor counter for a website (website:visitors).
    • Every time a “visitor” arrives, increment this counter.
    • The counter should automatically reset to 0 every 24 hours.
    • Challenge: How would you ensure the counter starts at 0 if it doesn’t exist, and still sets the expiration? (Hint: SETNX or SET key value EX NX might be useful, or a combination of SET and EXPIRE).
  3. Basic Message Logging:

    • Create a key app:event_log.
    • Whenever an “event” occurs (e.g., “User logged in”, “Item added to cart”), append a timestamped message to this key.
    • Retrieve the full log and print it.
    • Challenge: How would you ensure the log doesn’t grow indefinitely, but only keeps the last 1000 characters for example? (Hint: Redis strings don’t have a direct “trim” for middle, but you could GETSET and truncate client-side, or think about a different data type for logs if this becomes too complex for strings - a foreshadowing!).

These exercises will help you solidify your understanding of Redis Strings and key management, which are foundational for more complex data types. In the next chapter, we’ll move on to Hashes, a data type perfect for representing objects.