传统缓存的问题

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

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

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

多级缓存_数据库

多级缓存方案

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

多级缓存_代码优化_02

JVM进程缓存

本地进程缓存

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

●分布式缓存,例如Redis:

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

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

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

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

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

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

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

Caffeine

Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址: https://github.com/ben-manes/caffeine/wiki/Home-zh-CN

多级缓存_多级缓存_03

Caffeine简单测试
public class CaffeineTest {

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

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

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

        // 取数据,不存在则去数据库查询
        String defaultGF = cache.get("defaultGF", key -> {
            // 这里可以去数据库根据 key查询value
            return "柳岩";
        });
        System.out.println("defaultGF = " + defaultGF);
    }

    /*
     基于大小设置驱逐策略:
     */
    @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)) // 设置缓存有效期为 10 秒
        .build();
        // 存数据
        cache.put("gf", "柳岩");
        // 获取数据
        System.out.println("gf: " + cache.getIfPresent("gf"));
        // 休眠一会儿
        Thread.sleep(1200L);
        System.out.println("gf: " + cache.getIfPresent("gf"));
    }
}
Caffeine提供了三种缓存驱逐策略:

●基于容量:设置缓存的数量.上限

//创建缓存对象
Cache<String, String> cache = Caffeine .newBuilder()
. maximumSize(1) //设置缓存大小上限为1
. build() ;

●基于时间:设置缓存的有效时间

//创建缓存对象
Cache<String,String> cache = Caffeine .newBuilder()
. expireAfterWrite (Durati on. ofSeconds(10)) //设置缓存有效期为10秒,从最后一次写入开始计时
.build() ;

●基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

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

案例

多级缓存_代码优化_04

添加缓存配置类

Configuration
public class CacheConfig {

    /**
     * 创建一个缓存对象,用于存储商品信息
     * 缓存初始容量为100,最大容量为10000
     *
     * @return 返回Cache<Long, Item>类型的缓存对象
     */
    @Bean
    public Cache<Long, Item> itemCache() {
        return Caffeine.newBuilder()
        .initialCapacity(100)
        .maximumSize(10_000)
        .build();
    }

    /**
     * 创建一个缓存对象,用于存储商品库存信息
     * 缓存初始容量为100,最大容量为10000
     *
     * @return 返回Cache<Long, ItemStock>类型的缓存对象
     */
    @Bean
    public Cache<Long, ItemStock> stockCache() {
        return Caffeine.newBuilder()
        .initialCapacity(100)
        .maximumSize(10_000)
        .build();
    }
}

测试代码

@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));
}

第一次查询访问了数据库

多级缓存_缓存_05

第二次查询没有访问数据库

多级缓存_缓存_06

多级缓存_redis_07