Redis & Advanced Caching

Redis Data Types: Strings

20 min Lesson 3 of 30

Redis Strings

Strings are the most basic and versatile data type in Redis. Despite the name, Redis strings are binary-safe and can contain any kind of data - text, numbers, serialized objects, or even images (up to 512 MB).

Binary-Safe: Redis strings can contain any sequence of bytes, including null bytes. This makes them suitable for storing binary data like images, compressed files, or serialized objects.

SET and GET Commands

The fundamental operations for working with strings are SET (write) and GET (read):

// Basic SET and GET\n127.0.0.1:6379> SET name "Edrees Salih"\nOK\n\n127.0.0.1:6379> GET name\n"Edrees Salih"\n\n// SET overwrites existing values\n127.0.0.1:6379> SET name "Ahmed Ali"\nOK\n\n127.0.0.1:6379> GET name\n"Ahmed Ali"\n\n// GET non-existent key returns nil\n127.0.0.1:6379> GET nonexistent\n(nil)

Using SET and GET in Laravel

use Illuminate\Support\Facades\Redis;\n\n// Set a value\nRedis::set('user:name', 'Edrees Salih');\n\n// Get a value\n$name = Redis::get('user:name');\necho $name; // "Edrees Salih"\n\n// Check if key exists\nif (Redis::exists('user:name')) {\n echo "Key exists!";\n}

MSET and MGET - Multiple Keys

Set or get multiple keys in a single atomic operation - much faster than multiple SET/GET calls:

// MSET - Set multiple keys at once\n127.0.0.1:6379> MSET user:1:name "Edrees" user:1:email "edrees@example.com" user:1:role "admin"\nOK\n\n// MGET - Get multiple keys at once\n127.0.0.1:6379> MGET user:1:name user:1:email user:1:role\n1) "Edrees"\n2) "edrees@example.com"\n3) "admin"
// Laravel example\nRedis::mset([\n 'product:100:name' => 'Laptop',\n 'product:100:price' => '999.99',\n 'product:100:stock' => '50'\n]);\n\n$values = Redis::mget(['product:100:name', 'product:100:price', 'product:100:stock']);\n// Returns: ["Laptop", "999.99", "50"]
Performance: MSET/MGET reduce network round-trips. Setting 100 keys with MSET is ~100x faster than 100 individual SET commands!

INCR and DECR - Atomic Counters

Redis provides atomic increment and decrement operations - perfect for counters, views, likes, and rate limiting:

// INCR - Increment by 1\n127.0.0.1:6379> SET page_views 100\nOK\n\n127.0.0.1:6379> INCR page_views\n(integer) 101\n\n127.0.0.1:6379> INCR page_views\n(integer) 102\n\n// DECR - Decrement by 1\n127.0.0.1:6379> DECR page_views\n(integer) 101\n\n// INCRBY - Increment by specified amount\n127.0.0.1:6379> INCRBY page_views 10\n(integer) 111\n\n// DECRBY - Decrement by specified amount\n127.0.0.1:6379> DECRBY page_views 5\n(integer) 106\n\n// INCR on non-existent key starts at 0\n127.0.0.1:6379> INCR new_counter\n(integer) 1

Practical Use Cases for Counters

// Track page views\nRedis::incr('page:home:views');\n\n// Track API requests (rate limiting)\n$key = 'rate_limit:user:' . $userId . ':' . date('Y-m-d-H-i');\nRedis::incr($key);\nRedis::expire($key, 60); // Expire after 60 seconds\n\nif (Redis::get($key) > 100) {\n throw new Exception('Rate limit exceeded');\n}\n\n// Track product stock\nRedis::decrby('product:' . $productId . 'stock', $quantity);\n\n// Track likes/votes\nRedis::incr('post:' . $postId . ':likes');
Atomicity: INCR/DECR operations are atomic - no race conditions even with concurrent requests. This makes Redis perfect for counters in distributed systems.

APPEND and STRLEN

Build strings incrementally and check their length:

// APPEND - Add to end of string\n127.0.0.1:6379> SET message "Hello"\nOK\n\n127.0.0.1:6379> APPEND message " World"\n(integer) 11\n\n127.0.0.1:6379> GET message\n"Hello World"\n\n// STRLEN - Get string length\n127.0.0.1:6379> STRLEN message\n(integer) 11\n\n// APPEND to non-existent key creates it\n127.0.0.1:6379> APPEND newkey "value"\n(integer) 5
// Laravel example - Building logs\n$logKey = 'app:log:' . date('Y-m-d');\nRedis::append($logKey, date('H:i:s') . ' - User logged in\n');\nRedis::append($logKey, date('H:i:s') . ' - Order created\n');\n\n$logSize = Redis::strlen($logKey);\necho "Log size: " . $logSize . " bytes";

TTL and EXPIRE - Key Expiration

Set automatic expiration on keys - essential for caching and temporary data:

