• 浅谈本地缓存的几种方案选型


    一、摘要

    说到缓存,面试官基本上会绕不开以下几个话题!

    项目中哪些地方用到了缓存?为什么要使用缓存?怎么使用它的?引入缓存后会带来哪些问题?

    这些问题,基本上是互联网公司面试时必问的一些问题,如果面试的时候,连缓存都不清楚,那确实多少显的有些尴尬!

    项目里面为什么要引入缓存?这个问题还得结合项目中的业务来回答!

    引入缓存,其实主要有两个用途:高性能高并发

    假设某个操作非常频繁,比如网站的商城首页,需要频繁的从数据库里面获取商品数据,可能从数据库一顿各种乱七八糟的操作下来,平均耗时 500 ms,随着请求频次越高,用户等待数据的返回结果时间越来越长,体验越来越差。

    如果此时,引入缓存,将数据库里面查询出来的商品数据信息,放入缓存服务里面,当用户再此发起查询操作的时候,直接从缓存服务里面获取,速度从耗时 500 ms,可能直接优化成 5 ms,体验上瞬间会上升好几个层次!

    这就是引入缓存带来的高性能体验结果

    当然,除此之外,引入缓存之前,以 mysql 数据库为例,单台机器一秒内的请求次数到达 2000 之后就会开始报警;引入缓存之后,比如以 redis 缓存服务器为例,单台机器一秒内的请求次数支持 110000 次,两者支持的并发量完全不是一个数量级的。

    这就是引入缓存带来的高并发体验结果

    尤其是对于流量很大的业务,引入缓存,给系统带来的提升是十分显著的

    可能有的同学又会发出疑问,缓存和数据库为啥差距这么大,有啥区别?

    我们都知道在计算机领域,数据的存储主要有两处:一处是内存,另一处是磁盘

    在计算机中,内存的数据读写性能远超磁盘的读写性能,尽管如此,其实两者也有不同,如果数据存储到内存中,虽然读写性能非常高,但是当电脑重启之后,数据会全部清除;而存入磁盘的数据,虽然读写性能很差,但是电脑重启之后数据不会丢失。

    因为两者的数据存储方案不同,造就了不同的实践用途

    我们上面讲到的缓存服务,其实本质就是将数据存储到内存中;而数据库服务,是将数据写入到磁盘,从磁盘中读取数据。

    无论是哪种方案,没有绝对的好与坏,主要还是取决于实际的业务用途。

    在项目中如何引入缓存呢?我们通常的做法如下:

    操作步骤:

    • 1.当用户发起访问某数据的操作时,检查缓存服务里面是否存在,如果存在,直接返回;如果不存在,走数据库的查询服务

    • 2.从数据库里面获取到有效数据之后,存入缓存服务,并返回给用户

    • 3.当被访问的数据发生更新的时候,需要同时删除缓存服务,以便用户再次查询的时候,能获取到最新的数据

    当然以上的缓存处理办法,对于简单的需要缓存的业务场景,能轻松应对。

    但是面对复杂的业务场景和服务架构,尤其是对缓存要求比较高的业务,引入缓存的方式,也会跟着一起变化!

    从缓存面向的对象不同,缓存分为:本地缓存分布式缓存多级缓存

    所谓本地缓存,相信大家都能理解,在单个计算机服务实例中,直接把数据缓存到内存中进行使用。

    但是现在的服务,大多都是以集群的方式来部署,你也可以这样理解,同一个网站服务,同时在两台计算机里面部署,比如你用到的session会话,就无法同时共享,因此需要引入一个独立的缓存服务来连接两台服务器,这个独立部署的缓存服务,我们把这种技术实践方案称为分布式缓存

    在实际的业务中,本地缓存分布式缓存会同时结合进行使用,当收到访问某个数据的操作时,会优先从本地缓存服务(也叫一级缓存)查询,如果没有,再从分布式缓存服务(也叫二级缓存)里面获取,如果也没有,最后再从数据库里面获取;从数据库查询完成之后,在依次更新分布式缓存服务、本次缓存服务,我们把这个技术实践方案叫多级缓存

    由于篇幅的原因,我们在后期给大家介绍分布式缓存服务多级缓存服务

    今天主要围绕本地缓存服务的技术实现,给大家进行分享和介绍!

    二、方案介绍

    如果使用过缓存的同学,可以很容易想到缓存需要哪些东西,通常我们在使用缓存的时候,比较关注两个地方,第一是内存持久化,第二是支持缓存的数据自动过期清楚。

    基于以上的要求,我们向介绍以下几种技术实现方案。

    2.1、手写一个缓存服务

    对于简单的数据缓存,我们完全可以自行编写一套缓存服务,实现过程如下!

    首先创建一个缓存实体类

    1. public class CacheEntity {
    2.     /**
    3.      * 缓存键
    4.      */
    5.     private String key;
    6.     /**
    7.      * 缓存值
    8.      */
    9.     private Object value;
    10.     /**
    11.      * 过期时间
    12.      */
    13.     private Long expireTime;
    14.     //...setget
    15. }

    接着,编写一个缓存操作工具类CacheUtils

    1. public class CacheUtils {
    2.     /**
    3.      * 缓存数据
    4.      */
    5.     private final static Map<String, CacheEntity> CACHE_MAP = new ConcurrentHashMap<>();
    6.     /**
    7.      * 定时器线程池,用于清除过期缓存
    8.      */
    9.     private static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    10.     static {
    11.         // 注册一个定时线程任务,服务启动1秒之后,每隔500毫秒执行一次
    12.         executor.scheduleAtFixedRate(new Runnable() {
    13.             @Override
    14.             public void run() {
    15.                 // 清理过期缓存
    16.                 clearCache();
    17.             }
    18.         },1000,500,TimeUnit.MILLISECONDS);
    19.     }
    20.     /**
    21.      * 添加缓存
    22.      * @param key    缓存键
    23.      * @param value  缓存值
    24.      */
    25.     public static void put(String keyObject value){
    26.         put(keyvalue0);
    27.     }
    28.     /**
    29.      * 添加缓存
    30.      * @param key    缓存键
    31.      * @param value  缓存值
    32.      * @param expire 缓存时间,单位秒
    33.      */
    34.     public static void put(String keyObject value, long expire){
    35.         CacheEntity cacheEntity = new CacheEntity()
    36.                 .setKey(key)
    37.                 .setValue(value);
    38.         if(expire > 0){
    39.             Long expireTime = System.currentTimeMillis() + Duration.ofSeconds(expire).toMillis();
    40.             cacheEntity.setExpireTime(expireTime);
    41.         }
    42.         CACHE_MAP.put(key, cacheEntity);
    43.     }
    44.     /**
    45.      * 获取缓存
    46.      * @param key
    47.      * @return
    48.      */
    49.     public static Object get(String key){
    50.         if(CACHE_MAP.containsKey(key)){
    51.             return CACHE_MAP.get(key).getValue();
    52.         }
    53.         return null;
    54.     }
    55.     /**
    56.      * 移除缓存
    57.      * @param key
    58.      */
    59.     public static void remove(String key){
    60.         if(CACHE_MAP.containsKey(key)){
    61.             CACHE_MAP.remove(key);
    62.         }
    63.     }
    64.     /**
    65.      * 清理过期的缓存数据
    66.      */
    67.     private static void clearCache(){
    68.         if(CACHE_MAP.size() > 0){
    69.             return;
    70.         }
    71.         Iterator<Map.Entry<String, CacheEntity>> iterator = CACHE_MAP.entrySet().iterator();
    72.         while (iterator.hasNext()){
    73.             Map.Entry<String, CacheEntity> entry = iterator.next();
    74.             if(entry.getValue().getExpireTime() != null && entry.getValue().getExpireTime().longValue() > System.currentTimeMillis()){
    75.                 iterator.remove();
    76.             }
    77.         }
    78.     }
    79. }

    最后,我们来测试一下缓存服务

    1. // 写入缓存数据
    2. CacheUtils.put("userName""张三"3);
    3. // 读取缓存数据
    4. Object value1 = CacheUtils.get("userName");
    5. System.out.println("第一次查询结果:" + value1);
    6. // 停顿4
    7. Thread.sleep(4000);
    8. // 读取缓存数据
    9. Object value2 = CacheUtils.get("userName");
    10. System.out.println("第二次查询结果:" + value2);

    输出结果,与预期一致!

    1. 第一次查询结果:张三
    2. 第二次查询结果:null

    实现思路其实很简单,采用ConcurrentHashMap作为缓存数据存储服务,然后开启一个定时调度,每隔500毫秒检查一下过期的缓存数据,然后清除掉!

    2.2、基于 Guava Cache 实现本地缓存

    Guava 是 Google 团队开源的一款 Java 核心增强库,包含集合、并发原语、缓存、IO、反射等工具箱,性能和稳定性上都有保障,应用十分广泛。

    相比自己编写的缓存服务,Guava Cache 要强大的多,支持很多特性如下:

    • 支持最大容量限制

    • 支持两种过期删除策略(插入时间和读取时间)

    • 支持简单的统计功能

    • 基于 LRU 算法实现

    使用方面也很简单,首先引入guava库包。

    1. <!--guava-->
    2. <dependency>
    3.     <groupId>com.google.guava</groupId>
    4.     <artifactId>guava</artifactId>
    5.     <version>31.1-jre</version>
    6. </dependency>

    案例代码如下:

    1. // 创建一个缓存实例
    2. Cache<StringString> cache = CacheBuilder.newBuilder()
    3.         // 初始容量
    4.         .initialCapacity(5)
    5.         // 最大缓存数,超出淘汰
    6.         .maximumSize(10)
    7.         // 过期时间
    8.         .expireAfterWrite(3, TimeUnit.SECONDS)
    9.         .build();
    10. // 写入缓存数据
    11. cache.put("userName""张三");
    12. // 读取缓存数据
    13. String value1 = cache.get("userName", () -> {
    14.     // 如果key不存在,会执行回调方法
    15.     return "key已过期";
    16. });
    17. System.out.println("第一次查询结果:" + value1);
    18. // 停顿4
    19. Thread.sleep(4000);
    20. // 读取缓存数据
    21. String value2 = cache.get("userName", () -> {
    22.     // 如果key不存在,会执行回调方法
    23.     return "key已过期";
    24. });
    25. System.out.println("第二次查询结果:" + value2);

    输出结果:

    1. 第一次查询结果:张三
    2. 第二次查询结果:key已过期
    2.3、基于 Caffeine 实现本地缓存

    Caffeine 是基于 java8 实现的新一代缓存工具,缓存性能接近理论最优,可以看作是 Guava Cache 的增强版,功能上两者类似,不同的是 Caffeine 采用了一种结合 LRU、LFU 优点的算法:W-TinyLFU,在性能上有明显的优越性。

    使用方面也很简单,首先引入caffeine库包。

    1. <!--caffeine-->
    2. <dependency>
    3.     <groupId>com.github.ben-manes.caffeine</groupId>
    4.     <artifactId>caffeine</artifactId>
    5.     <version>2.9.3</version>
    6. </dependency>

    案例代码如下:

    1. // 创建一个缓存实例
    2. Cache<StringString> cache = Caffeine.newBuilder()
    3.         // 初始容量
    4.         .initialCapacity(5)
    5.         // 最大缓存数,超出淘汰
    6.         .maximumSize(10)
    7.         // 设置缓存写入间隔多久过期
    8.         .expireAfterWrite(3, TimeUnit.SECONDS)
    9.         // 设置缓存最后访问后间隔多久淘汰,实际很少用到
    10.         //.expireAfterAccess(3, TimeUnit.SECONDS)
    11.         .build();
    12. // 写入缓存数据
    13. cache.put("userName""张三");
    14. // 读取缓存数据
    15. String value1 = cache.get("userName", (key) -> {
    16.     // 如果key不存在,会执行回调方法
    17.     return "key已过期";
    18. });
    19. System.out.println("第一次查询结果:" + value1);
    20. // 停顿4
    21. Thread.sleep(4000);
    22. // 读取缓存数据
    23. String value2 = cache.get("userName", (key) -> {
    24.     // 如果key不存在,会执行回调方法
    25.     return "key已过期";
    26. });
    27. System.out.println("第二次查询结果:" + value2);

    输出结果:

    1. 第一次查询结果:张三
    2. 第二次查询结果:key已过期
    2.4、基于 Encache 实现本地缓存

    Encache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。

    同 Caffeine 和 Guava Cache 相比,Encache 的功能更加丰富,扩展性更强,特性如下:

    • 支持多种缓存淘汰算法,包括 LRU、LFU 和 FIFO

    • 缓存支持堆内存储、堆外存储、磁盘存储(支持持久化)三种

    • 支持多种集群方案,解决数据共享问题

    使用方面也很简单,首先引入ehcache库包。

    1. <!--ehcache-->
    2. <dependency>
    3.     <groupId>org.ehcache</groupId>
    4.     <artifactId>ehcache</artifactId>
    5.     <version>3.9.7</version>
    6. </dependency>

    案例代码如下:

    1. /**
    2.  * 自定义过期策略实现
    3.  */
    4. public  class CustomExpiryPolicy<K, V> implements ExpiryPolicy<K, V> {
    5.     private final Map<K, Duration> keyExpireMap = new ConcurrentHashMap();
    6.     public Duration setExpire(K key, Duration duration) {
    7.         return keyExpireMap.put(key, duration);
    8.     }
    9.     public Duration getExpireByKey(K key) {
    10.         return Optional.ofNullable(keyExpireMap.get(key))
    11.                 .orElse(null);
    12.     }
    13.     public Duration removeExpire(K key) {
    14.         return keyExpireMap.remove(key);
    15.     }
    16.     @Override
    17.     public Duration getExpiryForCreation(K key, V value) {
    18.         return Optional.ofNullable(getExpireByKey(key))
    19.                 .orElse(Duration.ofNanos(Long.MAX_VALUE));
    20.     }
    21.     @Override
    22.     public Duration getExpiryForAccess(K key, Supplier<? extends V> value) {
    23.         return getExpireByKey(key);
    24.     }
    25.     @Override
    26.     public Duration getExpiryForUpdate(K key, Supplier<? extends V> oldValue, V newValue) {
    27.         return getExpireByKey(key);
    28.     }
    29. }
    1. public static void main(String[] args) throws InterruptedException {
    2.     String userCache = "userCache";
    3.     // 自定义过期策略
    4.     CustomExpiryPolicy<ObjectObject> customExpiryPolicy = new CustomExpiryPolicy<>();
    5.     // 声明一个容量为20的堆内缓存配置
    6.     CacheConfigurationBuilder configurationBuilder = CacheConfigurationBuilder
    7.             .newCacheConfigurationBuilder(String.classString.class, ResourcePoolsBuilder.heap(20))
    8.             .withExpiry(customExpiryPolicy);
    9.     // 初始化一个缓存管理器
    10.     CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
    11.             // 创建cache实例
    12.             .withCache(userCache, configurationBuilder)
    13.             .build(true);
    14.     // 获取cache实例
    15.     Cache<StringString> cache = cacheManager.getCache(userCache, String.classString.class);
    16.     // 获取过期策略
    17.     CustomExpiryPolicy expiryPolicy = (CustomExpiryPolicy)cache.getRuntimeConfiguration().getExpiryPolicy();
    18.     // 写入缓存数据
    19.     cache.put("userName""张三");
    20.     // 设置3秒过期
    21.     expiryPolicy.setExpire("userName", Duration.ofSeconds(3));
    22.     // 读取缓存数据
    23.     String value1 = cache.get("userName");
    24.     System.out.println("第一次查询结果:" + value1);
    25.     // 停顿4
    26.     Thread.sleep(4000);
    27.     // 读取缓存数据
    28.     String value2 = cache.get("userName");
    29.     System.out.println("第二次查询结果:" + value2);
    30. }

    输出结果:

    1. 第一次查询结果:张三
    2. 第二次查询结果:null

    三、小结

    从易用性角度看:Guava Cache、Caffeine 和 Encache 都有十分成熟的接入方案,使用简单。

    从功能性角度看:Guava Cache 和 Caffeine 功能类似,都是只支持堆内缓存,Encache 相比功能更为丰富,不仅支持堆内缓存,还支持磁盘写入、集群实现。

    从性能角度看:Caffeine 最优、GuavaCache 次之,Encache 最差。

    以下是网络上三者性能对比的结果。

    对于本地缓存的技术选型,推荐采用 Caffeine,性能上毫无疑问,遥遥领先。

    虽然 Ehcache 功能非常的丰富,甚至提供了持久化和集群的功能,但是相比更成熟的分布式缓存中间件 redis 来说,还是稍逊一些!

  • 相关阅读:
    Docker 配置国内镜像加速器
    c高级 day1
    Java-微服务-谷粒商城-1-环境搭建&项目初始化
    (未完待续)【Netty专题】Netty实战与核心组件详解
    Hadoop原理讲解(面试题)
    用busybox构建最小根文件系统详解
    跟着我学 AI丨知识图谱,搜索的根
    巧用h2-database.jar连接数据库
    前篇 + 入门
    头歌计算机组成原理汉字字库存储芯片扩展实验
  • 原文地址:https://blog.csdn.net/H_Sino/article/details/138175130