第 1 步:下载安装包
PowerShell 命令行下载
curl.exe -L -o "$env:USERPROFILE\Downloads\Redis-x64-5.0.14.1.zip" "https://ghfast.top/https://github.com/tporadowski/redis/releases/download/v5.0.14.1/Redis-x64-5.0.14.1.zip"
第 2 步:解压到安装目录
D:\Program Files\Redis
第 3 步:为每个节点建独立目录和配置
为什么:每个节点必须有独立的 nodes.conf(集群元数据)、数据文件和日志,混在一起会互相覆盖。目录结构:
D:\Program Files\Redis
├── redis-server.exe、redis-cli.exe # 程序共用
└── cluster
├── 7000\ redis.conf data\ # 每节点一份
├── 7001\ ...
└── 7005\ ...
每节点的 redis.conf 关键配置(以 7000 为例):
port 7000 # 节点端口,6 个节点各不相同
bind 127.0.0.1 # 只监听本机
cluster-enabled yes # ★ 开启集群模式,没有它就是普通单机实例
cluster-config-file nodes.conf # 集群元数据(节点ID、槽位分配、主从关系),自动生成自动维护
cluster-node-timeout 15000 # 节点失联 15 秒触发故障转移
cluster-require-full-coverage no # 部分槽位不可用时集群仍对外服务
appendonly yes # AOF 持久化,重启不丢数据
dir "D:/Program Files/Redis/cluster/7000/data"
logfile "D:/Program Files/Redis/cluster/7000/redis.log"
3.1 在 D:\Program Files\Redis 下创建 cluster
管理员 PowerShell 执行(一行建 6 个节点目录 + data 子目录):
bash
7000..7005 | ForEach-Object { New-Item -ItemType Directory -Force -Path "D:\Program Files\Redis\cluster\$_\data" }
3.2 写配置文件
每节点的配置只有 3 处不同:port、dir、logfile 里的端口号,其余完全一样。
以 7000 为例,新建 D:\Program Files\Redis\cluster\7000\redis.conf,内容:
bash
# 端口(每节点唯一:7000/7001/.../7005)
port 7000
# 只监听本机回环地址,安全
bind 127.0.0.1
protected-mode yes
# ========== 集群核心配置(缺一不可) ==========
# 开启集群模式;没有这行就是普通单机实例
cluster-enabled yes
# 集群元数据文件:节点ID、槽位分配、主从关系。
# 只写文件名(相对路径),Redis 会在 dir 指定目录下自动生成和维护它
cluster-config-file nodes.conf
# 节点失联 15 秒判定为主观下线,触发故障转移
cluster-node-timeout 15000
# 部分槽位不可用时集群仍可服务(默认 yes 会整体拒服务)
cluster-require-full-coverage no
# ========== 持久化 ==========
# AOF:每秒刷盘,重启数据不丢(集群实验环境推荐开)
appendonly yes
appendfilename "appendonly.aof"
dbfilename dump.rdb
databases 16
# ========== 路径(每节点指向自己的目录) ==========
dir "D:/Program Files/Redis/cluster/7000/data"
logfile "D:/Program Files/Redis/cluster/7000/redis.log"
3.3 复制出另外 5 份并改端口号
bash
# 读取 7000 的配置作为模板,替换端口号后生成 7001-7005
$template = Get-Content "D:\Program Files\Redis\cluster\7000\redis.conf" -Raw
7001..7005 | ForEach-Object {
$content = $template -replace '7000', "$_"
Set-Content "D:\Program Files\Redis\cluster\$_\redis.conf" $content
}
3.4 自检(防踩坑的关键一步)
bash
7000..7005 | ForEach-Object {
"=== $_ ==="
Select-String -Path "D:\Program Files\Redis\cluster\$_\redis.conf" -Pattern '^(port|dir|logfile|cluster-enabled)'
}
第 4 步:注册为 Windows 服务
为什么:redis-server.exe 直接跑会随控制台关闭而退出;注册成服务才能开机自启、后台常驻。tporadowski 版内置了服务管理命令(不需要 NSSM):
bash
cd "D:\Program Files\Redis"
.\redis-server.exe --service-install "D:\Program Files\Redis\cluster\7002\redis.conf" --service-name Redis7002
.\redis-server.exe --service-install "D:\Program Files\Redis\cluster\7003\redis.conf" --service-name Redis7003
.\redis-server.exe --service-install "D:\Program Files\Redis\cluster\7004\redis.conf" --service-name Redis7004
.\redis-server.exe --service-install "D:\Program Files\Redis\cluster\7005\redis.conf" --service-name Redis7005
查看服务
bash
Get-Service Redis*
第 5 步:启动全部节点
bash
.\redis-server.exe --service-start --service-name Redis7000
.\redis-server.exe --service-start --service-name Redis7001
.\redis-server.exe --service-start --service-name Redis7002
.\redis-server.exe --service-start --service-name Redis7003
.\redis-server.exe --service-start --service-name Redis7004
.\redis-server.exe --service-start --service-name Redis7005
此时 6 个节点各自独立运行,还不是集群
第 6 步:组建集群(核心一步)
用 redis-cli 的集群管理命令把 6 个节点"编织"成集群:
bash
.\redis-cli.exe --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 1
--cluster create 进入「集群创建」模式
--cluster-replicas 1 每个主节点配 1 个从节点
所以 6 个节点会被自动分成:
- 3 个 master(主节点):7000、7001、7002 → 负责读写,数据分片(16384 个哈希槽平均分给这 3 个主节点)
- 3 个 replica(从节点):7003、7004、7005 → 分别是各主节点的备份,主挂了自动顶上
第 7 步:验证
bash
.\redis-cli.exe -p 7000 cluster info

