缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。
缓存的作用:
缓存的成本:
@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时间,到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,在修改数据库的同时,更新缓存。 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
优点:实现简单,维护方便
缺点:额外的内存消耗、可能造成短期的不一致
业务实现
// 缓存空对象
@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问题:一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
优点:没有额外的内存消耗、保证一致性、实现简单
缺点:线程需要等待,性能受影响、可能有死锁风险
互斥锁流程图
互斥锁业务代码
// 热点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逻辑过期实体类,使用泛型使其通用化
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LogicalExpiration<T> {
private T value;
private Date date;
}
2、核心业务代码
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();
}