665 lines
15 KiB
Plaintext
665 lines
15 KiB
Plaintext
# 缓存策略详解
|
||
|
||
## Redis 缓存架构
|
||
|
||
### 缓存层次结构
|
||
```
|
||
应用层 -> 本地缓存 (Caffeine) -> Redis 缓存 -> 数据库
|
||
```
|
||
|
||
### 核心组件
|
||
- **Spring Cache**: 统一缓存抽象
|
||
- **Redis**: 分布式缓存存储
|
||
- **Caffeine**: 本地缓存提升性能
|
||
- **Redisson**: Redis 客户端和分布式锁
|
||
|
||
## 缓存配置
|
||
|
||
### Redis 连接配置
|
||
```yaml
|
||
spring:
|
||
data:
|
||
redis:
|
||
host: 127.0.0.1
|
||
port: 6379
|
||
password:
|
||
database: 0
|
||
timeout: 2000ms
|
||
lettuce:
|
||
pool:
|
||
max-active: 200
|
||
max-idle: 20
|
||
min-idle: 5
|
||
max-wait: 1000ms
|
||
```
|
||
|
||
### 缓存管理器配置
|
||
```java
|
||
@Configuration
|
||
@EnableCaching
|
||
public class CacheConfiguration {
|
||
|
||
@Bean
|
||
@Primary
|
||
public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
|
||
// Redis 缓存管理器
|
||
RedisCacheManager.Builder builder = RedisCacheManager
|
||
.RedisCacheManagerBuilder
|
||
.fromConnectionFactory(redisTemplate.getConnectionFactory())
|
||
.cacheDefaults(buildCacheConfiguration());
|
||
|
||
return builder.build();
|
||
}
|
||
|
||
@Bean("localCacheManager")
|
||
public CacheManager localCacheManager() {
|
||
// 本地缓存管理器
|
||
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||
cacheManager.setCaffeine(Caffeine.newBuilder()
|
||
.maximumSize(1000)
|
||
.expireAfterWrite(30, TimeUnit.MINUTES));
|
||
return cacheManager;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 缓存使用模式
|
||
|
||
### 基础缓存注解
|
||
```java
|
||
@Service
|
||
public class UserServiceImpl implements UserService {
|
||
|
||
/**
|
||
* 查询缓存
|
||
*/
|
||
@Cacheable(value = "user", key = "#id")
|
||
public UserDO getUser(Long id) {
|
||
return userMapper.selectById(id);
|
||
}
|
||
|
||
/**
|
||
* 更新缓存
|
||
*/
|
||
@CachePut(value = "user", key = "#user.id")
|
||
public UserDO updateUser(UserDO user) {
|
||
userMapper.updateById(user);
|
||
return user;
|
||
}
|
||
|
||
/**
|
||
* 删除缓存
|
||
*/
|
||
@CacheEvict(value = "user", key = "#id")
|
||
public void deleteUser(Long id) {
|
||
userMapper.deleteById(id);
|
||
}
|
||
|
||
/**
|
||
* 清空缓存
|
||
*/
|
||
@CacheEvict(value = "user", allEntries = true)
|
||
public void clearUserCache() {
|
||
// 清空所有用户缓存
|
||
}
|
||
}
|
||
```
|
||
|
||
### 条件缓存
|
||
```java
|
||
// 条件缓存 - 只有状态为启用的用户才缓存
|
||
@Cacheable(value = "user", key = "#id", condition = "#user.status == 1")
|
||
public UserDO getUser(Long id) {
|
||
return userMapper.selectById(id);
|
||
}
|
||
|
||
// 排除缓存 - 管理员用户不缓存
|
||
@Cacheable(value = "user", key = "#id", unless = "#result.username == 'admin'")
|
||
public UserDO getUser(Long id) {
|
||
return userMapper.selectById(id);
|
||
}
|
||
```
|
||
|
||
### 自定义缓存键
|
||
```java
|
||
// 复合键
|
||
@Cacheable(value = "user", key = "#tenantId + ':' + #username")
|
||
public UserDO getUserByUsername(Long tenantId, String username) {
|
||
return userMapper.selectOne(new LambdaQueryWrapper<UserDO>()
|
||
.eq(UserDO::getTenantId, tenantId)
|
||
.eq(UserDO::getUsername, username));
|
||
}
|
||
|
||
// 使用 SpEL 表达式
|
||
@Cacheable(value = "user", key = "T(String).valueOf(#user.tenantId).concat(':').concat(#user.username)")
|
||
public UserDO createUser(UserCreateReqVO reqVO) {
|
||
// 创建用户逻辑
|
||
}
|
||
```
|
||
|
||
## Redis 数据结构使用
|
||
|
||
### 字符串 (String)
|
||
```java
|
||
@Component
|
||
public class StringCacheService {
|
||
|
||
@Resource
|
||
private StringRedisTemplate stringRedisTemplate;
|
||
|
||
/**
|
||
* 设置缓存
|
||
*/
|
||
public void set(String key, String value, Duration timeout) {
|
||
stringRedisTemplate.opsForValue().set(key, value, timeout);
|
||
}
|
||
|
||
/**
|
||
* 获取缓存
|
||
*/
|
||
public String get(String key) {
|
||
return stringRedisTemplate.opsForValue().get(key);
|
||
}
|
||
|
||
/**
|
||
* 原子递增
|
||
*/
|
||
public Long increment(String key) {
|
||
return stringRedisTemplate.opsForValue().increment(key);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 哈希 (Hash)
|
||
```java
|
||
@Component
|
||
public class HashCacheService {
|
||
|
||
@Resource
|
||
private StringRedisTemplate stringRedisTemplate;
|
||
|
||
/**
|
||
* 设置 Hash 字段
|
||
*/
|
||
public void hset(String key, String field, String value) {
|
||
stringRedisTemplate.opsForHash().put(key, field, value);
|
||
}
|
||
|
||
/**
|
||
* 获取 Hash 字段
|
||
*/
|
||
public String hget(String key, String field) {
|
||
return (String) stringRedisTemplate.opsForHash().get(key, field);
|
||
}
|
||
|
||
/**
|
||
* 获取所有 Hash 字段
|
||
*/
|
||
public Map<Object, Object> hgetAll(String key) {
|
||
return stringRedisTemplate.opsForHash().entries(key);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 列表 (List)
|
||
```java
|
||
@Component
|
||
public class ListCacheService {
|
||
|
||
@Resource
|
||
private StringRedisTemplate stringRedisTemplate;
|
||
|
||
/**
|
||
* 左推入
|
||
*/
|
||
public void lpush(String key, String value) {
|
||
stringRedisTemplate.opsForList().leftPush(key, value);
|
||
}
|
||
|
||
/**
|
||
* 右弹出
|
||
*/
|
||
public String rpop(String key) {
|
||
return stringRedisTemplate.opsForList().rightPop(key);
|
||
}
|
||
|
||
/**
|
||
* 获取列表
|
||
*/
|
||
public List<String> range(String key, long start, long end) {
|
||
return stringRedisTemplate.opsForList().range(key, start, end);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 集合 (Set)
|
||
```java
|
||
@Component
|
||
public class SetCacheService {
|
||
|
||
@Resource
|
||
private StringRedisTemplate stringRedisTemplate;
|
||
|
||
/**
|
||
* 添加成员
|
||
*/
|
||
public void sadd(String key, String... values) {
|
||
stringRedisTemplate.opsForSet().add(key, values);
|
||
}
|
||
|
||
/**
|
||
* 获取所有成员
|
||
*/
|
||
public Set<String> smembers(String key) {
|
||
return stringRedisTemplate.opsForSet().members(key);
|
||
}
|
||
|
||
/**
|
||
* 判断是否存在
|
||
*/
|
||
public Boolean sismember(String key, String value) {
|
||
return stringRedisTemplate.opsForSet().isMember(key, value);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 有序集合 (ZSet)
|
||
```java
|
||
@Component
|
||
public class ZSetCacheService {
|
||
|
||
@Resource
|
||
private StringRedisTemplate stringRedisTemplate;
|
||
|
||
/**
|
||
* 添加成员
|
||
*/
|
||
public void zadd(String key, String value, double score) {
|
||
stringRedisTemplate.opsForZSet().add(key, value, score);
|
||
}
|
||
|
||
/**
|
||
* 获取排名范围
|
||
*/
|
||
public Set<String> zrange(String key, long start, long end) {
|
||
return stringRedisTemplate.opsForZSet().range(key, start, end);
|
||
}
|
||
|
||
/**
|
||
* 获取分数范围
|
||
*/
|
||
public Set<String> zrangeByScore(String key, double min, double max) {
|
||
return stringRedisTemplate.opsForZSet().rangeByScore(key, min, max);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 缓存键设计规范
|
||
|
||
### 键命名规范
|
||
```java
|
||
public class RedisKeyConstants {
|
||
|
||
/**
|
||
* 用户缓存
|
||
* KEY 格式:user:{id}
|
||
* 过期时间:30 分钟
|
||
*/
|
||
public static final String USER = "user";
|
||
|
||
/**
|
||
* 验证码缓存
|
||
* KEY 格式:captcha:{uuid}
|
||
* 过期时间:5 分钟
|
||
*/
|
||
public static final String CAPTCHA = "captcha";
|
||
|
||
/**
|
||
* 权限缓存
|
||
* KEY 格式:permission:{userId}
|
||
* 过期时间:1 小时
|
||
*/
|
||
public static final String PERMISSION = "permission";
|
||
|
||
/**
|
||
* 字典缓存
|
||
* KEY 格式:dict:{type}
|
||
* 过期时间:永不过期
|
||
*/
|
||
public static final String DICT = "dict";
|
||
}
|
||
```
|
||
|
||
### 键生成工具
|
||
```java
|
||
@Component
|
||
public class RedisKeyBuilder {
|
||
|
||
/**
|
||
* 构建用户缓存键
|
||
*/
|
||
public static String buildUserKey(Long userId) {
|
||
return RedisKeyConstants.USER + ":" + userId;
|
||
}
|
||
|
||
/**
|
||
* 构建验证码缓存键
|
||
*/
|
||
public static String buildCaptchaKey(String uuid) {
|
||
return RedisKeyConstants.CAPTCHA + ":" + uuid;
|
||
}
|
||
|
||
/**
|
||
* 构建权限缓存键
|
||
*/
|
||
public static String buildPermissionKey(Long userId) {
|
||
return RedisKeyConstants.PERMISSION + ":" + userId;
|
||
}
|
||
|
||
/**
|
||
* 构建租户级别缓存键
|
||
*/
|
||
public static String buildTenantKey(String prefix, Long tenantId, Object suffix) {
|
||
return prefix + ":" + tenantId + ":" + suffix;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 分布式锁
|
||
|
||
### Redisson 分布式锁
|
||
```java
|
||
@Component
|
||
public class DistributedLockService {
|
||
|
||
@Resource
|
||
private RedissonClient redissonClient;
|
||
|
||
/**
|
||
* 尝试获取锁
|
||
*/
|
||
public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {
|
||
RLock lock = redissonClient.getLock(lockKey);
|
||
try {
|
||
return lock.tryLock(waitTime, leaseTime, unit);
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 释放锁
|
||
*/
|
||
public void unlock(String lockKey) {
|
||
RLock lock = redissonClient.getLock(lockKey);
|
||
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
|
||
lock.unlock();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 锁模板方法
|
||
*/
|
||
public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime,
|
||
TimeUnit unit, Supplier<T> supplier) {
|
||
RLock lock = redissonClient.getLock(lockKey);
|
||
try {
|
||
if (lock.tryLock(waitTime, leaseTime, unit)) {
|
||
return supplier.get();
|
||
} else {
|
||
throw new ServiceException("获取锁失败");
|
||
}
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
throw new ServiceException("获取锁被中断");
|
||
} finally {
|
||
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
|
||
lock.unlock();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 分布式锁应用
|
||
```java
|
||
@Service
|
||
public class OrderService {
|
||
|
||
@Resource
|
||
private DistributedLockService lockService;
|
||
|
||
/**
|
||
* 创建订单(防重复提交)
|
||
*/
|
||
public OrderDO createOrder(OrderCreateReqVO reqVO) {
|
||
String lockKey = "order:create:" + reqVO.getUserId();
|
||
|
||
return lockService.executeWithLock(lockKey, 5, 10, TimeUnit.SECONDS, () -> {
|
||
// 检查重复订单
|
||
validateDuplicateOrder(reqVO);
|
||
|
||
// 创建订单
|
||
return doCreateOrder(reqVO);
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
## 缓存预热
|
||
|
||
### 应用启动预热
|
||
```java
|
||
@Component
|
||
public class CacheWarmUpService {
|
||
|
||
@Resource
|
||
private DictDataService dictDataService;
|
||
|
||
@Resource
|
||
private ConfigService configService;
|
||
|
||
@EventListener(ApplicationReadyEvent.class)
|
||
public void warmUpCache() {
|
||
log.info("[warmUpCache][开始预热缓存]");
|
||
|
||
// 预热字典缓存
|
||
dictDataService.warmUpCache();
|
||
|
||
// 预热配置缓存
|
||
configService.warmUpCache();
|
||
|
||
log.info("[warmUpCache][预热缓存完成]");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 定时预热
|
||
```java
|
||
@Component
|
||
public class CacheScheduleService {
|
||
|
||
/**
|
||
* 定时刷新热点数据
|
||
*/
|
||
@Scheduled(fixedRate = 300000) // 5分钟执行一次
|
||
public void refreshHotData() {
|
||
// 刷新热点商品缓存
|
||
refreshProductCache();
|
||
|
||
// 刷新活动缓存
|
||
refreshActivityCache();
|
||
}
|
||
}
|
||
```
|
||
|
||
## 缓存监控
|
||
|
||
### 缓存统计
|
||
```java
|
||
@Component
|
||
public class CacheMetricsService {
|
||
|
||
@Resource
|
||
private CacheManager cacheManager;
|
||
|
||
/**
|
||
* 获取缓存统计信息
|
||
*/
|
||
public Map<String, Object> getCacheStats() {
|
||
Map<String, Object> stats = new HashMap<>();
|
||
|
||
Collection<String> cacheNames = cacheManager.getCacheNames();
|
||
for (String cacheName : cacheNames) {
|
||
Cache cache = cacheManager.getCache(cacheName);
|
||
if (cache instanceof RedisCache) {
|
||
// 获取 Redis 缓存统计
|
||
stats.put(cacheName, getRedisCacheStats(cache));
|
||
}
|
||
}
|
||
|
||
return stats;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 缓存健康检查
|
||
```java
|
||
@Component
|
||
public class CacheHealthIndicator extends AbstractHealthIndicator {
|
||
|
||
@Resource
|
||
private StringRedisTemplate stringRedisTemplate;
|
||
|
||
@Override
|
||
protected void doHealthCheck(Health.Builder builder) throws Exception {
|
||
try {
|
||
// 测试 Redis 连接
|
||
String pong = stringRedisTemplate.getConnectionFactory()
|
||
.getConnection().ping();
|
||
|
||
if ("PONG".equals(pong)) {
|
||
builder.up().withDetail("redis", "连接正常");
|
||
} else {
|
||
builder.down().withDetail("redis", "连接异常");
|
||
}
|
||
} catch (Exception e) {
|
||
builder.down().withException(e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 缓存优化策略
|
||
|
||
### 多级缓存
|
||
```java
|
||
@Service
|
||
public class MultiLevelCacheService {
|
||
|
||
@Resource
|
||
private CacheManager localCacheManager;
|
||
|
||
@Resource
|
||
private CacheManager redisCacheManager;
|
||
|
||
public <T> T get(String key, Class<T> type, Supplier<T> loader) {
|
||
// 1. 先查本地缓存
|
||
Cache localCache = localCacheManager.getCache("local");
|
||
T value = localCache.get(key, type);
|
||
if (value != null) {
|
||
return value;
|
||
}
|
||
|
||
// 2. 再查 Redis 缓存
|
||
Cache redisCache = redisCacheManager.getCache("redis");
|
||
value = redisCache.get(key, type);
|
||
if (value != null) {
|
||
// 回写本地缓存
|
||
localCache.put(key, value);
|
||
return value;
|
||
}
|
||
|
||
// 3. 最后查数据库
|
||
value = loader.get();
|
||
if (value != null) {
|
||
// 写入两级缓存
|
||
localCache.put(key, value);
|
||
redisCache.put(key, value);
|
||
}
|
||
|
||
return value;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 缓存穿透防护
|
||
```java
|
||
@Service
|
||
public class BloomFilterService {
|
||
|
||
@Resource
|
||
private RedisTemplate<String, Object> redisTemplate;
|
||
|
||
/**
|
||
* 检查是否可能存在
|
||
*/
|
||
public boolean mightContain(String key) {
|
||
// 使用布隆过滤器检查
|
||
String bloomKey = "bloom:user";
|
||
return redisTemplate.opsForValue().getBit(bloomKey, hash(key));
|
||
}
|
||
|
||
/**
|
||
* 添加到布隆过滤器
|
||
*/
|
||
public void put(String key) {
|
||
String bloomKey = "bloom:user";
|
||
redisTemplate.opsForValue().setBit(bloomKey, hash(key), true);
|
||
}
|
||
|
||
private long hash(String key) {
|
||
// 简单的哈希算法
|
||
return Math.abs(key.hashCode()) % 1000000;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 缓存雪崩防护
|
||
```java
|
||
@Service
|
||
public class AntiAvalancheService {
|
||
|
||
/**
|
||
* 随机过期时间防止雪崩
|
||
*/
|
||
@Cacheable(value = "user", key = "#id")
|
||
public UserDO getUser(Long id) {
|
||
// 缓存时间:30分钟 + 随机0-10分钟
|
||
Duration timeout = Duration.ofMinutes(30 + new Random().nextInt(10));
|
||
|
||
UserDO user = userMapper.selectById(id);
|
||
// 手动设置过期时间
|
||
redisTemplate.expire("user:" + id, timeout);
|
||
|
||
return user;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 缓存设计原则
|
||
1. **热点数据优先**: 经常访问的数据才缓存
|
||
2. **合理的过期时间**: 根据业务场景设置过期时间
|
||
3. **缓存键规范**: 使用有意义的键名和统一的命名规范
|
||
4. **避免大对象**: 单个缓存对象不要太大
|
||
5. **监控告警**: 设置缓存命中率等监控指标
|
||
|
||
### 常见问题解决
|
||
1. **缓存击穿**: 使用分布式锁保护热点数据
|
||
2. **缓存穿透**: 使用布隆过滤器或缓存空值
|
||
3. **缓存雪崩**: 设置随机过期时间
|
||
4. **数据一致性**: 使用缓存更新策略和事务
|
||
5. **内存泄漏**: 设置合理的过期时间和大小限制 |