多级缓存原理及实现

首先为什么需要多级缓存?

传统的缓存策略一般是请求到达Tomcat服务后,先查询Redis,如果未命中则查询数据库,存在下面的问题:

  • 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈。

  • Redis缓存失效时,会对数据库产生冲击。

多级缓存方案:多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能

用户请求 ——》 反向代理Nginx ——》 业务Nginx (通过lua编写业务逻辑) ——》Redis ——> tomcat(本地缓存) ——》数据库。

实现多级缓存我们需要了解到的知识:Lua 编程(针对Nginx中的逻辑编码)、Nginx本地缓存、本地缓存实现、Openresty、Redis缓存预热。

分布式缓存,例如Redis:

优点:存储容量更大、可靠性更好、可以在集群间共享

缺点:访问缓存有网络开销

场景:缓存数据量较大、可靠性要求较高、需要在集群间共享

进程本地缓存,例如HashMap、GuavaCache、Caffeine

优点:读取本地内存,没有网络开销,速度更快

缺点:存储容量有限、可靠性较低、无法共享

场景:性能要求较高,缓存数据量较小

Redis 冷启动与缓存预热

冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。

缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。

缓存预热

@Componentpublic class RedisHandler implements InitializingBean {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Override         
    public void afterPropertiesSet() throws Exception {
     // 初始化缓存 ... 
    }
}

缓存同步策略

缓存数据同步的常见方式有三种:

设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新

优势:简单、方便

缺点:时效性差,缓存过期之前可能不一致

场景:更新频率较低,时效性要求低的业务

同步双写:在修改数据库的同时,直接修改缓存

优势:时效性强,缓存与数据库强一致

缺点:有代码侵入,耦合度高;

场景:对一致性、时效性要求较高的缓存数据

异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

优势:低耦合,可以同时通知多个缓存服务

缺点:时效性一般,可能存在中间不一致状态

场景:时效性要求一般,有多个服务需要同步

基于MQ的异步同步策略

基于Canal的异步通知:Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。不过这里我们会使用GitHub上的第三方开源的canal-starter。地址:https://github.com/NormanGyllenhaal/canal-client

<!--canal-->
<dependency>
  <groupId>top.javatool</groupId>
  <artifactId>canal-spring-boot-starter</artifactId>
  <version>1.2.1-RELEASE</version> 
</dependency>

编写配置

canal:   
    destination: canalName # canal实例名称,要跟canal-server运行时设置的destination一致   
    server: IP:port # canal地址 

编写监听器,监听Canal消息

@CanalTable("tb_ table") 
@Component 
public class ItemHandler implements EntryHandler<Item> {
    @Override   
    public void insert(Item item) {
     // 新增数据到redis         
    }          
    @Override          
    public void update(Item before, Item after) {
        // 更新redis数据                       
        // 更新本地缓存       
    }  
   @Override          
   public void delete(Item item) {
         // 删除redis数据                      
        // 清理本地缓存         
   }
}

Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解:

@Id 标记表中的id字段

@Column(name = "name") 标记表中与属性名不一致的字段

@Transient 标记不属于表中的字段