0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

本地缓存的技术实践

科技绿洲 来源:Java技术指北 作者:Java技术指北 2023-09-30 15:29 次阅读

一、摘要

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

图片

操作步骤:

  • 1.当用户发起访问某数据的操作时,检查缓存服务里面是否存在,如果存在,直接返回;如果不存在,走数据库的查询服务
  • 2.从数据库里面获取到有效数据之后,存入缓存服务,并返回给用户
  • 3.当被访问的数据发生更新的时候,需要同时删除缓存服务,以便用户再次查询的时候,能获取到最新的数据

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

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

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

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

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

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

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

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

二、方案介绍

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

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

2.1、手写一个缓存服务

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

首先创建一个缓存实体类

public class CacheEntity {

    /**
     * 缓存键
     */
    private String key;

    /**
     * 缓存值
     */
    private Object value;

    /**
     * 过期时间
     */
    private Long expireTime;

    //...set、get
}

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

public class CacheUtils {

    /**
     * 缓存数据
     */
    private final static Map< String, CacheEntity > CACHE_MAP = new ConcurrentHashMap<  >();

    /**
     * 定时器线程池,用于清除过期缓存
     */
    private static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();


    static {
        // 注册一个定时线程任务,服务启动1秒之后,每隔500毫秒执行一次
        executor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                // 清理过期缓存
                clearCache();
            }
        },1000,500,TimeUnit.MILLISECONDS);
    }

    /**
     * 添加缓存
     * @param key    缓存键
     * @param value  缓存值
     */
    public static void put(String key, Object value){
        put(key, value, 0);
    }


    /**
     * 添加缓存
     * @param key    缓存键
     * @param value  缓存值
     * @param expire 缓存时间,单位秒
     */
    public static void put(String key, Object value, long expire){
        CacheEntity cacheEntity = new CacheEntity()
                .setKey(key)
                .setValue(value);
        if(expire > 0){
            Long expireTime = System.currentTimeMillis() + Duration.ofSeconds(expire).toMillis();
            cacheEntity.setExpireTime(expireTime);
        }
        CACHE_MAP.put(key, cacheEntity);
    }


    /**
     * 获取缓存
     * @param key
     * @return
     */
    public static Object get(String key){
        if(CACHE_MAP.containsKey(key)){
            return CACHE_MAP.get(key).getValue();
        }
        return null;
    }

    /**
     * 移除缓存
     * @param key
     */
    public static void remove(String key){
        if(CACHE_MAP.containsKey(key)){
            CACHE_MAP.remove(key);
        }
    }

    /**
     * 清理过期的缓存数据
     */
    private static void clearCache(){
        if(CACHE_MAP.size() > 0){
            return;
        }
        Iterator< Map.Entry< String, CacheEntity >> iterator = CACHE_MAP.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry< String, CacheEntity > entry = iterator.next();
            if(entry.getValue().getExpireTime() != null && entry.getValue().getExpireTime().longValue() > System.currentTimeMillis()){
                iterator.remove();
            }
        }
    }

}

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

// 写入缓存数据
CacheUtils.put("userName", "张三", 3);

// 读取缓存数据
Object value1 = CacheUtils.get("userName");
System.out.println("第一次查询结果:" + value1);

// 停顿4秒
Thread.sleep(4000);

// 读取缓存数据
Object value2 = CacheUtils.get("userName");
System.out.println("第二次查询结果:" + value2);

输出结果,与预期一致!

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

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

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

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

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

  • 支持最大容量限制
  • 支持两种过期删除策略(插入时间和读取时间)
  • 支持简单的统计功能
  • 基于 LRU 算法实现

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

< !--guava-- >
< dependency >
    < groupId >com.google.guava< /groupId >
    < artifactId >guava< /artifactId >
    < version >31.1-jre< /version >
< /dependency >

案例代码如下:

// 创建一个缓存实例
Cache< String, String > cache = CacheBuilder.newBuilder()
        // 初始容量
        .initialCapacity(5)
        // 最大缓存数,超出淘汰
        .maximumSize(10)
        // 过期时间
        .expireAfterWrite(3, TimeUnit.SECONDS)
        .build();

// 写入缓存数据
cache.put("userName", "张三");

// 读取缓存数据
String value1 = cache.get("userName", () - > {
    // 如果key不存在,会执行回调方法
    return "key已过期";
});
System.out.println("第一次查询结果:" + value1);

// 停顿4秒
Thread.sleep(4000);

// 读取缓存数据
String value2 = cache.get("userName", () - > {
    // 如果key不存在,会执行回调方法
    return "key已过期";
});
System.out.println("第二次查询结果:" + value2);

输出结果:

第一次查询结果:张三
第二次查询结果:key已过期

2.3、基于 Caffeine 实现本地缓存

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

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

< !--caffeine-- >
< dependency >
    < groupId >com.github.ben-manes.caffeine< /groupId >
    < artifactId >caffeine< /artifactId >
    < version >2.9.3< /version >
