分布式缓存
缓存是提升系统性能的重要手段。通过将频繁访问的数据存储在高速存储介质中,缓存可以大幅减少数据访问延迟,降低后端存储的压力。在分布式系统中,缓存的作用更加凸显——单机缓存无法满足多实例共享的需求,分布式缓存成为必然选择。
为什么需要缓存
性能瓶颈的本质
大多数应用的性能瓶颈在于数据访问,尤其是数据库访问。数据库访问慢的原因是多方面的:
磁盘 I/O:传统数据库将数据存储在磁盘上,即使是 SSD,其随机读写速度也比内存慢几个数量级。一次磁盘随机读取可能需要 10ms,而内存读取只需要 100ns。
复杂查询:涉及多表关联、聚合计算的查询需要大量 CPU 时间。即使数据量不大,复杂的执行计划也可能导致查询缓慢。
锁竞争:高并发场景下,数据库锁成为瓶颈。写操作会阻塞读操作,事务之间相互等待,吞吐量下降。
网络延迟:应用服务器与数据库服务器之间的网络往返增加了请求延迟。
缓存通过将热点数据存储在内存中,避免了上述大部分问题。内存的访问速度比磁盘快十万倍以上,合理的缓存策略可以将请求延迟从几百毫秒降低到几毫秒。
缓存的收益与代价
收益:
- 降低延迟:内存访问速度远快于磁盘,用户请求得到快速响应
- 减轻数据库压力:大量请求被缓存拦截,数据库只需要处理少量请求
- 提高吞吐量:缓存可以支持更高的并发,系统整体吞吐量提升
- 降低成本:减少数据库资源消耗,可能使用更小的数据库实例
代价:
- 数据一致性问题:缓存与数据库之间存在数据不一致窗口
- 缓存维护成本:需要额外的服务器资源,增加运维复杂度
- 缓存穿透/击穿/雪崩:缓存使用不当可能导致严重问题
- 代码复杂度增加:需要处理缓存读取、更新、失效等逻辑
缓存的基本模式
Cache-Aside(旁路缓存)
Cache-Aside 是最常用的缓存模式,应用程序同时维护缓存和数据库。读操作先查缓存,缓存未命中则查数据库并回填缓存;写操作先更新数据库,然后删除缓存。
// Cache-Aside 模式示例
public class CacheAsidePattern {
private final Cache cache;
private final Database database;
/**
* 读操作流程:
* 1. 先查缓存
* 2. 缓存未命中,查数据库
* 3. 将数据库结果写入缓存
* 4. 返回结果
*/
public User getUser(String userId) {
// 1. 查缓存
User user = cache.get("user:" + userId);
if (user != null) {
return user; // 缓存命中
}
// 2. 缓存未命中,查数据库
user = database.queryUser(userId);
if (user != null) {
// 3. 回填缓存
cache.set("user:" + userId, user, 3600); // 过期时间 1 小时
}
return user;
}
/**
* 写操作流程:
* 1. 先更新数据库
* 2. 再删除缓存(而不是更新缓存)
*/
public void updateUser(User user) {
// 1. 更新数据库
database.updateUser(user);
// 2. 删除缓存(让下次读取时重新加载)
cache.delete("user:" + user.getId());
}
}
为什么删除缓存而不是更新缓存?
删除缓存比更新缓存更安全,原因如下:
-
并发问题:如果两个线程同时更新同一数据,更新缓存可能导致旧值覆盖新值。删除缓存则不会有这个问题,因为后续读取会重新加载最新值。
-
懒加载:很多缓存数据可能永远不会被再次访问,提前更新是浪费。删除后,只有在真正需要时才会加载。
-
复杂计算:如果缓存值需要复杂计算,每次写操作都重新计算是浪费。删除后只在读时计算。
Read-Through(读穿透)
Read-Through 模式将缓存读取逻辑封装在缓存层,应用程序只与缓存交互。当缓存未命中时,缓存层自动从数据库加载数据。
// Read-Through 模式示例
public class ReadThroughCache {
private final Cache cache;
private final Database database;
public User getUser(String userId) {
// 应用层只调用缓存
// 缓存层负责处理数据库查询
return cache.get("user:" + userId, () -> {
// 缓存未命中时的加载逻辑
return database.queryUser(userId);
});
}
}
// 缓存层实现
public class Cache {
private final RedisClient redis;
public <T> T get(String key, Supplier<T> loader) {
// 1. 查 Redis
String value = redis.get(key);
if (value != null) {
return deserialize(value);
}
// 2. 缓存未命中,调用加载器
T data = loader.get();
if (data != null) {
// 3. 写入缓存
redis.setex(key, 3600, serialize(data));
}
return data;
}
}
Read-Through 的优势在于应用层代码更简洁,缓存逻辑集中管理。缺点是需要缓存层支持自定义加载逻辑。
Write-Through(写穿透)
Write-Through 模式将缓存写入和数据库写入合并为一个操作。应用程序更新缓存,缓存层同步更新数据库。
// Write-Through 模式示例
public class WriteThroughCache {
private final Cache cache;
public void updateUser(User user) {
// 应用层只更新缓存
// 缓存层负责同步更新数据库
cache.put("user:" + user.getId(), user);
}
}
// 缓存层实现
public class Cache {
private final RedisClient redis;
private final Database database;
public void put(String key, Object value) {
// 1. 同步写入数据库
database.update(value);
// 2. 写入缓存
redis.set(key, serialize(value));
}
}
Write-Through 保证了缓存与数据库的强一致性,但每次写操作都需要等待数据库更新完成,性能有所下降。
Write-Behind(写回)
Write-Behind 模式先更新缓存,异步批量更新数据库。这种方式可以显著提高写性能,但存在数据丢失风险。
// Write-Behind 模式示例
public class WriteBehindCache {
private final Cache cache;
private final WriteQueue writeQueue;
public void updateUser(User user) {
// 1. 立即更新缓存
cache.put("user:" + user.getId(), user);
// 2. 加入写队列,异步更新数据库
writeQueue.add(user);
}
}
// 异步写入器
public class AsyncWriter {
private final BlockingQueue<Object> writeQueue = new LinkedBlockingQueue<>();
private final Database database;
public void start() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleWithFixedDelay(() -> {
List<Object> batch = new ArrayList<>();
writeQueue.drainTo(batch, 100); // 批量取出最多 100 条
if (!batch.isEmpty()) {
// 批量写入数据库
database.batchUpdate(batch);
}
}, 0, 1, TimeUnit.SECONDS); // 每秒执行一次
}
}
Write-Behind 的优势:
- 写操作延迟极低(只写内存)
- 可以批量写入数据库,提高吞吐量
Write-Behind 的风险:
- 系统崩溃时可能丢失未持久化的数据
- 数据一致性问题(缓存和数据库可能不一致)
模式对比
| 模式 | 读流程 | 写流程 | 一致性 | 性能 | 复杂度 |
|---|---|---|---|---|---|
| Cache-Aside | 先缓存后数据库 | 先数据库后删缓存 | 最终一致 | 高 | 低 |
| Read-Through | 缓存层封装 | 直接写数据库 | 最终一致 | 高 | 中 |
| Write-Through | 缓存层封装 | 缓存层同步写数据库 | 强一致 | 中 | 中 |
| Write-Behind | 缓存层封装 | 缓存层异步写数据库 | 弱一致 | 最高 | 高 |
分布式缓存的挑战
缓存穿透
缓存穿透是指查询一个根本不存在的数据,由于缓存没有命中,每次都会查询数据库。如果有人恶意大量请求不存在的数据,数据库可能被压垮。
解决方案:
空值缓存:将查询结果为空的 key 也缓存起来,设置较短的过期时间。
public User getUser(String userId) {
User user = cache.get("user:" + userId);
if (user != null) {
return user;
}
// 检查是否是空值缓存
if (cache.get("null:user:" + userId) != null) {
return null; // 之前查询过,确定不存在
}
user = database.queryUser(userId);
if (user != null) {
cache.set("user:" + userId, user, 3600);
} else {
// 空值缓存,过期时间较短
cache.set("null:user:" + userId, "", 60);
}
return user;
}
布隆过滤器:在查询缓存前,先用布隆过滤器判断 key 是否可能存在。布隆过滤器可以快速判断一个元素"一定不存在"或"可能存在"。
public class BloomFilterProtection {
private final BloomFilter<String> bloomFilter;
private final Cache cache;
private final Database database;
public User getUser(String userId) {
// 1. 先用布隆过滤器判断
if (!bloomFilter.mightContain(userId)) {
return null; // 一定不存在
}
// 2. 正常的缓存查询流程
User user = cache.get("user:" + userId);
if (user != null) {
return user;
}
user = database.queryUser(userId);
if (user != null) {
cache.set("user:" + userId, user, 3600);
}
return user;
}
// 数据写入时更新布隆过滤器
public void addUser(User user) {
database.insert(user);
bloomFilter.put(user.getId());
}
}
缓存击穿
缓存击穿是指某个热点 key 过期的瞬间,大量请求同时查询这个 key,都发现缓存失效,然后同时去查询数据库,导致数据库压力骤增。
解决方案:
互斥锁:只允许一个线程去查询数据库,其他线程等待。
public class CacheBreakdownProtection {
private final Cache cache;
private final Database database;
private final DistributedLock lock;
public User getUser(String userId) {
User user = cache.get("user:" + userId);
if (user != null) {
return user;
}
// 获取分布式锁
String lockKey = "lock:user:" + userId;
try {
if (lock.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
// 获取锁成功,再次检查缓存(双重检查)
user = cache.get("user:" + userId);
if (user != null) {
return user;
}
// 查询数据库
user = database.queryUser(userId);
if (user != null) {
cache.set("user:" + userId, user, 3600);
}
return user;
} else {
// 获取锁失败,等待后重试
Thread.sleep(100);
return getUser(userId);
}
} finally {
lock.unlock(lockKey);
}
}
}
热点数据永不过期:对于真正的热点数据,可以不设置过期时间,通过后台任务定期更新。
public class HotDataProtection {
private final Cache cache;
private final Database database;
// 后台任务定时刷新热点数据
@Scheduled(fixedRate = 300000) // 每 5 分钟刷新
public void refreshHotData() {
List<String> hotKeys = getHotKeys();
for (String key : hotKeys) {
String userId = key.replace("user:", "");
User user = database.queryUser(userId);
if (user != null) {
// 热点数据不设置过期时间
cache.set(key, user, -1);
}
}
}
}
缓存雪崩
缓存雪崩是指大量缓存 key 在同一时间集中过期,或者缓存服务器宕机,导致所有请求都打到数据库上。
解决方案:
过期时间加随机值:避免大量 key 同时过期。
public void setWithRandomExpire(String key, Object value, int baseExpire) {
// 在基础过期时间上增加随机值
Random random = new Random();
int randomExpire = random.nextInt(baseExpire / 10); // 0 ~ 10% 的随机增量
cache.set(key, value, baseExpire + randomExpire);
}
缓存高可用:使用 Redis Sentinel 或 Redis Cluster 保证缓存服务的高可用。
熔断降级:当缓存不可用时,触发熔断,返回降级数据或错误页面,保护数据库。
public class CacheAvalancheProtection {
private final CircuitBreaker circuitBreaker;
private final Cache cache;
private final Database database;
public User getUser(String userId) {
// 熔断器保护
return circuitBreaker.executeSupplier(() -> {
User user = cache.get("user:" + userId);
if (user != null) {
return user;
}
user = database.queryUser(userId);
if (user != null) {
cache.set("user:" + userId, user, 3600);
}
return user;
}, () -> {
// 降级逻辑:返回默认数据或从本地缓存读取
return getDefaultUser();
});
}
}
缓存与数据库一致性
缓存与数据库之间的一致性是分布式缓存最棘手的问题之一。由于缓存和数据库是两个独立的存储系统,无法保证原子性的同时更新。
常见方案:
先删缓存,再更新数据库:这种方式有问题。如果线程 A 删除缓存后,线程 B 读取数据,此时数据库还没有更新,线程 B 会将旧数据写入缓存。
先更新数据库,再删缓存:这是推荐的方式。即使删除缓存失败,缓存中的数据也只是暂时的不一致,可以通过延迟双删或消息队列重试来修复。
// 先更新数据库,再删缓存
public void updateUser(User user) {
// 1. 更新数据库
database.updateUser(user);
// 2. 删除缓存
cache.delete("user:" + user.getId());
}
延迟双删:为了处理并发场景下的问题,可以在删除缓存后延迟一段时间再删除一次。
public void updateUser(User user) {
// 1. 删除缓存
cache.delete("user:" + user.getId());
// 2. 更新数据库
database.updateUser(user);
// 3. 延迟删除缓存(处理并发读取写入旧数据的情况)
executor.schedule(() -> {
cache.delete("user:" + user.getId());
}, 500, TimeUnit.MILLISECONDS); // 延迟 500ms
}
消息队列保证最终一致性:将删除缓存的操作发送到消息队列,保证最终执行成功。
public void updateUser(User user) {
// 1. 更新数据库
database.updateUser(user);
// 2. 发送删除缓存消息
mq.send("cache-delete", "user:" + user.getId());
}
// 消费者
public void onMessage(String key) {
cache.delete(key);
// 失败时重试,或者将消息保留在队列中
}
Redis 分布式缓存
Redis 是目前最流行的分布式缓存解决方案,它是一个开源的内存数据结构存储系统,支持多种数据结构,性能极高。
Redis 数据结构
Redis 支持五种基本数据结构,每种都有其适用场景。
String(字符串):最基本的数据类型,可以存储字符串、整数、浮点数。
// String 操作示例
public class RedisStringOperations {
private final RedisTemplate<String, String> redisTemplate;
// 设置值
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
}
// 设置值(如果不存在)
public Boolean setIfAbsent(String key, String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value, 3600, TimeUnit.SECONDS);
}
// 获取值
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
// 原子递增
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key);
}
// 原子递增指定值
public Long increment(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
}
Hash(哈希):适合存储对象,可以单独操作对象的某个字段。
// Hash 操作示例
public class RedisHashOperations {
private final RedisTemplate<String, String> redisTemplate;
// 存储用户对象
public void saveUser(User user) {
String key = "user:" + user.getId();
Map<String, String> userMap = new HashMap<>();
userMap.put("name", user.getName());
userMap.put("email", user.getEmail());
userMap.put("age", String.valueOf(user.getAge()));
redisTemplate.opsForHash().putAll(key, userMap);
}
// 获取单个字段
public String getUserField(String userId, String field) {
return (String) redisTemplate.opsForHash().get("user:" + userId, field);
}
// 获取所有字段
public Map<Object, Object> getUser(String userId) {
return redisTemplate.opsForHash().entries("user:" + userId);
}
// 更新单个字段
public void updateUserField(String userId, String field, String value) {
redisTemplate.opsForHash().put("user:" + userId, field, value);
}
// 原子递增某个字段
public Long incrementField(String userId, String field, long delta) {
return redisTemplate.opsForHash().increment("user:" + userId, field, delta);
}
}
List(列表):有序可重复列表,适合存储消息队列、最新列表等。
// List 操作示例
public class RedisListOperations {
private final RedisTemplate<String, String> redisTemplate;
// 左推入(最新消息在前)
public Long lpush(String key, String value) {
return redisTemplate.opsForList().leftPush(key, value);
}
// 右推入(消息队列)
public Long rpush(String key, String value) {
return redisTemplate.opsForList().rightPush(key, value);
}
// 左弹出
public String lpop(String key) {
return redisTemplate.opsForList().leftPop(key);
}
// 右弹出
public String rpop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
// 阻塞弹出(消息队列消费者)
public String brpop(String key, long timeout) {
return redisTemplate.opsForList().rightPop(key, timeout, TimeUnit.SECONDS);
}
// 获取范围内的元素
public List<String> lrange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
}
Set(集合):无序不重复集合,适合存储标签、关注关系等。
// Set 操作示例
public class RedisSetOperations {
private final RedisTemplate<String, String> redisTemplate;
// 添加元素
public Long sadd(String key, String... values) {
return redisTemplate.opsForSet().add(key, values);
}
// 获取所有元素
public Set<String> smembers(String key) {
return redisTemplate.opsForSet().members(key);
}
// 判断元素是否存在
public Boolean sismember(String key, String value) {
return redisTemplate.opsForSet().isMember(key, value);
}
// 集合交集(共同关注)
public Set<String> sinter(String key1, String key2) {
return redisTemplate.opsForSet().intersect(key1, key2);
}
// 集合并集
public Set<String> sunion(String key1, String key2) {
return redisTemplate.opsForSet().union(key1, key2);
}
}
Sorted Set(有序集合):有序不重复集合,每个元素关联一个分数,按分数排序。适合排行榜、带权重的集合等。
// Sorted Set 操作示例
public class RedisSortedSetOperations {
private final RedisTemplate<String, String> redisTemplate;
// 添加元素(带分数)
public Boolean zadd(String key, String value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
// 获取排行榜(分数从高到低)
public Set<String> zrevrange(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}
// 获取排行榜(带分数)
public Set<ZSetOperations.TypedTuple<String>> zrevrangeWithScores(
String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}
// 获取元素排名
public Long zrevrank(String key, String value) {
return redisTemplate.opsForZSet().reverseRank(key, value);
}
// 获取元素分数
public Double zscore(String key, String value) {
return redisTemplate.opsForZSet().score(key, value);
}
// 增加元素分数
public Double zincrby(String key, String value, double delta) {
return redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
}
Redis 持久化
Redis 提供两种持久化方式:RDB 和 AOF。
RDB(快照):定期将内存中的数据生成快照保存到磁盘。
# redis.conf 配置
save 900 1 # 900秒内有1个写操作就保存
save 300 10 # 300秒内有10个写操作就保存
save 60 10000 # 60秒内有10000个写操作就保存
dbfilename dump.rdb
dir /var/lib/redis
RDB 的优点:
- 文件紧凑,适合备份和传输
- 恢复速度快,直接加载快照
- 对性能影响小,fork 子进程处理
RDB 的缺点:
- 无法做到实时持久化,可能丢失数据
- fork 大数据集时可能阻塞
AOF(日志):记录每个写操作命令,恢复时重新执行。
# redis.conf 配置
appendonly yes
appendfilename "appendonly.aof"
# 同步策略
appendfsync always # 每个写操作都同步,最安全但最慢
appendfsync everysec # 每秒同步一次,推荐
appendfsync no # 由操作系统决定,最快但可能丢数据
# AOF 重写
auto-aof-rewrite-percentage 100 # 文件大小翻倍时触发重写
auto-aof-rewrite-min-size 64mb # 最小重写大小
AOF 的优点:
- 数据更安全,最多丢失 1 秒数据
- 可读性好,可以分析和修改
AOF 的缺点:
- 文件体积大
- 恢复速度慢
- 写入性能有一定影响
混合持久化:Redis 4.0 引入,RDB 作为基础,AOF 记录增量。
# redis.conf 配置
aof-use-rdb-preamble yes
Redis 集群
当单机 Redis 无法满足容量或性能需求时,需要使用 Redis 集群。
主从复制:最简单的高可用方案。主节点处理写请求,从节点复制数据并处理读请求。
# 从节点配置
replicaof 192.168.1.100 6379
replica-read-only yes
Redis Sentinel:监控主从节点,自动故障转移。
# sentinel.conf 配置
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
// Sentinel 客户端配置
public class RedisSentinelConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration config = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("192.168.1.101", 26379)
.sentinel("192.168.1.102", 26379)
.sentinel("192.168.1.103", 26379);
return new LettuceConnectionFactory(config);
}
}
Redis Cluster:官方分布式方案,数据自动分片到多个节点。
Redis Cluster 的特点:
- 16384 个槽位分配到多个主节点
- 客户端可以直接连接任意节点
- 支持在线扩缩容
// Redis Cluster 客户端配置
public class RedisClusterConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration config = new RedisClusterConfiguration(
Arrays.asList(
"192.168.1.101:6379",
"192.168.1.102:6379",
"192.168.1.103:6379"
)
);
return new LettuceConnectionFactory(config);
}
}
本地缓存与分布式缓存结合
分布式缓存虽然功能强大,但每次访问都有网络开销。将本地缓存与分布式缓存结合,可以进一步提升性能。
多级缓存架构
// 多级缓存实现
public class MultiLevelCache {
private final Cache localCache; // 本地缓存(如 Caffeine)
private final Cache redisCache; // 分布式缓存(Redis)
private final Database database;
public User getUser(String userId) {
// 1. 先查本地缓存
User user = localCache.get("user:" + userId);
if (user != null) {
return user;
}
// 2. 查分布式缓存
user = redisCache.get("user:" + userId);
if (user != null) {
// 回填本地缓存
localCache.put("user:" + userId, user);
return user;
}
// 3. 查数据库
user = database.queryUser(userId);
if (user != null) {
// 同时写入本地缓存和分布式缓存
localCache.put("user:" + userId, user);
redisCache.put("user:" + userId, user, 3600);
}
return user;
}
public void updateUser(User user) {
// 更新数据库
database.updateUser(user);
// 删除分布式缓存
redisCache.delete("user:" + user.getId());
// 删除本地缓存
localCache.delete("user:" + user.getId());
}
}
本地缓存一致性
多级缓存面临的一致性挑战更大:本地缓存分布在多个应用实例上,如何保证它们的一致性?
消息广播:当一个实例更新数据时,广播消息通知所有实例删除本地缓存。
// 基于消息的本地缓存失效
public class CacheInvalidationListener {
private final Cache localCache;
private final MessageQueue mq;
// 更新数据时发送失效消息
public void invalidate(String key) {
localCache.delete(key);
// 广播到所有实例
mq.publish("cache-invalidation", key);
}
// 接收失效消息
@Subscribe("cache-invalidation")
public void onInvalidation(String key) {
localCache.delete(key);
}
}
短过期时间:本地缓存设置较短的过期时间,即使暂时不一致也能快速恢复。
// 本地缓存使用短过期时间
public class LocalCacheConfig {
public Cache buildLocalCache() {
return Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS) // 30 秒过期
.maximumSize(10000)
.build();
}
}
缓存最佳实践
合理设置过期时间
过期时间的设置需要权衡:
- 过短:缓存命中率低,频繁访问数据库
- 过长:数据更新后不一致时间长
// 根据数据特点设置过期时间
public class CacheExpirationStrategy {
// 热点数据:较长过期时间
public void cacheHotData(String key, Object value) {
cache.set(key, value, 3600); // 1 小时
}
// 普通数据:中等过期时间
public void cacheNormalData(String key, Object value) {
cache.set(key, value, 300); // 5 分钟
}
// 变化频繁的数据:较短过期时间
public void cacheVolatileData(String key, Object value) {
cache.set(key, value, 60); // 1 分钟
}
// 加随机值避免雪崩
public void cacheWithRandomExpire(String key, Object value, int baseExpire) {
Random random = new Random();
int randomExpire = baseExpire + random.nextInt(baseExpire / 5);
cache.set(key, value, randomExpire);
}
}
缓存预热
系统启动时预加载热点数据,避免启动后大量请求穿透到数据库。
// 缓存预热
@Component
public class CacheWarmup implements ApplicationRunner {
private final Cache cache;
private final Database database;
@Override
public void run(ApplicationArguments args) {
log.info("开始缓存预热...");
// 加载热点用户
List<User> hotUsers = database.queryHotUsers();
for (User user : hotUsers) {
cache.set("user:" + user.getId(), user, 3600);
}
// 加载热门商品
List<Product> hotProducts = database.queryHotProducts();
for (Product product : hotProducts) {
cache.set("product:" + product.getId(), product, 3600);
}
log.info("缓存预热完成,加载 {} 用户,{} 商品",
hotUsers.size(), hotProducts.size());
}
}
监控缓存指标
监控缓存命中率、内存使用等关键指标,及时发现问题。
// 缓存监控
public class CacheMetrics {
private final AtomicLong hits = new AtomicLong(0);
private final AtomicLong misses = new AtomicLong(0);
public void recordHit() {
hits.incrementAndGet();
}
public void recordMiss() {
misses.incrementAndGet();
}
public double getHitRate() {
long total = hits.get() + misses.get();
if (total == 0) return 0;
return (double) hits.get() / total;
}
@Scheduled(fixedRate = 60000)
public void reportMetrics() {
log.info("缓存统计: 命中={}, 未命中={}, 命中率={}",
hits.get(), misses.get(), getHitRate());
}
}
小结
本章我们深入学习了分布式缓存的核心知识:
缓存的价值:显著降低数据访问延迟,减轻数据库压力,提高系统吞吐量。
缓存模式:Cache-Aside 最常用,Read-Through 和 Write-Through 封装更好,Write-Behind 性能最高但一致性最弱。
缓存挑战:缓存穿透、击穿、雪崩是三大典型问题,需要通过空值缓存、布隆过滤器、互斥锁、随机过期时间等手段解决。
一致性问题:缓存与数据库的一致性是分布式系统的难题,推荐先更新数据库再删除缓存,配合延迟双删或消息队列保证最终一致性。
Redis 实践:Redis 是主流分布式缓存,支持丰富的数据结构,需要合理选择持久化策略,必要时使用集群扩展。
多级缓存:本地缓存 + 分布式缓存组合使用,可以进一步提升性能,但需要处理一致性问题。
最佳实践:合理设置过期时间、缓存预热、监控关键指标是保证缓存系统稳定运行的关键。
缓存是系统优化的重要手段,但不是银弹。需要根据业务场景选择合适的缓存策略,并持续监控和优化。
如果你想深入学习缓存,推荐阅读:
- Redis 官方文档:redis.io/docs
- 《Redis 设计与实现》——黄健宏
- AWS 缓存策略白皮书:docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/