SpringCloud-09 多级缓存-亿级流量的缓存方案

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

请求要经过 Tomcat 处理, Tomcat 的性能成为整个系统的瓶颈
Redis 缓存失效时,会对数据库产生冲击

多级缓存方案
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能:
①浏览器访问静态资源时,优先读取浏览器本地缓存
②访问非静态资源(ajax查询数据)时,访问服务端
③请求到达Nginx后,优先读取Nginx本地缓存
④如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)
⑤如果Redis查询未命中,则查询Tomcat
⑥请求进入Tomcat后,优先查询JVM进程缓存
⑦如果JVM进程缓存未命中,则查询数据库


在多级缓存架构中,Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑,因此这样的nginx服务不再是一个反向代理服务器,而是一个编写业务的Web服务器了。 因此这样的业务Nginx服务也需要搭建集群来提高并发,再有专门的nginx服务来做反向代理。另外,Tomcat服务将来也会部署为集群模式。

多级缓存的关键有两个:
①一个是在nginx中编写业务,实现nginx本地缓存、Redis、Tomcat的查询
②另一个就是在Tomcat中实现JVM进程缓存

其中Nginx编程则会用到OpenResty框架结合Lua这样的语言

目录

1、JVM进程缓存

1.1 导入商品案例

1.2 初识Caffeine

1.2.1 Caffeine示例

1.2.2 Caffeine提供了三种缓存驱逐策略

1.3 实现JVM进程缓存

2、Lua语法入门

2.1 初识Lua

​编辑​编辑​编辑

2.2 变量和循环

2.2.1 数据类型

 2.2.2 变量

2.2.3 循环

2.3 条件控制、函数

3、多级缓存

3.1 安装OpenResty

3.2 OpenResty快速入门

3.3 请求参数处理​编辑

3.4 查询Tomcat

3.5 Redis缓存预热

3.6 查询Redis缓存

3.7 Nginx本地缓存

4、缓存同步策略

4.1 数据同步策略

4.2 安装Canal

4.3 监听Canal


1、JVM进程缓存

1.1 导入商品案例

《案例导入说明.md》

1.2 初识Caffeine

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。

分布式缓存 ,例如 Redis
优点:存储容量更大、可靠性更好、可以在集群间共享
缺点:访问缓存有网络开销
场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
进程本地缓存 ,例如 HashMap GuavaCache
优点:读取本地内存,没有网络开销,速度更快
缺点:存储容量有限、可靠性较低、无法共享
场景:性能要求较高,缓存数据量较小

Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是CaffeineGitHub地址:GitHub - ben-manes/caffeine: A high performance caching library for Java

1.2.1 Caffeine示例

@Test
void testBasicOps() {
    // 构建cache对象
    Cache<String, String> cache = Caffeine.newBuilder().build();

    // 存数据
    cache.put("gf", "迪丽热巴");

    // 取数据
    String gf = cache.getIfPresent("gf");
    System.out.println("gf = " + gf);

    // 取数据,包含两个参数:
    // 参数一:缓存的key
    // 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
    // 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
    String defaultGF = cache.get("defaultGF", key -> {
        // 根据key去数据库查询数据
        return "柳岩";
    });
    System.out.println("defaultGF = " + defaultGF);
}

1.2.2 Caffeine提供了三种缓存驱逐策略

Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。
【注】在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。

基于容量:设置缓存的数量上限
    @Test
    void testEvictByNum() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                // 设置缓存大小上限为 1
                .maximumSize(1)
                .build();
        // 存数据
        cache.put("gf1", "柳岩");
        cache.put("gf2", "范冰冰");
        cache.put("gf3", "迪丽热巴");
        // 延迟10ms,给清理线程一点时间
        Thread.sleep(10L);
        // 获取数据
        System.out.println("gf1: " + cache.getIfPresent("gf1"));
        System.out.println("gf2: " + cache.getIfPresent("gf2"));
        System.out.println("gf3: " + cache.getIfPresent("gf3"));
    }
基于时间:设置缓存的有效时间
    @Test
    void testEvictByTime() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 1 秒
                .build();
        // 存数据
        cache.put("gf", "柳岩");
        // 获取数据
        System.out.println("gf: " + cache.getIfPresent("gf"));
        // 休眠一会儿
        Thread.sleep(1200L);
        System.out.println("gf: " + cache.getIfPresent("gf"));
    }