< /dependency >

案例代码如下:

// 创建一个缓存实例
Cache< String, String > cache = Caffeine.newBuilder()
        // 初始容量
        .initialCapacity(5)
        // 最大缓存数,超出淘汰
        .maximumSize(10)
        // 设置缓存写入间隔多久过期
        .expireAfterWrite(3, TimeUnit.SECONDS)
        // 设置缓存最后访问后间隔多久淘汰,实际很少用到
        //.expireAfterAccess(3, TimeUnit.SECONDS)
        .build();

// 写入缓存数据
cache.put("userName", "张三");

// 读取缓存数据
String value1 = cache.get("userName", (key) - > {
    // 如果key不存在,会执行回调方法
    return "key已过期";
});
System.out.println("第一次查询结果:" + value1);

// 停顿4秒
Thread.sleep(4000);

// 读取缓存数据
String value2 = cache.get("userName", (key) - > {
    // 如果key不存在,会执行回调方法
    return "key已过期";
});
System.out.println("第二次查询结果:" + value2);

输出结果:

第一次查询结果:张三
第二次查询结果:key已过期

2.4、基于 Encache 实现本地缓存

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

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

  • 支持多种缓存淘汰算法,包括 LRU、LFU 和 FIFO
  • 缓存支持堆内存储、堆外存储、磁盘存储(支持持久化)三种
  • 支持多种集群方案,解决数据共享问题

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

< !--ehcache-- >
< dependency >
    < groupId >org.ehcache< /groupId >
    < artifactId >ehcache< /artifactId >
    < version >3.9.7< /version >
< /dependency >

案例代码如下:

/**
 * 自定义过期策略实现
 */
public  class CustomExpiryPolicy< K, V > implements ExpiryPolicy< K, V > {

    private final Map< K, Duration > keyExpireMap = new ConcurrentHashMap();


    public Duration setExpire(K key, Duration duration) {
        return keyExpireMap.put(key, duration);
    }

    public Duration getExpireByKey(K key) {
        return Optional.ofNullable(keyExpireMap.get(key))
                .orElse(null);
    }

    public Duration removeExpire(K key) {
        return keyExpireMap.remove(key);
    }

    @Override
    public Duration getExpiryForCreation(K key, V value) {
        return Optional.ofNullable(getExpireByKey(key))
                .orElse(Duration.ofNanos(Long.MAX_VALUE));
    }

    @Override
    public Duration getExpiryForAccess(K key, Supplier< ? extends V > value) {
        return getExpireByKey(key);
    }

    @Override
    public Duration getExpiryForUpdate(K key, Supplier< ? extends V > oldValue, V newValue) {
        return getExpireByKey(key);
    }
}
public static void main(String[] args) throws InterruptedException {
    String userCache = "userCache";

    // 自定义过期策略
    CustomExpiryPolicy< Object, Object > customExpiryPolicy = new CustomExpiryPolicy<  >();

    // 声明一个容量为20的堆内缓存配置
    CacheConfigurationBuilder configurationBuilder = CacheConfigurationBuilder
            .newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(20))
            .withExpiry(customExpiryPolicy);

    // 初始化一个缓存管理器
    CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            // 创建cache实例
            .withCache(userCache, configurationBuilder)
            .build(true);

    // 获取cache实例
    Cache< String, String > cache = cacheManager.getCache(userCache, String.class, String.class);
    // 获取过期策略
    CustomExpiryPolicy expiryPolicy = (CustomExpiryPolicy)cache.getRuntimeConfiguration().getExpiryPolicy();

    // 写入缓存数据
    cache.put("userName", "张三");
    // 设置3秒过期
    expiryPolicy.setExpire("userName", Duration.ofSeconds(3));

    // 读取缓存数据
    String value1 = cache.get("userName");
    System.out.println("第一次查询结果:" + value1);

    // 停顿4秒
    Thread.sleep(4000);

    // 读取缓存数据
    String value2 = cache.get("userName");
    System.out.println("第二次查询结果:" + value2);
}

输出结果:

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

三、小结

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

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

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

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

图片

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

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

关于 redis 的使用,有兴趣的同学可以查看历史文章,之前有写过 redis 系列相关的技术实践介绍。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 数据
    +关注

    关注

    8

    文章

    7045

    浏览量

    89061
  • 服务器
    +关注

    关注

    12

    文章

    9176

    浏览量

    85460
  • 内存
    +关注

    关注

    8

    文章

    3026

    浏览量

    74065
  • 缓存
    +关注

    关注

    1

    文章

    240

    浏览量

    26681
  • 磁盘
    +关注

    关注

    1

    文章

    379

    浏览量

    25209
