掘金 后端 ( ) • 2024-04-26 14:07

二级缓存的优势与缺点

优点:

1)二级缓存相比只调用一层 Redis 缓存,访问速度更快。对于一些不经常修改的数据而查询十分频繁的可以直接放在本地缓存(一级)里面。

作为面试者的扩展延伸:我在本地缓存的实现中,我使用到了本地缓存性能之王 Caffeine 作为一级缓存,在市面上很多像 Redisson、Druid、Hbase 等知名开源项目都用到了 Caffeine 。它实现了更加好用的缓存淘汰算法 W-TinyLFU 算法,结合了 LRU(最近最久未使用算法) 算法以及 LFU(最少使用算法) 算法的优点,所以选择它能使本地缓存的使用更加方便快速。

2)使用了本地缓存相比直接去 Redis 中取,能够减少与远程 Redis 的数据 I/O 网络交互,降低了网络之间的消耗。

缺点:

1)增加了本地缓存对于一致性的维护更加复杂,提高了维护成本。

2)在分布式环境下,如何解决各个节点本地缓存一致性问题?使用类 Redis 的发布订阅功能,当一个节点的数据发生修改时,直接通知其他节点的缓存进行更新。

是不是已经初步了解了二级缓存的应用咧~ 下来先带你实现一下二级缓存。

image-20240426105159466

简单实现

1)引入依赖

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2)配置 Caffeine

@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<String,Object> caffeineCache(){
        return Caffeine.newBuilder()
                .initialCapacity(128)//初始大小
                .maximumSize(1024)//最大数量
                .expireAfterWrite(60, TimeUnit.SECONDS)//过期时间
                .build();
    }
}

3)使用二级缓存

@Resource
private Cache<String, Object> cache;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Test
void testCache() {
    // 缓存测试存取
    cache.put("test", "test");
    assertEquals("test", cache.getIfPresent("test"));
​
    //实现二级缓存读取
    cache.get("test",
              k -> {
                  //先查询 Redis
                  Object obj = redisTemplate.opsForValue().get(k);
                  if (Objects.nonNull(obj)) {
                      System.out.println("缓存命中:" + k + " 从 Redis 读取成功");
                      return obj;
                  }
​
                  // Redis没有则查询 DB
                  System.out.println("缓存没有命中:" + k + " 从 DB 读取");
                  // 从 DB 读取 ..此处模拟省略
                  obj = "test";
                  // 存入 Redis
                  redisTemplate.opsForValue().set(k, "test");
                  //存入本地缓存由cache.get执行
                  return obj;
              });
​
}

这样一个缓存简单的二级缓存使用就是这样啦,但这样是不是感觉对代码的侵入性有点强了,使用起来不够灵活,下面再来带大家使用 Spring 提供的 CacheManager 接口以及注解来实现它。

image-20240426101016819

使用 Spring 的 CacheManager 实现

1)引入依赖

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2)配置 CacheManager

@Configuration
public class CacheManagerConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager=new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(128)
                .maximumSize(1024)
                .expireAfterWrite(60, TimeUnit.SECONDS));
        return cacheManager;
    }
}

3)在启动类上面加上 @EnableCaching 注解

@SpringBootApplication()
@EnableCaching
public class Application {
​
}

4)使用二级缓存查询

@Resource
private RedisTemplate<String,Object> redisTemplate;
@Cacheable(value = "test",key = "#id")
public String getStringTextById(Long id) {
    String key= "test" + id;
    //先查询 Redis
    String obj = (String) redisTemplate.opsForValue().get(key);
    if (Objects.nonNull(obj)){
        log.info("从Redis中获取数据");
        return obj;
    }
    // Redis没有则查询 DB
    log.info("从DB中获取数据");
    //此处省略查询DB的代码
    obj = "test";
    //redis 中存入
    redisTemplate.opsForValue().set(key,obj,120, TimeUnit.SECONDS);
    return obj;
}

这里要注意的是!

1)@Cacheable 注解的 value 跟 cacheNames 是互为别名的关系,表示当前方法的结果被缓存到哪个 cache 上面,它可以是一个数组绑定多个 cache

2)@Cacheable 注解的 key 用来指定返回结果的 key,这个属性可以支持 SpringEL 表达式。

SpringEL表达式如下:

#参数名
#参数对象.属性名
#参数为数组对应下标

5)使用二级缓存更新数据

@CachePut(cacheNames = "test",key = "#id")
public String updateOrder(String testData, Long id) {
    log.info("更新数据");
    //更新数据库 此处省略
    //修改 Redis
    redisTemplate.opsForValue().set("test" + id,
                                    testData, 120, TimeUnit.SECONDS);
    //返回要缓存本地的数据
    return testData;
}

这里要注意的是!需要返回对象或值,因为要进行本地缓存操作。

6)使用二级缓存删除数据

@CacheEvict(cacheNames = "test",key = "#id")
public void deleteOrder(Long id) {
    log.info("删除数据");
    //此处省略删除数据库的代码
    //删除 Redis
    redisTemplate.delete("test" + id);
}

在这里简单的使用就到这里啦!还有更加优雅的方式大家可以去实现一下,通过 AOP 去实现二级缓存~

image-20240426103353051

二级缓存抛砖引玉

在上面跟大家一起谈了谈二级缓存的实现以及使用,这里我们可以跟面试官再次周旋一下!我曾经在阅读 Spring 源码时我还了解到了 Spring 的三级缓存实现。

在 Spring 框架中,循环依赖是指两个或多个 Bean 之间相互依赖,形成一个循环引用的情况。这种情况下,Spring IOC 容器在实例化 Bean 时可能会出现问题,因为它无法决定应该首先实例化哪个 Bean。为了解决这个问题,Spring 引入了三级缓存。

三级缓存是指 Spring IOC 容器中用于解决循环依赖问题的一种机制,它包含三个缓存阶段:

1)singletonObjects:这是 Spring IOC 容器的一级缓存,用于存储已经完全创建并初始化的 Bean实例。当 Bean 完全创建后,它会被放置在这个缓存中。

2)earlySingletonObjects:这是 Spring IOC 容器的二级缓存,用于存储提前暴露的、尚未完全初始化的 Bean 实例。当 Bean 正在被创建但尚未完成初始化时,它会被放置在这个缓存中。

3)singletonFactories:这是 Spring IOC 容器的三级缓存,用于存储 Bean 的工厂对象。在创建Bean 实例时,Spring 首先会在这个缓存中查找工厂对象,如果找到则使用该工厂对象来创建 Bean 实例。

三级缓存的作用

三级缓存的作用是为了解决循环依赖时的初始化顺序问题。在初始化 Bean 时,Spring 会首先将 Bean 的实例放入三级缓存中,然后进行属性注入等操作。如果发现循环依赖,Spring 会在二级缓存中查找对应的 Bean 实例,如果找到则直接返回,否则会调用Bean的工厂方法来创建 Bean 实例,并将其放入二级缓存中。当 Bean 实例化完成后,会从二级缓存中移除,并放入一级缓存中。

为什么需要三级缓存而不是二级缓存

二级缓存只存储了尚未初始化完成的 Bean 实例,而三级缓存存储了 Bean 的工厂对象。这样做的好处是,当发现循环依赖时,可以通过 Bean 的工厂对象来创建 Bean 实例,从而避免了直接从二级缓存中获取可能尚未完成初始化的 Bean 实例而导致的问题。因此,三级缓存提供了更加灵活和可靠的解决方案,能够更好地处理循环依赖问题。