Redis Usage

  • ~13.50K 字
  1. 1. 前言
  2. 2. 什么是 Redis?
  3. 3. 安装 Redis
    1. 3.1. Windows 环境
    2. 3.2. Docker 环境(推荐)
  4. 4. Redis 基本数据类型
    1. 4.1. 1. String(字符串)
    2. 4.2. 2. Hash(哈希)
    3. 4.3. 3. List(列表)
    4. 4.4. 4. Set(集合)
    5. 4.5. 5. Sorted Set(有序集合)
  5. 5. 在 .NET 中使用 Redis
    1. 5.1. 安装 NuGet 包
    2. 5.2. 基本连接
    3. 5.3. 配置连接选项
    4. 5.4. 实际应用示例
      1. 5.4.1. 1. 缓存用户信息
      2. 5.4.2. 2. 分布式锁
      3. 5.4.3. 3. 计数器和限流
      4. 5.4.4. 4. 发布/订阅
      5. 5.4.5. 5. 排行榜系统
  6. 6. ASP.NET Core 集成
    1. 6.1. 依赖注入配置
    2. 6.2. appsettings.json
    3. 6.3. 使用 IDistributedCache
  7. 7. Redis 持久化
    1. 7.1. RDB(快照)
    2. 7.2. AOF(追加文件)
  8. 8. 最佳实践
    1. 8.1. 1. 键命名规范
    2. 8.2. 2. 设置过期时间
    3. 8.3. 3. 使用管道(Pipeline)
    4. 8.4. 4. 避免大键
    5. 8.5. 5. 使用连接池
  9. 9. 性能优化
    1. 9.1. 1. 使用异步方法
    2. 9.2. 2. 批量操作
    3. 9.3. 3. 使用 Lua 脚本
  10. 10. 常见问题
    1. 10.1. 1. 缓存雪崩
    2. 10.2. 2. 缓存穿透
    3. 10.3. 3. 缓存击穿
  11. 11. 监控与运维
    1. 11.1. 常用命令
    2. 11.2. .NET 中的监控
  12. 12. 参考资源

前言

作为 .NET 开发人员,Redis 是一个必须掌握的技术。Redis 是一个开源的内存数据结构存储系统,可以用作数据库、缓存和消息代理。

什么是 Redis?

Redis(Remote Dictionary Server)是一个基于内存的键值存储系统,具有以下特点:

  • 高性能:数据存储在内存中,读写速度极快
  • 丰富的数据类型:支持字符串、哈希、列表、集合、有序集合等
  • 持久化:支持数据持久化到磁盘
  • 原子性操作:所有操作都是原子性的
  • 支持事务:支持事务操作
  • 发布订阅:支持消息发布/订阅模式

安装 Redis

Windows 环境

  1. 下载 Redis for Windows:访问 https://github.com/microsoftarchive/redis/releases
  2. 解压到指定目录
  3. 运行 redis-server.exe 启动服务
  4. 运行 redis-cli.exe 打开客户端

Docker 环境(推荐)

1
2
3
4
5
6
7
8
# 拉取 Redis 镜像
docker pull redis:latest

# 运行 Redis 容器
docker run --name my-redis -p 6379:6379 -d redis

# 进入 Redis 命令行
docker exec -it my-redis redis-cli

Redis 基本数据类型

1. String(字符串)

字符串是 Redis 最基本的数据类型,一个键最大能存储 512MB。

1
2
3
4
5
6
7
8
9
10
11
12
# 设置值
SET mykey "Hello Redis"

# 获取值
GET mykey

# 设置带过期时间的值(秒)
SETEX session:user1 3600 "userdata"

# 递增/递减
INCR counter
DECR counter

2. Hash(哈希)

哈希是一个键值对集合,适合存储对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 设置单个字段
HSET user:1000 name "张三"
HSET user:1000 age 30
HSET user:1000 email "zhangsan@example.com"

# 设置多个字段
HMSET user:1001 name "李四" age 25 email "lisi@example.com"

# 获取单个字段
HGET user:1000 name

# 获取所有字段
HGETALL user:1000

