【Redis】实现缓存及相关问题

Redis实现缓存及相关问题

认识缓存

缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。

缓存的作用:

  1. 降低后端负载
  2. 提高读写效率,降低响应时间

缓存的成本:

  1. 数据一致性成本
  2. 代码维护成本
  3. 运维成本

添加缓存

缓存作用模型

查询商铺缓存的流程

添加缓存业务代码

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服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  1. 给不同的Key的TTL添加随机值
  2. 利用Redis集群提高服务的可用性
  3. 给缓存业务添加降级限流策略
  4. 给业务添加多级缓存

缓存击穿/热点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();
}
相关推荐
一心只为学17 分钟前
pgpool配置安装之服务器的配置
运维·数据库·postgresql·pgpool
轻口味1 小时前
【每日学点鸿蒙知识】调试、网络、缓存、富文本编辑等
缓存·华为·harmonyos
i-Java1 小时前
CentOS7安装redis
linux·redis·缓存·centos
码农君莫笑1 小时前
加密 SQLite 数据库管理研究
数据库·sql·sqlite
鸿永与1 小时前
『SQLite』约束怎么用
数据库·sqlite
liuzhilongDBA2 小时前
pg数据库运维经验2024
运维·数据库
背太阳的牧羊人3 小时前
使用 SQLite3 的基本操作步骤
数据库·sqlite
从后端到QT3 小时前
Android NDK开发入门2之适应idm环境
前端·javascript·数据库
背太阳的牧羊人3 小时前
使用 SQL 和表格数据进行问答和 RAG(6)—将指定目录下的 CSV 或 Excel 文件导入 SQLite 数据库
数据库·sql·langchain·sqlite·excel
会飞的爱迪生3 小时前
nginx反向代理+缓存
运维·nginx·缓存