Redis Caching in Practice
5/1/24About 4 min
Redis Caching in Practice
Overview
Redis is an open-source in-memory data structure store that can be used as a database, cache, and message broker. This article introduces Redis core features and practical applications.
1. Installation and Configuration
1.1 Installing with Docker
# Pull Redis image
docker pull redis:latest
# Start container
docker run -d \
--name redis \
-p 6379:6379 \
-v /path/to/data:/data \
redis:latest1.2 Configuration File
# redis.conf
bind 0.0.0.0
port 6379
requirepass yourpassword
maxmemory 1gb
maxmemory-policy allkeys-lru1.3 Starting a Container with Configuration
docker run -d \
--name redis \
-p 6379:6379 \
-v /path/to/redis.conf:/usr/local/etc/redis/redis.conf \
-v /path/to/data:/data \
redis:latest redis-server /usr/local/etc/redis/redis.conf2. Data Types
2.1 String
# Set value
SET key value
SET name "Blogger"
# Get value
GET key
GET name
# Set expiration
SET key value EX 3600 # expires in 1 hour
# Atomic operations
INCR counter
DECR counter
INCRBY counter 102.2 Hash
# Set hash values
HSET user:1 name "Blogger"
HSET user:1 age 30
HSET user:1 email "user@example.com"
# Get hash values
HGET user:1 name
HGETALL user:1
# Batch set
HMSET user:2 name "张三" age 25 email "zhangsan@example.com"
# Batch get
HMGET user:2 name age2.3 List
# Add from left
LPUSH tasks "task1"
LPUSH tasks "task2"
# Add from right
RPUSH tasks "task3"
# Get list
LRANGE tasks 0 -1
# Pop elements
LPOP tasks # pop from left
RPOP tasks # pop from right2.4 Set
# Add elements
SADD tags "java" "csharp" "python"
# Get all elements
SMEMBERS tags
# Check if element exists
SISMEMBER tags "java"
# Set operations
SADD set1 1 2 3
SADD set2 3 4 5
SINTER set1 set2 # intersection
SUNION set1 set2 # union
SDIFF set1 set2 # difference2.5 Sorted Set
# Add elements (with scores)
ZADD scores 95 "张三"
ZADD scores 88 "李四"
ZADD scores 92 "王五"
# Get ranking (high to low)
ZREVRANGE scores 0 2 WITHSCORES
# Get elements within score range
ZRANGEBYSCORE scores 90 100
# Get element score
ZSCORE scores "张三"3. Using the .NET Client
3.1 Installing Dependencies
dotnet add package StackExchange.Redis3.2 Basic Operations
using StackExchange.Redis;
// Create connection
var connection = ConnectionMultiplexer.Connect("localhost:6379,password=yourpassword");
// Get database
var db = connection.GetDatabase();
// String operations
await db.StringSetAsync("name", "Blogger");
var name = await db.StringGetAsync("name");
// Hash operations
await db.HashSetAsync("user:1", new HashEntry[]
{
new HashEntry("name", "Blogger"),
new HashEntry("age", 30),
new HashEntry("email", "user@example.com")
});
var user = await db.HashGetAllAsync("user:1");
// List operations
await db.ListLeftPushAsync("tasks", "task1");
await db.ListRightPushAsync("tasks", "task2");
var tasks = await db.ListRangeAsync("tasks");3.3 Distributed Lock
public async Task<bool> AcquireLock(string lockKey, string lockValue, TimeSpan expiry)
{
// SET key value NX EX seconds
return await db.StringSetAsync(lockKey, lockValue, expiry, When.NotExists);
}
public async Task<bool> ReleaseLock(string lockKey, string lockValue)
{
// Use Lua script for atomicity
var script = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
";
var result = await db.ScriptEvaluateAsync(script, new RedisKey[] { lockKey }, new RedisValue[] { lockValue });
return (long)result == 1;
}4. Caching Strategies
4.1 Cache Penetration
Problem: Queries for non-existent data cause every request to hit the database.
Solution:
public async Task<User> GetUserById(int id)
{
var cacheKey = $"user:{id}";
// Get from cache
var cached = await db.StringGetAsync(cacheKey);
if (!cached.IsNull)
{
return JsonSerializer.Deserialize<User>(cached);
}
// Get from database
var user = await _dbContext.Users.FindAsync(id);
if (user != null)
{
// Set cache (with expiration)
await db.StringSetAsync(cacheKey, JsonSerializer.Serialize(user), TimeSpan.FromMinutes(30));
}
else
{
// Cache empty value to prevent penetration
await db.StringSetAsync(cacheKey, string.Empty, TimeSpan.FromMinutes(5));
}
return user;
}4.2 Cache Breakdown
Problem: When a hot key expires, a large number of requests hit the database simultaneously.
Solution: Use a distributed lock
public async Task<User> GetUserByIdWithLock(int id)
{
var cacheKey = $"user:{id}";
var lockKey = $"lock:user:{id}";
var lockValue = Guid.NewGuid().ToString();
// Try to acquire lock
var acquired = await AcquireLock(lockKey, lockValue, TimeSpan.FromSeconds(10));
if (acquired)
{
try
{
// Double-check
var cached = await db.StringGetAsync(cacheKey);
if (!cached.IsNull)
{
return JsonSerializer.Deserialize<User>(cached);
}
// Get from database and update cache
var user = await _dbContext.Users.FindAsync(id);
if (user != null)
{
await db.StringSetAsync(cacheKey, JsonSerializer.Serialize(user), TimeSpan.FromMinutes(30));
}
return user;
}
finally
{
await ReleaseLock(lockKey, lockValue);
}
}
else
{
// Wait for other thread to update cache, then retry
await Task.Delay(100);
return await GetUserByIdWithLock(id);
}
}4.3 Cache Avalanche
Problem: A large number of cache entries expire at the same time, overwhelming the database.
Solution:
- Random expiration times: Add randomness to cache expiration
- Multi-level caching: Use local cache + distributed cache
- Cache warming: Preload hot data on system startup
// Random expiration time
var expiry = TimeSpan.FromMinutes(30 + new Random().Next(0, 30));
await db.StringSetAsync(cacheKey, value, expiry);5. Advanced Features
5.1 Pub/Sub
// Subscriber
var subscriber = connection.GetSubscriber();
await subscriber.SubscribeAsync("channel", (channel, message) =>
{
Console.WriteLine($"收到消息: {message}");
});
// Publisher
await subscriber.PublishAsync("channel", "Hello Redis!");5.2 Pipelining
// Use pipeline for batch operations
var batch = db.CreateBatch();
var tasks = new List<Task>();
for (int i = 0; i < 1000; i++)
{
tasks.Add(batch.StringSetAsync($"key:{i}", $"value:{i}"));
}
batch.Execute();
await Task.WhenAll(tasks);5.3 Lua Scripts
// Use Lua script for atomic operations
var script = @"
local current = redis.call('GET', KEYS[1])
if current then
return redis.call('SET', KEYS[1], tonumber(current) + tonumber(ARGV[1]))
else
return redis.call('SET', KEYS[1], ARGV[1])
end
";
var result = await db.ScriptEvaluateAsync(script, new RedisKey[] { "counter" }, new RedisValue[] { "1" });6. Cluster Deployment
6.1 Master-Replica Replication
# Replica configuration
replicaof master_host master_port
masterauth master_password6.2 Sentinel Mode
# sentinel.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 1800006.3 Cluster Mode
# Create cluster
redis-cli --cluster create \
127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 17. Performance Optimization
7.1 Configuration Optimization
# Disable persistence (cache-only scenario)
save ""
appendonly no
# Increase memory limit
maxmemory 8gb
maxmemory-policy allkeys-lru
# Network optimization
tcp-keepalive 300
tcp-backlog 5117.2 Usage Recommendations
- Batch operations: Use MGET/MSET, pipelining, or Lua scripts to reduce network round trips
- Data structure selection: Choose the right data structure for each scenario
- Connection pooling: Reuse Redis connections instead of creating new ones frequently
- Monitoring: Use Redis CLI or third-party tools to monitor performance
Summary
Redis is a high-performance in-memory database that supports multiple data structures and advanced features. Proper usage can significantly improve system performance and scalability.
Author: Lei Tao
Date: May 1, 2024