# 获取多个字段
HMGET user:1000 name age

3. List(列表)

列表是简单的字符串列表,按插入顺序排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 从左侧插入
LPUSH mylist "world"
LPUSH mylist "hello"

# 从右侧插入
RPUSH mylist "!"

# 获取列表元素
LRANGE mylist 0 -1

# 获取列表长度
LLEN mylist

# 弹出元素
LPOP mylist
RPOP mylist

4. Set(集合)

集合是无序的字符串集合,不允许重复元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 添加元素
SADD myset "apple"
SADD myset "banana"
SADD myset "orange"

# 获取所有元素
SMEMBERS myset

# 判断元素是否存在
SISMEMBER myset "apple"

# 集合运算
SINTER set1 set2 # 交集
SUNION set1 set2 # 并集
SDIFF set1 set2 # 差集

5. Sorted Set(有序集合)

有序集合在集合的基础上增加了一个分数(score)参数,元素按分数排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 添加元素
ZADD leaderboard 100 "player1"
ZADD leaderboard 200 "player2"
ZADD leaderboard 150 "player3"

# 获取排名(从低到高)
ZRANGE leaderboard 0 -1 WITHSCORES

# 获取排名(从高到低)
ZREVRANGE leaderboard 0 -1 WITHSCORES

# 获取分数
ZSCORE leaderboard "player1"

# 增加分数
ZINCRBY leaderboard 50 "player1"

在 .NET 中使用 Redis

安装 NuGet 包

推荐使用 StackExchange.Redis,这是 .NET 最流行的 Redis 客户端库。

1
dotnet add package StackExchange.Redis

基本连接

1
2
3
4
5
6
7
8
9
10
using StackExchange.Redis;

// 创建连接
var redis = ConnectionMultiplexer.Connect("localhost:6379");
var db = redis.GetDatabase();

// 简单的 Set/Get 操作
db.StringSet("mykey", "Hello Redis from .NET");
var value = db.StringGet("mykey");
Console.WriteLine(value); // 输出: Hello Redis from .NET

配置连接选项

1
2
3
4
5
6
7
8
9
10
11
var configOptions = new ConfigurationOptions
{
EndPoints = { "localhost:6379" },
Password = "your_password", // 如果有密码
ConnectTimeout = 5000,
SyncTimeout = 5000,
AbortOnConnectFail = false,
DefaultDatabase = 0
};

var redis = ConnectionMultiplexer.Connect(configOptions);

实际应用示例

1. 缓存用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class UserService
{
private readonly IDatabase _redis;

public UserService(IConnectionMultiplexer redis)
{
_redis = redis.GetDatabase();
}

public async Task<User> GetUserAsync(int userId)
{
var cacheKey = $"user:{userId}";

// 尝试从缓存获取
var cachedUser = await _redis.StringGetAsync(cacheKey);
if (cachedUser.HasValue)
{
return JsonSerializer.Deserialize<User>(cachedUser);
}

// 从数据库获取
var user = await _dbContext.Users.FindAsync(userId);

// 存入缓存(1小时过期)
if (user != null)
{
var serialized = JsonSerializer.Serialize(user);
await _redis.StringSetAsync(cacheKey, serialized, TimeSpan.FromHours(1));
}

return user;
}
}

2. 分布式锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class DistributedLockService
{
private readonly IDatabase _redis;

public async Task<bool> AcquireLockAsync(string resource, string token, TimeSpan expiry)
{
var key = $"lock:{resource}";
return await _redis.StringSetAsync(key, token, expiry, When.NotExists);
}

public async Task<bool> ReleaseLockAsync(string resource, string token)
{
var key = $"lock:{resource}";
var currentValue = await _redis.StringGetAsync(key);

if (currentValue == token)
{
return await _redis.KeyDeleteAsync(key);
}

return false;
}

// 使用示例
public async Task ProcessWithLockAsync()
{
var token = Guid.NewGuid().ToString();
var lockAcquired = await AcquireLockAsync("myresource", token, TimeSpan.FromSeconds(30));

if (lockAcquired)
{
try
{
// 执行需要加锁的操作
await DoSomethingAsync();
}
finally
{
await ReleaseLockAsync("myresource", token);
}
}
}
}