基于引用 :设置缓存为软引用或弱引用,利用 GC 来回收缓存数据。性能较差,不建议使用。

1.3 实现JVM进程缓存

首先,我们需要定义两个Caffeine的缓存对象,分别保存商品、库存的缓存数据。在item-service的com.heima.item.config包下定义CaffeineConfig类:

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }

    @Bean
    public Cache<Long, ItemStock> stockCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }
}

然后,修改item-service中的com.heima.item.web包下的ItemController类,添加缓存逻辑:

@RestController
@RequestMapping("item")
public class ItemController {

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    @Autowired
    private Cache<Long, Item> itemCache;
    @Autowired
    private Cache<Long, ItemStock> stockCache;
    
    // ...其它略
    
    @GetMapping("/{id}")
    public Item findById(@PathVariable("id") Long id) {
        return itemCache.get(id, key -> itemService.query()
                .ne("status", 3).eq("id", key)
                .one()
        );
    }

    @GetMapping("/stock/{id}")
    public ItemStock findStockById(@PathVariable("id") Long id) {
        return stockCache.get(id, key -> stockService.getById(key));
    }
}

2、Lua语法入门

2.1 初识Lua

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:The Programming Language Lua

2.2 变量和循环

2.2.1 数据类型

 2.2.2 变量

加local是局部变量;不加local是全局变量

2.2.3 循环

对于table,我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。

2.3 条件控制、函数

条件控制

3、多级缓存

利用OpenResty完成多级缓存业务的编写

3.1 安装OpenResty

《安装OpenResty.md》

OpenResty® 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:

具备 Nginx 的完整功能
基于 Lua 语言进行扩展,集成了大量精良的 Lua 库、第三方模块
允许使用 Lua自定义业务逻辑、自定义库

官方网站: OpenResty® - 中文官方站

3.2 OpenResty快速入门

3.3 请求参数处理

3.4 查询Tomcat

3.5 Redis缓存预热

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

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

我们数据量较少,可以在启动时将所有数据都放入缓存中。

缓存预热需要在项目启动时完成,并且必须是拿到RedisTemplate之后。这里我们利用InitializingBean接口来实现,因为InitializingBean可以在对象被Spring创建并且成员变量全部注入后执行。

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    // Spring里的默认Json处理工具
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {//该方法在bean创建完,Autowired之后执行
        // 初始化缓存
        // 1.查询商品信息
        List<Item> itemList = itemService.list();
        // 2.放入缓存
        for (Item item : itemList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }

        // 3.查询商品库存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入缓存
        for (ItemStock stock : stockList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
        }
    }
}

3.6 查询Redis缓存

3.7 Nginx本地缓存

4、缓存同步策略

4.1 数据同步策略

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

设置有效期 :给缓存设置有效期,到期后自动删除。再次查询时更新
优势:简单、方便
缺点:时效性差,缓存过期之前可能不一致
场景:更新频率较低,时效性要求低的业务
同步双写 :在修改数据库的同时,直接修改缓存
优势:时效性强,缓存与数据库强一致
缺点:有代码侵入,耦合度高;
场景:对一致性、时效性要求较高的缓存数据
异步通知: 修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
优势:低耦合,可以同时通知多个缓存服务
缺点:时效性一般,可能存在中间不一致状态
场景:时效性要求一般,有多个服务需要同步 

基于MQ的异步通知                                                                                 基于Canal的异步通知

4.2 安装Canal

Canal [kə'næl],译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费GitHub的地址:GitHub - alibaba/canal: 阿里巴巴 MySQL binlog 增量订阅&消费组件

Canal是基于mysql的主从同步来实现的,MySQL主从同步的原理如下:

MySQL master 将数据变更写入二进制日志 ( binary log ),其中记录的数据叫做 binary log events
MySQL slave master binary log events 拷贝到它的中继日志 (relay log)
MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

Canal就是把自己伪装成MySQL的一个slave节点,从而监听masterbinary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。 

 《安装Canal.md》

4.3 监听Canal

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值