// EXPIRE - Set expiration in seconds\n127.0.0.1:6379> SET session:abc123 "user_data"\nOK\n\n127.0.0.1:6379> EXPIRE session:abc123 3600\n(integer) 1 // 1 = success\n\n// TTL - Check time to live (seconds remaining)\n127.0.0.1:6379> TTL session:abc123\n(integer) 3595\n\n// EXPIREAT - Expire at specific Unix timestamp\n127.0.0.1:6379> EXPIREAT session:abc123 1735689600\n(integer) 1\n\n// PERSIST - Remove expiration\n127.0.0.1:6379> PERSIST session:abc123\n(integer) 1\n\n// TTL returns -1 for keys without expiration\n127.0.0.1:6379> TTL session:abc123\n(integer) -1\n\n// TTL returns -2 for non-existent keys\n127.0.0.1:6379> TTL nonexistent\n(integer) -2
// Laravel example\n// Set with expiration (in seconds)\nRedis::setex('cache:product:100', 3600, $productData);\n\n// Or use set with expire\nRedis::set('cache:product:100', $productData);\nRedis::expire('cache:product:100', 3600);\n\n// Check TTL\n$ttl = Redis::ttl('cache:product:100');\necho "Expires in {$ttl} seconds";\n\n// Remove expiration\nRedis::persist('cache:product:100');

SETEX - Set with Expiration

Combine SET and EXPIRE into a single atomic operation:

// SETEX - Set with expiration (seconds)\n127.0.0.1:6379> SETEX cache:homepage 300 "<html>...</html>"\nOK\n\n// Equivalent to:\n127.0.0.1:6379> SET cache:homepage "<html>...</html>"\n127.0.0.1:6379> EXPIRE cache:homepage 300\n\n// PSETEX - Set with expiration in milliseconds\n127.0.0.1:6379> PSETEX cache:api_response 5000 '{"status":"ok"}'\nOK
Best Practice: Always use SETEX instead of separate SET + EXPIRE. It's atomic (no race condition) and faster (one command instead of two).

NX and XX Flags - Conditional SET

Control when SET should succeed based on key existence:

// SETNX - Set if Not eXists (NX flag)\n127.0.0.1:6379> SETNX lock:process "locked"\n(integer) 1 // Success - key didn't exist\n\n127.0.0.1:6379> SETNX lock:process "locked"\n(integer) 0 // Failed - key already exists\n\n// SET with NX flag (modern syntax)\n127.0.0.1:6379> SET lock:process "locked" NX\nOK\n\n127.0.0.1:6379> SET lock:process "locked" NX\n(nil) // Failed\n\n// SET with XX flag - Set only if key eXists\n127.0.0.1:6379> SET existing_key "new_value" XX\nOK // Success if key exists\n\n127.0.0.1:6379> SET nonexistent_key "value" XX\n(nil) // Failed - key doesn't exist\n\n// Combine NX with EX for atomic lock with expiration\n127.0.0.1:6379> SET lock:process "locked" NX EX 30\nOK // Lock acquired for 30 seconds

Distributed Locking Pattern

// Acquire lock with automatic expiration\n$lockKey = 'lock:import_products';\n$lockValue = uniqid(); // Unique value to identify this lock holder\n\n$acquired = Redis::set($lockKey, $lockValue, 'NX', 'EX', 300);\n\nif ($acquired) {\n try {\n // Critical section - only one process can be here\n $this->importProducts();\n } finally {\n // Release lock (only if we still own it)\n if (Redis::get($lockKey) === $lockValue) {\n Redis::del($lockKey);\n }\n }\n} else {\n throw new Exception('Could not acquire lock - another process is running');\n}

String Encoding and Memory Optimization

Redis automatically optimizes string storage based on content:

// Integers stored as integers (not strings) - saves memory\n127.0.0.1:6379> SET count 12345\nOK\n\n127.0.0.1:6379> OBJECT ENCODING count\n"int"\n\n// Small strings stored efficiently\n127.0.0.1:6379> SET short "hello"\nOK\n\n127.0.0.1:6379> OBJECT ENCODING short\n"embstr" // Embedded string - optimized\n\n// Large strings use raw encoding\n127.0.0.1:6379> SET large "very long string..."\nOK\n\n127.0.0.1:6379> OBJECT ENCODING large\n"raw"
Memory Optimization: Redis uses up to 3 different encodings for strings:
  • int: 64-bit signed integer (8 bytes)
  • embstr: Strings ≤ 44 bytes (optimized, immutable)
  • raw: Strings > 44 bytes (standard encoding)

Common String Patterns

// 1. Cache database queries\n$cacheKey = 'users:active:list';\n$users = Redis::get($cacheKey);\n\nif (!$users) {\n $users = User::where('active', true)->get()->toJson();\n Redis::setex($cacheKey, 600, $users); // Cache 10 minutes\n}\n\n// 2. Session storage\n$sessionId = session()->getId();\nRedis::setex('session:' . $sessionId, 7200, serialize($sessionData));\n\n// 3. API response caching\n$apiKey = 'api:weather:' . $city;\n$response = Redis::get($apiKey);\n\nif (!$response) {\n $response = $this->callWeatherAPI($city);\n Redis::setex($apiKey, 1800, $response); // Cache 30 minutes\n}\n\n// 4. Feature flags\nRedis::set('feature:new_dashboard', 'enabled');\n\nif (Redis::get('feature:new_dashboard') === 'enabled') {\n // Show new dashboard\n}
String Size Limits:
  • Maximum string size: 512 MB
  • Practical limit: Keep strings under 1 MB for best performance
  • Large strings slow down serialization and network transfer
  • Consider using hashes for structured data instead
Exercise: Implement a page view counter system:
  1. Create a function that increments view count for a given page
  2. Track both daily and total views (use two keys)
  3. Set daily counter to expire at end of day
  4. Add a function to get current view counts
  5. Test with multiple page IDs and verify counters work correctly
  6. Bonus: Implement a rate limiter that blocks users who view more than 100 pages per minute

In the next lesson, we'll explore Redis Lists and Sets - powerful data structures for managing collections of items.