3. 计数器和限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class RateLimiter
{
private readonly IDatabase _redis;

public async Task<bool> IsAllowedAsync(string userId, int maxRequests, TimeSpan window)
{
var key = $"ratelimit:{userId}";
var count = await _redis.StringIncrementAsync(key);

if (count == 1)
{
await _redis.KeyExpireAsync(key, window);
}

return count <= maxRequests;
}
}

// 使用示例
var rateLimiter = new RateLimiter(redis);
if (await rateLimiter.IsAllowedAsync("user123", 100, TimeSpan.FromMinutes(1)))
{
// 处理请求
}
else
{
// 请求过于频繁,返回 429
}

4. 发布/订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 订阅者
public class MessageSubscriber
{
public void Subscribe(IConnectionMultiplexer redis)
{
var subscriber = redis.GetSubscriber();

subscriber.Subscribe("notifications", (channel, message) =>
{
Console.WriteLine($"收到消息: {message}");
});
}
}

// 发布者
public class MessagePublisher
{
private readonly IConnectionMultiplexer _redis;

public async Task PublishAsync(string message)
{
var subscriber = _redis.GetSubscriber();
await subscriber.PublishAsync("notifications", message);
}
}

5. 排行榜系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class LeaderboardService
{
private readonly IDatabase _redis;
private const string LeaderboardKey = "game:leaderboard";

public async Task UpdateScoreAsync(string playerId, double score)
{
await _redis.SortedSetAddAsync(LeaderboardKey, playerId, score);
}

public async Task<List<(string PlayerId, double Score)>> GetTopPlayersAsync(int count)
{
var entries = await _redis.SortedSetRangeByRankWithScoresAsync(
LeaderboardKey,
0,
count - 1,
Order.Descending
);

return entries.Select(e => (e.Element.ToString(), e.Score)).ToList();
}

public async Task<long?> GetPlayerRankAsync(string playerId)
{
var rank = await _redis.SortedSetRankAsync(LeaderboardKey, playerId, Order.Descending);
return rank.HasValue ? rank.Value + 1 : null;
}
}

ASP.NET Core 集成

依赖注入配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Program.cs 或 Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// 配置 Redis
services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var configuration = ConfigurationOptions.Parse(
Configuration.GetConnectionString("Redis")
);
return ConnectionMultiplexer.Connect(configuration);
});

// 注册服务
services.AddScoped<IUserService, UserService>();
services.AddSingleton<IDistributedLockService, DistributedLockService>();
}

appsettings.json

1
2
3
4
5
{
"ConnectionStrings": {
"Redis": "localhost:6379,password=your_password,ssl=false,abortConnect=false"
}
}

使用 IDistributedCache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 配置
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp_";
});

// 使用
public class CachedDataService
{
private readonly IDistributedCache _cache;

public async Task<string> GetDataAsync(string key)
{
var cachedData = await _cache.GetStringAsync(key);
if (cachedData != null)
{
return cachedData;
}

var data = await FetchDataFromSourceAsync();

var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
};

await _cache.SetStringAsync(key, data, options);
return data;
}
}

Redis 持久化

RDB(快照)

  • 在指定时间间隔内生成数据集的时间点快照
  • 适合备份和灾难恢复
  • 配置示例:
1
2
3
4
# redis.conf
save 900 1 # 900秒内至少1个键被修改
save 300 10 # 300秒内至少10个键被修改
save 60 10000 # 60秒内至少10000个键被修改

AOF(追加文件)

  • 记录每个写操作
  • 更好的数据持久性
  • 配置示例:
1
2
3
# redis.conf
appendonly yes
appendfsync everysec # 每秒同步一次

最佳实践

1. 键命名规范

1
2
3
4
5
6
// 使用冒号分隔的命名空间
"user:1000:profile"
"product:5678:stock"
"session:abc123def"

// 避免过长的键名,建议不超过1024字节

2. 设置过期时间

