.NET Redis 数据结构、分布式锁、缓存问题
<h2>一、Redis 常用 5 个数据结构</h2><div> </div>
<div>StackExchange.Redis 是 .NET 最主流客户端,所有结构都有对应 API。</div>
<div> </div>
<h3>1. String(字符串)</h3>
<div> </div>
<div>用途:缓存对象、计数器、分布式锁、简单配置
<div> </div>
</div>
<div>
<div dir="ltr">
<div>
<pre><code>// 存对象(序列化)
await db.StringSetAsync("user:1", JsonSerializer.Serialize(user));
// 取对象
var user = JsonSerializer.Deserialize<User>(await db.StringGetAsync("user:1"));
// 计数器
await db.StringIncrementAsync("view:count");
</code></pre>
</div>
<div> </div>
</div>
</div>
<h3>2. Hash(哈希)</h3>
<div> </div>
<div>用途:存储对象、用户信息、商品详情
<div> </div>
</div>
<div>
<div dir="ltr">
<div>
<pre><code>await db.HashSetAsync("product:100", new HashEntry[] {
new HashEntry("name", "iPhone"),
new HashEntry("price", "5999")
});
var name = await db.HashGetAsync("product:100", "name");
</code></pre>
</div>
<div> </div>
</div>
</div>
<h3>3. List(列表)</h3>
<div> </div>
<div>用途:消息队列、最新动态、排行榜列表
<div> </div>
命令:LPush、RPop、LRange</div>
<div> </div>
<h3>4. Set(集合)</h3>
<div> </div>
<div>用途:去重、共同关注、抽奖、标签
<div> </div>
命令:SAdd、SRem、SInter(交集)</div>
<div> </div>
<h3>5. SortedSet(有序集合)</h3>
<div> </div>
<div>用途:排行榜、延时任务、权重排序
<div> </div>
命令:ZAdd、ZRangeWithScores</div>
<div> </div>
<hr>
<div> </div>
<h2>二、.NET Redis 分布式锁</h2>
<div> </div>
<div>分布式锁 = 解决多服务 / 多实例同时操作同一资源的问题。</div>
<div> </div>
<h3>核心原则</h3>
<div> </div>
<ol>
<li>互斥:同一时间只有一个线程拿到锁</li>
<li>防死锁:必须设置过期时间</li>
<li>防误删:只能删自己加的锁</li>
<li>原子性:加锁 / 解锁必须原子操作</li>
</ol>
<div> </div>
<hr>
<div> </div>
<h3>.NET 标准分布式锁代码</h3>
<div> </div>
<div>
<div dir="ltr">
<div>
<pre><code>/// <summary>
/// 获取分布式锁
/// </summary>
/// <param name="lockKey">锁key</param>
/// <param name="requestId">唯一请求ID(防止误删)</param>
/// <param name="expireSeconds">过期时间(防死锁)</param>
public async Task<bool> LockAsync(string lockKey, string requestId, int expireSeconds = 10)
{
// NX = 不存在才设置 | EX = 秒级过期
return await _db.StringSetAsync(lockKey, requestId, TimeSpan.FromSeconds(expireSeconds), When.NotExists);
}
/// <summary>
/// 释放分布式锁(Lua 保证原子性)
/// </summary>
public async Task UnlockAsync(string lockKey, string requestId)
{
string luaScript = @"
if redis.call('get', KEYS) == ARGV then
return redis.call('del', KEYS)
else
return 0
end";
await _db.ScriptEvaluateAsync(luaScript, new RedisKey[] { lockKey }, new RedisValue[] { requestId });
}
</code></pre>
</div>
<div> </div>
</div>
</div>
<h3>为什么必须用 Lua?</h3>
<div> </div>
<div>因为判断锁 + 删除锁必须是原子操作,否则会出现:
<div> </div>
线程 A 判断锁是自己 → 锁过期 → 线程 B 加锁 → 线程 A 删除 B 的锁。</div>
<div> </div>
<hr>
<div> </div>
<h2>三、缓存穿透 / 缓存击穿 / 缓存雪崩(.NET 解决方案)</h2>
<div> </div>
<div>这三个是高频面试题 + 高并发必踩坑。</div>
<div> </div>
<hr>
<div> </div>
<h3>1. 缓存穿透(查不存在的数据)</h3>
<div> </div>
<div>现象:查询一条数据库根本没有的数据 → 每次都查库 → 压垮数据库。</div>
<div> </div>
<div>解决方案:</div>
<div> </div>
<ol>
<li>缓存空值</li>
</ol>
<div> </div>
<div>
<div dir="ltr">
<div>
<pre><code>// 查不到数据时缓存空对象,过期时间短一点
await db.StringSetAsync(key, "", TimeSpan.FromMinutes(1));
</code></pre>
</div>
<div> </div>
</div>
</div>
<div> </div>
<ol start="2">
<li>布隆过滤器(Bloom Filter)
<div> </div>
提前把存在的 key 放入布隆过滤器,不存在直接返回。</li>
</ol>
<div> </div>
<hr>
<div> </div>
<h3>2. 缓存击穿(热点 Key 失效)</h3>
<div> </div>
<div>现象:一个极高并发的 Key 过期 → 所有请求瞬间打数据库。</div>
<div> </div>
<div>解决方案:</div>
<div> </div>
<ol>
<li>热点 Key 永不过期</li>
<li>互斥锁(分布式锁)
<div> </div>
只有一个线程去查库,其他线程等待。</li>
</ol>
<div> </div>
<div>.NET 简单击穿防护逻辑:</div>
<div> </div>
<div>
<div dir="ltr">
<div>
<pre><code>var value = await db.StringGetAsync(key);
if (!value.IsNull) return value;
// 加锁
if (await LockAsync(lockKey, requestId))
{
try
{
// 再次检查缓存(双重检查)
value = await db.StringGetAsync(key);
if (!value.IsNull) return value;
// 查询数据库
var data = await _repo.GetData(id);
// 写缓存
await db.StringSetAsync(key, JsonSerializer.Serialize(data), TimeSpan.FromHours(1));
return data;
}
finally
{
await UnlockAsync(lockKey, requestId);
}
}
else
{
// 等待重试
await Task.Delay(100);
return await GetCacheAsync(id);
}
</code></pre>
</div>
<div> </div>
</div>
</div>
<div> </div>
<hr>
<div> </div>
<h3>3. 缓存雪崩(大量 Key 同时过期)</h3>
<div> </div>
<div>现象:大量缓存同一时间集体失效 → 数据库压力暴增。</div>
<div> </div>
<div>解决方案:</div>
<div> </div>
<ol>
<li>给过期时间加随机值</li>
</ol>
<div> </div>
<div>
<div dir="ltr">
<div>
<pre><code>var expire = TimeSpan.FromHours(2) + TimeSpan.FromSeconds(new Random().Next(0, 600));
</code></pre>
</div>
<div> </div>
</div>
</div>
<div> </div>
<ol start="2">
<li>多级缓存(本地缓存 + Redis)</li>
<li>服务熔断 / 降级</li>
<li>Redis 集群高可用(主从 + 哨兵)</li>
</ol>
<div> </div>
<hr>
<div> </div>
<h1>四、三者快速区分</h1>
<div> </div>
<ul>
<li>穿透:查不存在的数据 → 缓存永远不命中</li>
<li>击穿:一个热点 Key 过期 → 并发打库</li>
<li>雪崩:大量 Key 同时过期 → 数据库崩溃</li>
</ul>
<div> </div>
<hr>
<div> </div>
<h1>总结</h1>
<div> </div>
<ol>
<li>5 种数据结构:String/Hash/List/Set/SortedSet,.NET 用 StackExchange.Redis 操作。</li>
<li>分布式锁:必须满足 <code>互斥 + 过期 + 唯一ID + Lua解锁</code>。</li>
<li>缓存三大问题:
<ul>
<li>穿透 → 缓存空值 / 布隆过滤器</li>
<li>击穿 → 永不过期 / 分布式锁</li>
<li>雪崩 → 随机过期 / 集群 / 多级缓存</li>
</ul>
</li>
</ol><br><br>
来源:https://www.cnblogs.com/chuansheng/p/19916001
頁:
[1]