收藏 人收藏

    评论

    相关推荐

    高并发系统中的缓存 缓存系统存在的三大问题

    缓存在计算机系统是无处不在,在CPU层面有L1-L3的Cache,在Linux中有TLB加速虚拟地址和物理地址的转换,在浏览器有本地缓存、手机有本地
    的头像 发表于 07-15 11:03 4236次阅读

    LRU缓存模块最佳实践

    LRU(Least Recently Used)是一种缓存替换算法,它的核心思想是当缓存满时,替换最近最少使用的数据。在实际应用中,LRU算法被广泛应用于缓存、页面置换等领域。Rust语言提供了一个
    的头像 发表于 09-30 16:47 904次阅读

    如何选择合适的本地缓存

    小编最近在使用系统的时候,发现尽管应用已经使用了 redis 缓存提高查询效率,但是仍然有进一步优化的空间,于是想到了比分布式缓存性能更好的本地缓存,因此对领域内常用的
    的头像 发表于 01-18 11:19 852次阅读
    如何选择合适的<b class='flag-5'>本地</b><b class='flag-5'>缓存</b>?

    157.157、缓存 缓存使用 本地锁在分布式下的问题

    缓存
    充八万
    发布于 :2023年07月18日 04:44:59

    ASP缓存技术

    使用ASP中的缓存技术可以很大程度上提高你的网站性能,其实这些实现方法是非常的简单,它将说明如何在服务器上的缓存是如何工作以及你如何使用一种被称为断开连接的ADO连接技术。在介绍这些
    发表于 11-21 10:53

    内容中心网络中基于用户偏好的协作缓存策略

    用户本地偏好度指标,实现缓存内容的选择;然后,对需要缓存内容执行差异化缓存策略,全局活跃的内容则缓存在重要的中心节点,非活跃内容则按
    发表于 12-19 15:23 4次下载
    内容中心网络中基于用户偏好的协作<b class='flag-5'>缓存</b>策略

    缓存的基本原理 缓存的分类

    缓存的主要手段有:浏览器缓存、CDN、反向代理、本地缓存、分布式缓存、数据库缓存
    发表于 06-13 12:04 4752次阅读

    缓存技术的工作原理

    缓存系统有时也称为混合存储网关设备,以强调其作为本地和云基础架构之间桥梁的作用。正如该Gartner定义所指出,与其他嵌入式缓存一样,这些设备基于设备和应用程序拦截文件、块或对象存储I / O。
    的头像 发表于 08-10 11:51 2990次阅读

    ThingJS平台推出3D场景本地缓存技术

    为提升用户访问体验,缩短项目加载时间,ThingJS平台推出3D场景本地缓存技术:IndexedDB,也称客户端缓存持久化技术。通俗来说,I
    发表于 03-13 11:19 1820次阅读

    关于浏览器缓存最详细解析

    浏览器缓存即 http 缓存,将请求过的数据(html、css、js)存在浏览器(本地磁盘)中,当再次访问这些资源时可以从本地直接加载,减少服务端请求。
    的头像 发表于 04-16 16:01 2726次阅读

    聊聊本地缓存和分布式缓存

    本地缓存 :应用中的缓存组件,缓存组件和应用在同一进程中,缓存的读写非常快,没有网络开销。但各应用或集群的各节点都需要维护自己的单独
    发表于 06-11 15:12 836次阅读
    聊聊<b class='flag-5'>本地</b><b class='flag-5'>缓存</b>和分布式<b class='flag-5'>缓存</b>

    如何在 Linux 上查看本地 DNS 缓存

      刷新本地 DNS 缓存可以解决 HTTP 错误并保护您免受 DNS 欺骗。以下是在 Linux 上执行此操作的方法。 当您使用域名访问网站时,您的系统会向 DNS 服务器发送请求以获取该域
    的头像 发表于 06-26 10:52 3350次阅读
    如何在 Linux 上查看<b class='flag-5'>本地</b> DNS <b class='flag-5'>缓存</b>

    Ehcache!这才是Java本地缓存之王!

    就Java而言,其常用的缓存解决方案有很多,例如数据库缓存框架EhCache,分布式缓存Memcached等,这些缓存方案实际上都是为了提升吞吐效率,避免持久层压力过大。
    的头像 发表于 07-29 11:21 1815次阅读
    Ehcache!这才是Java<b class='flag-5'>本地</b><b class='flag-5'>缓存</b>之王!

    缓存之美——如何选择合适的本地缓存

    Guava cache是Google开发的Guava工具包中一套完善的JVM本地缓存框架,底层实现的数据结构类似于ConcurrentHashMap,但是进行了更多的能力拓展,包括缓存过期时间设置、
    的头像 发表于 11-17 14:24 302次阅读
    <b class='flag-5'>缓存</b>之美——如何选择合适的<b class='flag-5'>本地</b><b class='flag-5'>缓存</b>?

    HTTP缓存头的使用 本地缓存与远程缓存的区别

    HTTP缓存头是一组HTTP响应头,它们控制浏览器和中间代理服务器如何缓存网页内容。合理使用HTTP缓存头可以显著提高网站的加载速度和性能,减少服务器的负载。 1. HTTP缓存头概述
    的头像 发表于 12-18 09:41 112次阅读