1
2
// 始终为缓存数据设置过期时间,避免内存泄漏
await db.StringSetAsync("key", "value", TimeSpan.FromHours(1));

3. 使用管道(Pipeline)

1
2
3
4
5
6
7
8
9
10
11
// 批量操作使用管道,减少网络往返
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);

4. 避免大键

不要在单个键中存储过大的数据(建议不超过10KB)

考虑拆分大对象或使用哈希结构

5. 使用连接池

1
2
// StackExchange.Redis 内置连接池,使用单例模式
services.AddSingleton<IConnectionMultiplexer>(...);

性能优化

1. 使用异步方法

1
2
3
// 优先使用异步方法
await db.StringGetAsync("key");
await db.HashSetAsync("hash", "field", "value");

2. 批量操作

1
2
3
4
5
6
7
8
// 使用批量操作代替循环单个操作
var hashEntries = new HashEntry[]
{
new HashEntry("field1", "value1"),
new HashEntry("field2", "value2"),
new HashEntry("field3", "value3")
};
await db.HashSetAsync("myhash", hashEntries);

3. 使用 Lua 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 原子性执行复杂操作
var script = @"
local current = redis.call('GET', KEYS[1])
if current == false or tonumber(current) < tonumber(ARGV[1]) then
redis.call('SET', KEYS[1], ARGV[1])
return 1
end
return 0
";

var result = await db.ScriptEvaluateAsync(
script,
new RedisKey[] { "mykey" },
new RedisValue[] { 100 }
);

常见问题

1. 缓存雪崩

大量缓存同时过期,导致请求全部打到数据库。

解决方案

  • 设置随机过期时间
  • 使用热点数据永不过期
  • 使用互斥锁
1
2
var expiry = TimeSpan.FromMinutes(30 + Random.Shared.Next(0, 10));
await db.StringSetAsync(key, value, expiry);

2. 缓存穿透

查询不存在的数据,缓存和数据库都没有。

解决方案

  • 缓存空值
  • 使用布隆过滤器
1
2
3
4
5
6
var user = await GetUserFromDbAsync(userId);
if (user == null)
{
// 缓存空值,设置较短过期时间
await db.StringSetAsync(cacheKey, "null", TimeSpan.FromMinutes(5));
}

3. 缓存击穿

热点数据过期瞬间,大量请求同时访问。

解决方案

  • 使用分布式锁
  • 热点数据永不过期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public async Task<User> GetUserWithLockAsync(int userId)
{
var cacheKey = $"user:{userId}";
var cached = await db.StringGetAsync(cacheKey);

if (cached.HasValue)
return JsonSerializer.Deserialize<User>(cached);

var lockKey = $"lock:{cacheKey}";
var lockToken = Guid.NewGuid().ToString();

if (await db.StringSetAsync(lockKey, lockToken, TimeSpan.FromSeconds(10), When.NotExists))
{
try
{
var user = await GetUserFromDbAsync(userId);
if (user != null)
{
await db.StringSetAsync(cacheKey, JsonSerializer.Serialize(user), TimeSpan.FromHours(1));
}
return user;
}
finally
{
await db.KeyDeleteAsync(lockKey);
}
}

// 等待并重试
await Task.Delay(50);
return await GetUserWithLockAsync(userId);
}

监控与运维

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看 Redis 信息
INFO

# 查看内存使用
INFO memory

# 查看连接客户端
CLIENT LIST

# 慢查询日志
SLOWLOG GET 10

# 监控所有命令
MONITOR

# 查看键数量
DBSIZE

# 删除所有数据(慎用)
FLUSHALL

.NET 中的监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RedisMonitor
{
private readonly IConnectionMultiplexer _redis;

public async Task<RedisInfo> GetInfoAsync()
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
var info = await server.InfoAsync();

return new RedisInfo
{
UsedMemory = info.First(x => x.Key == "used_memory").Value,
ConnectedClients = info.First(x => x.Key == "connected_clients").Value,
TotalCommandsProcessed = info.First(x => x.Key == "total_commands_processed").Value
};
}
}

参考资源