一、Redis缓存概述
Redis是一款高性能的内存数据库,广泛应用于缓存场景。通过将热点数据缓存到Redis中,可以显著降低数据库压力,提升系统响应速度。
二、缓存策略
2.1 缓存读写模式
常见的缓存读写模式:
Cache-Aside模式
应用程序直接读写缓存和数据库:
sequenceDiagram
participant App as 应用程序
participant Cache as Redis缓存
participant DB as 数据库
App->>Cache: 查询缓存
alt 缓存命中
Cache-->>App: 返回数据
else 缓存未命中
Cache-->>App: 未命中
App->>DB: 查询数据库
DB-->>App: 返回数据
App->>Cache: 更新缓存
end
Read-Through模式
缓存层负责从数据库加载数据:
public T GetData(string key)
{
var data = _cache.Get(key);
if (data == null)
{
data = _db.Query(key);
_cache.Set(key, data);
}
return data;
}
Write-Through模式
写入时同时更新缓存和数据库:
public void SaveData(string key, object data)
{
_db.Save(key, data);
_cache.Set(key, data);
}
Write-Behind模式
先写缓存,异步写数据库:
public void SaveDataAsync(string key, object data)
{
_cache.Set(key, data);
_queue.Enqueue(() => _db.Save(key, data));
}
2.2 缓存淘汰策略
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| LRU | 淘汰最久未使用 | 命中率高 | 不适合突发访问 |
| LFU | 淘汰最少使用 | 适应访问模式变化 | 需要维护访问频率 |
| TTL | 按过期时间淘汰 | 实现简单 | 可能缓存不一致 |
| Random | 随机淘汰 | 性能高 | 命中率低 |
三、缓存穿透问题
3.1 问题描述
缓存穿透是指查询一个不存在的数据,缓存和数据库都没有,每次请求都打到数据库:
graph TD
A[恶意请求] --> B{缓存查询}
B --> C[缓存未命中]
C --> D[数据库查询]
D --> E[数据库无数据]
E --> F[返回空]
A --> G[大量请求]
G --> D
D --> H[数据库压力大]
3.2 解决方案
方案一:缓存空值
public T GetData(string key)
{
var data = _cache.Get(key);
if (data != null) return data;
data = _db.Query(key);
if (data == null)
{
_cache.Set(key, null, TimeSpan.FromMinutes(5));
return null;
}
_cache.Set(key, data);
return data;
}
方案二:布隆过滤器
public class BloomFilter
{
private readonly BitArray _bits;
private readonly int _hashCount;
public bool MightContain(string key)
{
foreach (var hash in ComputeHashes(key))
{
if (!_bits[hash]) return false;
}
return true;
}
}
public T GetData(string key)
{
if (!_bloomFilter.MightContain(key))
return null;
var data = _cache.Get(key);
if (data == null)
{
data = _db.Query(key);
_cache.Set(key, data);
}
return data;
}
方案三:参数校验
在入口层对参数进行校验,拒绝无效请求。
四、缓存击穿问题
4.1 问题描述
缓存击穿是指热点数据过期时,大量请求同时打到数据库:
graph TD
A[缓存过期] --> B[大量并发请求]
B --> C{缓存查询}
C --> D[缓存未命中]
D --> E[同时查询数据库]
E --> F[数据库压力激增]
4.2 解决方案
方案一:互斥锁
public T GetData(string key)
{
var data = _cache.Get(key);
if (data != null) return data;
lock (_lockObj)
{
data = _cache.Get(key);
if (data != null) return data;
data = _db.Query(key);
_cache.Set(key, data, TimeSpan.FromMinutes(30));
}
return data;
}
方案二:Redis分布式锁
public T GetData(string key)
{
var data = _cache.Get(key);
if (data != null) return data;
var lockKey = $"lock:{key}";
if (_redis.Lock(lockKey, TimeSpan.FromSeconds(5)))
{
try
{
data = _db.Query(key);
_cache.Set(key, data);
}
finally
{
_redis.Unlock(lockKey);
}
}
else
{
Thread.Sleep(100);
return GetData(key);
}
return data;
}
方案三:热点数据永不过期
对于热点数据,设置永不过期,通过后台任务定期更新。
五、缓存雪崩问题
5.1 问题描述
缓存雪崩是指大量缓存同时过期,导致大量请求打到数据库:
graph TD
A[大量缓存同时过期] --> B[大量请求]
B --> C{缓存查询}
C --> D[大量缓存未命中]
D --> E[大量请求打数据库]
E --> F[数据库崩溃]
5.2 解决方案
方案一:随机过期时间
public void SetCache(string key, object data)
{
var baseExpire = TimeSpan.FromMinutes(30);
var randomExpire = TimeSpan.FromSeconds(new Random().Next(300));
_cache.Set(key, data, baseExpire + randomExpire);
}
方案二:多级缓存
使用本地缓存+分布式缓存,减少分布式缓存压力:
graph TD
A[请求] --> B{本地缓存}
B --> C[命中]
B --> D[未命中]
D --> E{Redis缓存}
E --> F[命中]
E --> G[未命中]
G --> H[数据库查询]
H --> I[更新缓存]
方案三:缓存预热
在系统启动时预先加载热点数据到缓存。
六、缓存一致性问题
6.1 问题描述
缓存一致性是指缓存数据与数据库数据不一致的问题。
6.2 解决方案
方案一:先更新数据库,再删除缓存
public void UpdateData(string key, object data)
{
_db.Update(key, data);
_cache.Remove(key);
}
方案二:延迟双删
public void UpdateData(string key, object data)
{
_db.Update(key, data);
_cache.Remove(key);
Thread.Sleep(500);
_cache.Remove(key);
}
方案三:使用消息队列
public void UpdateData(string key, object data)
{
_db.Update(key, data);
_mq.Publish("cache:invalid", key);
}
// 消费端
public void HandleCacheInvalid(string key)
{
_cache.Remove(key);
}
七、Redis性能优化
7.1 数据结构选择
| 数据结构 | 适用场景 | 复杂度 |
|---|---|---|
| String | 简单键值 | O(1) |
| Hash | 对象存储 | O(1) |
| List | 队列 | O(1) |
| Set | 去重 | O(1) |
| ZSet | 排行榜 | O(logN) |
7.2 集群部署
使用Redis集群实现高可用和水平扩展:
graph TD
A[客户端] --> B[Redis集群]
B --> C[节点1 主]
B --> D[节点2 从]
B --> E[节点3 主]
B --> F[节点4 从]
C --> D
E --> F
八、缓存最佳实践
8.1 合理设置过期时间
根据数据变化频率设置合理的过期时间。
8.2 监控缓存命中率
实时监控缓存命中率,调整缓存策略。
8.3 避免缓存大对象
避免在缓存中存储过大的对象,影响性能。
8.4 考虑缓存穿透/击穿/雪崩
在设计阶段就考虑这些问题,提前做好防护。
8.5 使用连接池
使用连接池管理Redis连接,减少连接开销。
九、总结
Redis缓存是提升系统性能的关键技术,但需要合理设计缓存策略并解决常见问题。通过理解缓存穿透、击穿、雪崩等问题的原因和解决方案,能够构建稳定可靠的缓存系统。