数据库和缓存结合 | 青训营笔记

75 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 19 天

  • 缓存的适用场景

    • 精简为四字就是:读多写少

      • 访问量很大,需要使用缓存来承担一部分压力(读多、写少)
      • 即时性要求高,能承受一定时间内的数据不一致性。
      • 较长时间不会改变的数据,如后台管理的菜单列表,商品分类列表等等。
  • 使用java自带的序列化缺点

    • 它只适用于Java项目,对其他语言编写的项目不兼容,如Go或者PHP
    • 在Redis的可视化页面,无法进行较好的展示
    • Redis 的默认序列化机制改为JSON格式,一方面兼容性较高,另一方面在可视化界面也较好查看
  • 常用Redis链接工厂比较

    • Lettuce

      • SpringBoot 2.0 及之后版本 Redis 的默认连接工厂
      • Lettuce则完全克服了其线程不安全的缺点;
      • Lettuce是一个可伸缩的线程安全的 Redis客户端,支持同步、异步和响应式模式。多个线程可以共享一个连接实例,而不必担心多线程并发问题。
      • 它基于优秀 Netty NIO 框架构建,支持 Redis 的高级功能,如 Sentinel,集群,流水线,自动重新连接和 Redis 数据模型。
    • Jedis

      • Jedis 是一个优秀的基于 Java 语言的 Redis 客户端
      • Jedis 在实现上是直接连接 Redis-Server,在多个线程间共享一个 Jedis 实例时是线程不安全的,如果想要在多线程场景下使用 Jedis,需要使用连接池,每个线程都使用自己的 Jedis 实例,当连接数量增多时,会消耗较多的物理资源。
  • 缓存和限流的区别

    • 限流会限制最高的访问人数,保证系统的正常运行,不会崩溃。
    • 服务降级,会返回用户一些合适的提示信息,对于用户而言,刷新个四五次还是有可能访问成功的。
    • 都可以保证系统的运行,不至于让系统崩溃。
  • 缓存常见问题

    • 缓存穿透

      • 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也没有,如果我们不存储这个空数据,那么持续的访问就会导致我们的数据库压力倍增,此时我们就可以将空结果(null)存入到缓存中并且设置一个较短的过期时间
      • 接口层增加校验,如用户鉴权校验,编写一些特殊数据的校验,预防这样的事故的发生。如将id<=0的查询请求直接拒绝掉。
    • 缓存雪崩

      • 部署角度:实现 Redis 的高可用,主从+哨兵,Redis集群。

      • 应用程序角度:

        • 本地缓存 + 限流&降级
        • 允许的话,设置热点数据永远不过期
        • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生(但实际而言,这一点很多时候其实不做的,因为如果加上随机时间后,再碰撞又该如何呢?)
      • 恢复角度:Redis 的 RDB+AOF组合持久化策略,方便redis宕机后及时恢复数据

    • 缓存击穿

      • 设置热点数据不过期;
      • 第一时间去数据库获取数据填充到redis中,并且在请求数据库时需要加锁,避免所有的请求都直接访问数据库,一旦有一个请求正确查询到数据库时,将结果存入缓存中,其他的线程获取锁失败后,让其睡眠一会,之后重新尝试查询缓存,获取成功,直接返回结果;获取失败,则再次尝试获取锁,重复上述流程。
  • 更新数据库和缓存的常见四种方式

    • 先更新缓存,再更新数据库
    • 先更新数据库,再更新缓存
    • 删除缓存,再更新数据库
    • 更新数据库,再删除缓存
  • 更新数据的两种

    • 实际上就是由于第二个更新的数据载体可能更新失败
    • 所以为了保证数据正确,应该先将缓存失效即删除后再进行操作
  • 删除缓存和更新数据库的两种

    • 先删除缓存,再更新数据库

    • 先更新数据库再清除缓存

      • 实际上这两种还是都会产生并发条件下的数据异常,但是第二种数据异常的概率小,其需要满足以下三个情况
      • 时刻1:读请求的时候,缓存正好过期
      • 时刻2:读请求在写请求更新数据库之前查询数据库,
      • 时刻3:写请求,在更新数据库之后,要在读请求成功写入缓存前,先执行删除缓存操作
  • 数据库缓存操作总结

    • 无论是更新缓存还是删除缓存,一般都是选择先对数据库操作优先,而对缓存操作稍后
    • 实际上这属于一种最终一致性的实现,因为如果真的要保证强一致性,正确的做法应该是使用MySQL并且限流降级
  • 真正的数据一致性实现

    • 使用消息队列异步保证事务

      • 首先消息队列在高并发的场景下,可以毋庸置疑的说是一个非常重要的组件啦,所以引入消息队列以及维护消息队列,其实都不能算是额外的负担。

        其次消息队列具有持久化,即使项目重启也不会丢失。

        最后消息队列自身可以实现可靠性

        • 保证消息成功发送,发送到交换机;
        • 保证消息成功从交换机发送至队列;
        • 消费者端接收到消息,采用手动ACK确认机制,成功消费后才会删除消息,消费失败则重新投递
    • Canal 订阅日志实现

      • 可以使用 alibaba 的开源组件 Canal,订阅数据库变更日志,当数据库发生变更时,我们可以拿到具体操作的数据,然后再去根据具体的数据,去删除对应的缓存。当然Canal 也是要配合消息队列一起来使用的,因为其Canal本身是没有数据处理能力的。
    • 延迟双删

      • 解决问题:【先删除缓存,再更新数据库】在读写并发时,会产生缓存是旧数据,而数据库是新数据的问题,这该如何解决呢?
      • 在执行完更新数据库的操作后,先休眠一会儿,再进行一次缓存的删除,以确保数据一致性,这也就是市面上给出的主流解决方案--延时双删
      • 首先延迟删除的时间需要大于 1号用户执行流程的总时间即:【1号用户从数据库读取数据+写入缓存】时间
      • 但是要说具体是多长,这无法给出一个准确答复,只能经过不断的压测和实验,预估一个大概的时间,尽可能的去降低发生数据不一致的概率罢了。