缓存实现及其问题
认识缓存
缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。
缓存的作用:
- 降低后端负载
- 提高读写效率,降低响应时间
缓存的成本:
- 数据一致性成本
- 代码维护成本
- 运维成本
添加缓存
缓存作用模型
查询商铺缓存的流程
添加缓存业务代码
java
@Override
public List<UserDTO> getUserlist() {
Gson gson = new Gson();
// 1. 查询redis缓存
String cache = redisTemplate.opsForValue().get(CACHE_LIST_PRE);
// 2.1. 存在缓存
if (StrUtil.isNotBlank(cache)) {
// 3. 反序列化
return gson.fromJson(cache, new TypeToken<List<UserDTO>>() {}.getType());
}
// 2.2. 不存在缓存
// 3. 查询数据库
List<User> userList = list();
// 4. 信息脱敏
ArrayList<UserDTO> userDTOList = new ArrayList<>();
for (User user : userList) {
UserDTO dto = new UserDTO();
BeanUtil.copyProperties(user, dto);
dto.setPhone(DesensitizedUtil.mobilePhone(dto.getPhone()));
userDTOList.add(dto);
}
// 5. 保存到Redis
redisTemplate.opsForValue()
.set(CACHE_LIST_PRE, gson.toJson(userDTOList), 2, TimeUnit.MINUTES);
return userDTOList;
}
缓存更新
缓存更新策略
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 利用Redis的内存淘汰机制,内存不足时自动淘汰部分数据。 | 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,在修改数据库的同时,更新缓存。 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
- 低一致性需求:使用内存淘汰机制
- 高一致性需求:主动更新 + 超时剔除
主动更新策略
- 读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作:
- 先操作数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性(事物/分布式事物)
缓存穿透
什么是缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
缓存空对象
优点:实现简单,维护方便
缺点:额外的内存消耗、可能造成短期的不一致
业务实现
java
// 缓存空对象
@Override
public UserDTO getInfoById(Long id) {
// 缓存查询
String userString = redisTemplate.opsForValue()
.get(CACHE_USER_PRE + id);
if (StrUtil.isNotBlank(userString)) {
// 有缓存 => 真实数据
return gson.fromJson(userString, UserDTO.class);
}
if (userString != null) {
// 有缓存 => 空对象
throw new BusinessException(404, "用户不存在");
}
// 数据库查询
User user = getById(id);
if (user == null) {
// 缓存空对象
redisTemplate.opsForValue()
.set(CACHE_USER_PRE + id, "", 2, TimeUnit.MINUTES);
throw new BusinessException(404, "用户不存在");
}
// 信息脱敏
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 缓存真实数据
redisTemplate.opsForValue()
.set(CACHE_USER_PRE + id, gson.toJson(userDTO), 2, TimeUnit.MINUTES);
return userDTO;
}
布隆过滤器
优点:内存占用较少,没有多余key
缺点:实现复杂、存在误判可能
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿/热点Key
什么是缓存击穿
缓存击穿问题也叫热点Key问题:一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
互斥锁
优点:没有额外的内存消耗、保证一致性、实现简单
缺点:线程需要等待,性能受影响、可能有死锁风险
互斥锁流程图
互斥锁业务代码
java
// 热点key-互斥锁
@Override
public List<UserDTO> getUserlist() throws InterruptedException {
// 1. 查询redis缓存
String cache = redisTemplate.opsForValue().get(CACHE_LIST_PRE);
// 2.1. 存在缓存
if (StrUtil.isNotBlank(cache)) {
// 3. 反序列化
return gson.fromJson(cache, new TypeToken<List<UserDTO>>() {}.getType());
}
// ⭐️ 获取互斥锁
String lock = REDIS_LOCK_PRE + "userlist";
Boolean flag = redisTemplate.opsForValue()
.setIfAbsent(lock, "1", 30, TimeUnit.SECONDS);
if (BooleanUtil.isFalse(flag)) {
// ⭐️ 获取锁失败了 => 休眠 + 递归
Thread.sleep(200);
return getUserlist();
}
ArrayList<UserDTO> userDTOList = new ArrayList<>();
try {
// 2.2. 不存在缓存
// 3. 查询数据库
List<User> userList = list();
log.info("查询数据库");
// 4. 信息脱敏
for (User user : userList) {
UserDTO dto = new UserDTO();
BeanUtil.copyProperties(user, dto);
dto.setPhone(DesensitizedUtil.mobilePhone(dto.getPhone()));
userDTOList.add(dto);
}
// 5. 保存到Redis
redisTemplate.opsForValue()
.set(CACHE_LIST_PRE, gson.toJson(userDTOList), 10, TimeUnit.SECONDS);
} catch (Exception ignored) {
} finally {
// ⭐️ 释放锁
redisTemplate.delete(lock);
}
return userDTOList;
}
逻辑过期
优点:线程无需等待,性能较好
缺点:不保证一致性、有额外内存消耗、实现复杂
逻辑过期流程图
逻辑过期业务代码
1、LogicalExpiration逻辑过期实体类,使用泛型使其通用化
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LogicalExpiration<T> {
private T value;
private Date date;
}
2、核心业务代码
java
public UserDTO getUserById(Long id) {
// 1. 查询缓存
String userString = redisTemplate.opsForValue().get(CACHE_USER_PRE + id);
if (StrUtil.isBlank(userString)) {
// 没有缓存 -> 查询数据
User user = getById(id);
// 没有数据 -> 报错
if (user == null) {
throw new BusinessException(500, "用户不存在");
}
// 数据脱敏
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 新建缓存
Date date = new Date();
date.setTime(System.currentTimeMillis() + 2 * 60 * 1000);
redisTemplate.opsForValue().set(CACHE_USER_PRE + id, gson.toJson(
new LogicalExpiration<>(userDTO, date)
));
// 返回数据
return userDTO;
}
// 存在缓存 => 反序列化拿到对象
LogicalExpiration<UserDTO> logicalExpiration = gson.fromJson(
userString, new TypeToken<LogicalExpiration<UserDTO>>() {}.getType());
// 判断缓存是否过期
if (logicalExpiration.getDate().before(new Date())) {
// 已经过期 => 新建线程进行更新缓存
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
UserDTO userDTO = BeanUtil.copyProperties(getById(id), UserDTO.class);
Date date = new Date();
// 设置TTL为2min
date.setTime(System.currentTimeMillis() + 2 * 60 * 1000);
redisTemplate.opsForValue().set(CACHE_USER_PRE + id, gson.toJson(
new LogicalExpiration<>(userDTO, date)
));
});
}
// 返回数据
return logicalExpiration.getValue();
}