bash
.\redis-cli.exe -p 7000 cluster nodes
# 能看到 3 个 master 和 3 个 slave 及各自槽位段

bash
.\redis-cli.exe -c -p 7000 set test:key hello
.\redis-cli.exe -c -p 7001 get test:key
# -c 是 cluster 模式:key 不在当前节点时自动跟随 MOVED 重定向
# 能写入后从另一个节点读出 = 分片路由正常

第 8 步(应用接入):配置放 Apollo + c# 接入

安装 StackExchange.Redis
Program.cs 代码
bash
using Com.Ctrip.Framework.Apollo;
using WebApplication1.Redis;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Configuration.AddApollo(
builder.Configuration.GetSection("apollo"));
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
// Redis:连接串从 Apollo 读取(redis.connectionString),此处一次性初始化,全局单例
builder.Services.AddRedis(builder.Configuration);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
IRedisService 代码
bash
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
namespace WebApplication1.Redis;
public interface IRedisService
{
Task<string?> GetAsync(string key);
Task<bool> SetAsync(string key, string value, TimeSpan? expiry = null);
Task<bool> RemoveAsync(string key);
Task<long> IncrementAsync(string key, long delta = 1);
Task<T?> GetObjectAsync<T>(string key);
Task SetObjectAsync<T>(string key, T value, TimeSpan? expiry = null);
Task<bool> ExistsAsync(string key);
Task<bool> KeyExpireAsync(string key, TimeSpan expiry);
Task<bool> LockTakeAsync(string key, string value, TimeSpan expiry);
Task<bool> LockReleaseAsync(string key, string value);
}
public sealed class RedisService : IRedisService
{
private readonly IDatabase _db;
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
public RedisService(IConnectionMultiplexer redis)
{
_db = redis.GetDatabase(); // 轻量包装,不建立新连接
}
public Task<string?> GetAsync(string key) =>
Task.FromResult((string?)_db.StringGet(key));
public Task<bool> SetAsync(string key, string value, TimeSpan? expiry = null) =>
_db.StringSetAsync(key, value, expiry);
public Task<bool> RemoveAsync(string key) =>
_db.KeyDeleteAsync(key);
public Task<long> IncrementAsync(string key, long delta = 1) =>
_db.StringIncrementAsync(key, delta);
public async Task<T?> GetObjectAsync<T>(string key)
{
var json = await _db.StringGetAsync(key);
return json.IsNull ? default : JsonSerializer.Deserialize<T>(json.ToString(), JsonOpts);
}
public Task SetObjectAsync<T>(string key, T value, TimeSpan? expiry = null) =>
_db.StringSetAsync(key, JsonSerializer.Serialize(value, JsonOpts), expiry);
public Task<bool> ExistsAsync(string key) =>
_db.KeyExistsAsync(key);
public Task<bool> KeyExpireAsync(string key, TimeSpan expiry) =>
_db.KeyExpireAsync(key, expiry);
// 分布式锁:用完务必在 finally 中 Release
public Task<bool> LockTakeAsync(string key, string value, TimeSpan expiry) =>
_db.LockTakeAsync(key, value, expiry);
public Task<bool> LockReleaseAsync(string key, string value) =>
_db.LockReleaseAsync(key, value);
}
/// <summary>
/// Redis 一次性初始化:连接串从 Apollo 读取(key: redis.connectionString),
/// ConnectionMultiplexer 与 IRedisService 均为单例,全应用复用,业务侧只注入调用。
/// </summary>
public static class RedisSetup
{
public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration config)
{
var connStr = config["redis.connectionString"];
if (string.IsNullOrWhiteSpace(connStr))
throw new InvalidOperationException(
"未读取到 redis.connectionString,请检查 Apollo(test001 / application 命名空间)是否已发布该配置项");
services.AddSingleton<IConnectionMultiplexer>(_ =>
{
var mux = ConnectionMultiplexer.Connect(connStr);
mux.ConnectionFailed += (_, e) =>
Console.WriteLine($"[Redis] 连接失败 {e.EndPoint} {e.FailureType}");
mux.ConnectionRestored += (_, e) =>
Console.WriteLine($"[Redis] 连接恢复 {e.EndPoint}");
return mux;
});
services.AddSingleton<IRedisService, RedisService>();
return services;
}
}
UserController.cs
bash
// ============================================================
// UserController.cs ------ 业务侧使用示例:构造函数注入 IRedisService 即可,零初始化代码
// ============================================================
using Microsoft.AspNetCore.Mvc;
using WebApplication1.Redis;
namespace WebApplication1.Controllers;
public record UserProfile(string Name, int Age);
[ApiController]
[Route("api/[controller]")]
public class UserController(IRedisService redis) : ControllerBase
{
// GET /api/user/1001 ------ 先查缓存,未命中回源并写回
[HttpGet("{id}")]
public async Task<IActionResult> Get(string id)
{
var cacheKey = $"user:{{{id}}}:profile"; // {id} 为 hash tag,集群下同用户相关 key 同槽位
var cached = await redis.GetObjectAsync<UserProfile>(cacheKey);
if (cached is not null)
{
return Ok(new { from = "cache", data = cached });
}
// 模拟回源(实际是查数据库)
var profile = new UserProfile("张三", 30);
await redis.SetObjectAsync(cacheKey, profile, TimeSpan.FromMinutes(10));
return Ok(new { from = "db", data = profile });
}
// POST /api/user/1001/login ------ 计数器 + 过期
[HttpPost("{id}/login")]
public async Task<IActionResult> Login(string id)
{
var count = await redis.IncrementAsync($"user:{{{id}}}:logins");
await redis.KeyExpireAsync($"user:{{{id}}}:logins", TimeSpan.FromDays(1));
return Ok(new { loginCount = count });
}
// DELETE /api/user/1001 ------ 删除缓存
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(string id)
{
await redis.RemoveAsync($"user:{{{id}}}:profile");
return NoContent();
}
}