HM-SpringCloud微服务系列11.1【多级缓存的意义&JVM进程缓存】

HM-SpringCloud微服务系列11:多级缓存-高级篇

1. 什么是多级缓存

image
image

  1. 多级缓存是亿级流量的缓存方案
    • 浏览器访问静态资源时,优先读取浏览器本地缓存
    • 访问非静态资源(ajax查询数据)时,访问服务端
    • 请求到达Nginx后,优先读取Nginx本地缓存
    • 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)
    • 如果Redis查询未命中,则查询Tomcat
    • 请求进入Tomcat后,优先查询JVM进程缓存
    • 如果JVM进程缓存未命中,则查询数据库
  2. 在多级缓存架构中,Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑,因此这样的nginx服务不再是一个反向代理服务器,而是一个编写业务的Web服务器了
  3. 因此这样的业务Nginx服务也需要搭建集群来提高并发,再有专门的nginx服务来做反向代理,如图:
    image

    image
  4. 另外,我们的Tomcat服务将来也会部署为集群模式,如图:
    image
  5. 可见,多级缓存的关键有两个:
    • 一个是在nginx中编写业务,实现nginx本地缓存、Redis、Tomcat的查询
    • 另一个就是在Tomcat中实现JVM进程缓存
  6. 其中Nginx编程则会用到OpenResty框架结合Lua这样的语言。

2. JVM进程缓存

image

2.1 导入商品管理案例

详见https://www.cnblogs.com/yppah/p/16212294.html

2.2 初识Caffeine

2.2.1 本地进程缓存

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

    • 分布式缓存,例如Redis:
      • 优点:存储容量更大、可靠性更好、可以在集群间共享
      • 缺点:访问缓存有网络开销
      • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
    • 进程本地缓存,例如HashMap、GuavaCache:
      • 优点:读取本地内存,没有网络开销,速度更快
      • 缺点:存储容量有限、可靠性较低、无法共享
      • 场景:性能要求较高,缓存数据量较小
  2. Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址:https://github.com/ben-manes/caffeine
    Caffeine的性能非常好,下图是官方给出的性能对比:
    image
    可以看到Caffeine的性能遥遥领先,所以课程学习利用Caffeine框架来实现JVM进程缓存

  3. https://github.com/ben-manes/caffeine/wiki/Home-zh-CN
    image

2.2.2 Caffeine示例

  1. 可以通过item-service项目中的单元测试来学习Caffeine的使用,以下是缓存使用的基本API:
    @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);
    }
    
  2. Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。Caffeine提供了三种缓存驱逐策略:
    • 基于容量:设置缓存的数量上限
      // 创建缓存对象
      Cache<String, String> cache = Caffeine.newBuilder()
      	.maximumSize(1) // 设置缓存大小上限为 1
      	.build();
      
    • 基于时间:设置缓存的有效时间
      // 创建缓存对象
      Cache<String, String> cache = Caffeine.newBuilder()
      	// 设置缓存有效期为 10 秒,从最后一次写入开始计时 
      	.expireAfterWrite(Duration.ofSeconds(10)) 
      	.build();
      
    • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。

2.2.3 Caffeine入门

/*
     基本用法测试
*/
@Test
void testBasicOps() {
	// 创建缓存对象
	Cache<String, String> cache = Caffeine.newBuilder().build();
	// 存数据
	cache.put("gf", "谭越");

	// 取数据1
	String gf = cache.getIfPresent("gf"); //getIfPresent()不存在则返回null
	System.out.println("gf = " + gf);

	// 取数据2
	// get()用key="defaultGF"去JVM缓存找数据,有就返回;不存在则去数据库查询
	String defaultGF = cache.get("defaultGF", key -> {
		// 这里可以去数据库根据 key查询value,进而返回
		return "ty"; //假数据,此处上面应写查询数据库的逻辑
	});
	System.out.println("defaultGF = " + defaultGF);
}

image

/*
     基于大小设置驱逐策略:
*/
@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"));
}

image
image
可以看到,在给线程清理足够时间后,前两个key的值被清空了,因为最大上限为1

/*
     基于时间设置驱逐策略:
*/
@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); //1.2秒-->缓存会被清空
	System.out.println("gf: " + cache.getIfPresent("gf"));
}

image

2.3 实现JVM进程缓存

2.3.1 需求

利用Caffeine实现下列需求:

  • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
  • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
  • 缓存初始大小为100
  • 缓存上限为10000

2.3.2 实现

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

package com.heima.item.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<Long, Item> itemCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000) //实际上就是10000,_作用是方便读
                .build();
    }

    @Bean
    public Cache<Long, ItemStock> stockCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000) //实际上就是10000,_作用是方便读
                .build();
    }
}

image
【第二步】
然后,修改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));
    }
}

image
image
【第三步】
重启服务,访问测试
image
清空启动日志
访问http://localhost:8081/item/10001
image
image
可以看到控制台输出了查询SQL,现在再清空一下日志
image
然后,刷新http://localhost:8081/item/10001页面
image
image
发现这次访问,并没有打印SQL,即走了JVM缓存而没有去查数据库
PS:第一次访问时会访问数据库,所以会打印SQL,并且第一次访问后会将数据放入JVM缓存中


访问http://localhost:8081/item/stock/10001
image
image
image
image
image

posted @   yub4by  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示