多级缓存原理及实现
首先为什么需要多级缓存?
传统的缓存策略一般是请求到达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 标记不属于表中的字段