Redis 缓存实战
2024/5/1大约 5 分钟
Redis 缓存实战
概述
Redis 是一个开源的内存数据结构存储系统,可用作数据库、缓存和消息代理。本文将介绍 Redis 的核心功能和实战应用。
1. 安装与配置
1.1 使用 Docker 安装
# 拉取 Redis 镜像
docker pull redis:latest
# 启动容器
docker run -d \
--name redis \
-p 6379:6379 \
-v /path/to/data:/data \
redis:latest1.2 配置文件
# redis.conf
bind 0.0.0.0
port 6379
requirepass yourpassword
maxmemory 1gb
maxmemory-policy allkeys-lru1.3 启动带配置的容器
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. 数据类型
2.1 字符串(String)
# 设置值
SET key value
SET name "Blogger"
# 获取值
GET key
GET name
# 设置过期时间
SET key value EX 3600 # 1小时过期
# 原子操作
INCR counter
DECR counter
INCRBY counter 102.2 哈希(Hash)
# 设置哈希值
HSET user:1 name "Blogger"
HSET user:1 age 30
HSET user:1 email "user@example.com"
# 获取哈希值
HGET user:1 name
HGETALL user:1
# 批量设置
HMSET user:2 name "张三" age 25 email "zhangsan@example.com"
# 批量获取
HMGET user:2 name age2.3 列表(List)
# 从左侧添加
LPUSH tasks "task1"
LPUSH tasks "task2"
# 从右侧添加
RPUSH tasks "task3"
# 获取列表
LRANGE tasks 0 -1
# 弹出元素
LPOP tasks # 从左侧弹出
RPOP tasks # 从右侧弹出2.4 集合(Set)
# 添加元素
SADD tags "java" "csharp" "python"
# 获取所有元素
SMEMBERS tags
# 判断元素是否存在
SISMEMBER tags "java"
# 集合操作
SADD set1 1 2 3
SADD set2 3 4 5
SINTER set1 set2 # 交集
SUNION set1 set2 # 并集
SDIFF set1 set2 # 差集2.5 有序集合(Sorted Set)
# 添加元素(带分数)
ZADD scores 95 "张三"
ZADD scores 88 "李四"
ZADD scores 92 "王五"
# 获取排名(从高到低)
ZREVRANGE scores 0 2 WITHSCORES
# 获取分数范围内的元素
ZRANGEBYSCORE scores 90 100
# 获取元素分数
ZSCORE scores "张三"3. .NET 客户端使用
3.1 安装依赖
dotnet add package StackExchange.Redis3.2 基本操作
using StackExchange.Redis;
// 创建连接
var connection = ConnectionMultiplexer.Connect("localhost:6379,password=yourpassword");
// 获取数据库
var db = connection.GetDatabase();
// 字符串操作
await db.StringSetAsync("name", "Blogger");
var name = await db.StringGetAsync("name");
// 哈希操作
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");
// 列表操作
await db.ListLeftPushAsync("tasks", "task1");
await db.ListRightPushAsync("tasks", "task2");
var tasks = await db.ListRangeAsync("tasks");3.3 分布式锁
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)
{
// 使用 Lua 脚本保证原子性
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. 缓存策略
4.1 缓存穿透
问题:查询不存在的数据,导致每次都访问数据库。
解决方案:
public async Task<User> GetUserById(int id)
{
var cacheKey = $"user:{id}";
// 从缓存获取
var cached = await db.StringGetAsync(cacheKey);
if (!cached.IsNull)
{
return JsonSerializer.Deserialize<User>(cached);
}
// 从数据库获取
var user = await _dbContext.Users.FindAsync(id);
if (user != null)
{
// 设置缓存(带过期时间)
await db.StringSetAsync(cacheKey, JsonSerializer.Serialize(user), TimeSpan.FromMinutes(30));
}
else
{
// 设置空值缓存,防止穿透
await db.StringSetAsync(cacheKey, string.Empty, TimeSpan.FromMinutes(5));
}
return user;
}4.2 缓存击穿
问题:热点数据过期瞬间,大量请求同时访问数据库。
解决方案:使用分布式锁
public async Task<User> GetUserByIdWithLock(int id)
{
var cacheKey = $"user:{id}";
var lockKey = $"lock:user:{id}";
var lockValue = Guid.NewGuid().ToString();
// 尝试获取锁
var acquired = await AcquireLock(lockKey, lockValue, TimeSpan.FromSeconds(10));
if (acquired)
{
try
{
// 双重检查
var cached = await db.StringGetAsync(cacheKey);
if (!cached.IsNull)
{
return JsonSerializer.Deserialize<User>(cached);
}
// 从数据库获取并更新缓存
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
{
// 等待其他线程更新缓存后重试
await Task.Delay(100);
return await GetUserByIdWithLock(id);
}
}4.3 缓存雪崩
问题:大量缓存同时过期,导致数据库压力过大。
解决方案:
- 随机过期时间:给缓存设置随机的过期时间
- 多级缓存:使用本地缓存 + 分布式缓存
- 预热缓存:在系统启动时预加载热点数据
// 随机过期时间
var expiry = TimeSpan.FromMinutes(30 + new Random().Next(0, 30));
await db.StringSetAsync(cacheKey, value, expiry);5. 高级特性
5.1 发布订阅
// 订阅端
var subscriber = connection.GetSubscriber();
await subscriber.SubscribeAsync("channel", (channel, message) =>
{
Console.WriteLine($"收到消息: {message}");
});
// 发布端
await subscriber.PublishAsync("channel", "Hello Redis!");5.2 管道操作
// 使用管道批量操作
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 脚本
// 使用 Lua 脚本实现原子操作
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. 集群部署
6.1 主从复制
# 从节点配置
replicaof master_host master_port
masterauth master_password6.2 哨兵模式
# 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 集群模式
# 创建集群
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. 性能优化
7.1 配置优化
# 禁用持久化(纯缓存场景)
save ""
appendonly no
# 增加内存限制
maxmemory 8gb
maxmemory-policy allkeys-lru
# 优化网络
tcp-keepalive 300
tcp-backlog 5117.2 使用建议
- 批量操作:使用 MGET/MSET、管道或 Lua 脚本减少网络往返
- 数据结构选择:根据场景选择合适的数据结构
- 连接池:复用 Redis 连接,避免频繁创建连接
- 监控:使用 Redis CLI 或第三方工具监控性能
总结
Redis 是一个高性能的内存数据库,支持多种数据结构和高级特性。合理使用可以显著提升系统性能和可扩展性。
作者:Blogger
日期:2024年5月1日