Redis实现缓存及相关问题
认识缓存
缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。
缓存的作用:
- 降低后端负载
- 提高读写效率,降低响应时间
缓存的成本:
- 数据一致性成本
- 代码维护成本
- 运维成本
添加缓存
缓存作用模型
查询商铺缓存的流程
添加缓存业务代码
@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. 保存到RedisredisTemplate.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的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿/热点Key
什么是缓存击穿
缓存击穿问题也叫热点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. 保存到RedisredisTemplate.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为2mindate.setTime(System.currentTimeMillis() + 2 * 60 * 1000);redisTemplate.opsForValue().set(CACHE_USER_PRE + id, gson.toJson(new LogicalExpiration<>(userDTO, date)));});}// 返回数据return logicalExpiration.getValue();
}