前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >多级缓存(浏览器+nginx+redis+jvm)解决并发下的难题

多级缓存(浏览器+nginx+redis+jvm)解决并发下的难题

原创
作者头像
小草飞上天
发布于 2024-12-27 06:51:58
发布于 2024-12-27 06:51:58
32800
代码可运行
举报
文章被收录于专栏:java学习java学习
运行总次数:0
代码可运行

概述

在高并发系统中,需要一些特殊的机制来保障系统的稳定、高效。缓存则是系统高效运行的重中之重。

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

今天我们主要聊一聊多级缓存那些事。

传统缓存的问题

在传统的缓存策略。我们以java为例,当然其他的语言也是类似的。

在java中策略一般是请求到达Tomcat后,在应用程序中先去查询Redis,如果命中则返回,未命中则继续查询数据库。

那么这种策略会有什么问题呢?

  1. 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
  2. 当Redis缓存失效时,会对数据库产生冲击

所以在这种情况下,我们要增加多级别缓存,防止应用服务的冲击。

多级缓存是哪些级别的缓存?

多级缓存的目的就是充分利用一个请求中的各个环节,在每个环节都分别添加缓存,以减少打到应用的流量,提升服务性能。

多级缓存主要分为以下几点,主要包括于:

  1. 浏览器缓存:它的实现主要依靠 HTTP 协议中的缓存机制,当浏览器第一次请求一个资源时,服务器会将该资源的相关缓存规则(如 Cache-Control、Expires 等)一同返回给客户端,浏览器会根据这些规则来判断是否需要缓存该资源以及该资源的有效期。
  2. Nginx 缓存:在Nginx 配置中开启缓存功能,通过share_dict 共享数据。
  3. 分布式缓存:一般的分布式缓存,如 Redis、MemCached 等。
  4. 本地缓存:JVM 层面,应用系统在运行期间,应用系统独立在内存中产生的缓存,例如 Caffeine、Google Guava 等。

浏览器缓存

目前大部分的浏览器本身也会有缓存。但也可以通过后台响应针对性的进行一些缓存。我们这里不过多的赘述。简单给一些响应demo

配置 Cache-Control 控制缓存策略
代码语言:txt
AI代码解释
复制
response.setHeader("Cache-Control", "max-age=3600, public"); // 缓存一小时
配置 Expires 过期时间
代码语言:txt
AI代码解释
复制
response.setDateHeader("Expires", System.currentTimeMillis() + 3600 * 1000); // 缓存一小时
配置 Last-Modified 最后修改时间
代码语言:txt
AI代码解释
复制
long lastModifiedDate = getLastModifiedDate();
response.setDateHeader("Last-Modified", lastModifiedDate);

nginx缓存

对于 nginx 的缓存,可以参考我的文章 : 实战:使用lua脚本在nginx层解决高并发访问问题

当然,这里补充一下 nginx的本地缓存

OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。

1)开启共享字典,在nginx.conf的http下添加配置:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 # 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
 lua_shared_dict item_cache 150m; 

2)操作共享字典:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')

jvm进程缓存

为什么需要jvm进程缓存呢?

因为在jvm进程中进行本地缓存,例如HashMap、GuavaCache。它们读取的是本地的内存,没有丝毫的网络开销,速度非常快。但也有缺点,就是依赖于系统本身存储容量有限,单jvm中,无法进行多jvm共享。

所以在一些并发场景,针对一些不太大的数据信息缓存,可以产生不小的效果。

目前来说,在java中使用进程缓存,一般都是使用 HashMap 、Guava 、Caffeine .我们这里主要介绍Caffeine

Caffeine

Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。性能相当棒。

依赖

代码语言:txt
AI代码解释
复制

<!-- coffeine 本地缓存  版本自己选择 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

配置Caffeine

创建caffeine.yml 当然也可以自行定义在application.yml中 或者直接写死在代码中

代码语言:txt
AI代码解释
复制
caffeine:
  initialSize: 1
  maximumSize: 3

创建配置文件

代码语言:txt
AI代码解释
复制
@Configuration
@PropertySource(value = "classpath:caffeine.yml")
@ConfigurationProperties(prefix = "caffeine")
public class CacheConfig {

    @Value(value = "${initialSize}")
    int initialSize;

    @Value(value = "${maximumSize}")
    int maximumSize;

    @Bean
    public Cache<String,Object> caffeineCache(){
        return Caffeine.newBuilder()
                .initialCapacity(initialSize)//初始容积
                .maximumSize(maximumSize)//设置缓存最大上限值
                .expireAfterAccess(Duration.ofSeconds(5))//设置有效时间 多久后未访问过期 访问包括读写
//                .maximumWeight(30)  //最大权重
//                .weigher((p) -> p.getAge()) //计算权重的参数值
               // .expireAfterWrite(5, TimeUnit.SECONDS)//设置有效期,写入多久后过期
                .build();
    }
}

一些方法介绍

方法

使用

介绍

getIfPresent(key)

caffeineCache.getIfPresent("a");

当前是否有缓存,如果有缓存返回缓存,没有则返回null

get(key,Function(k,v))

caffeineCache.get("a",k->{ System.out.println("key不存在"); return "val"; });

获取缓存,当缓存不存在时,执行function方法:正常应该是去数据库获取数据并写入缓存

put(key,val)

caffeineCache.put("a",1);

caffeineCache.put("a",1); 写入值

invalidateAll(Iterable keys)

String[] s = {"a","b","c","d"}; caffeineCache.invalidateAll((Iterable<String>) () -> Arrays.stream(s).iterator());

批量删除缓存

invalidate(key)

caffeineCache.invalidate("a");

删除某个缓存

invalidateAll()

清空缓存

getAllPresent(Iterable keys)

参考批量删除 批量获取缓存

批量获取缓存

estimatedSize()

estimatedSize() caffeineCache.estimatedSize()

获取缓存中的个数

Caffeine提供了三种缓存驱逐策略

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

代码语言:txt
AI代码解释
复制
/ 创建缓存对象
Cache<String,String>cache =Caffeine.newBuilder().maximumSize(1).build();//设置缓存大小上限为1

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

代码语言:txt
AI代码解释
复制
Cache<String,String>cache =Caffeine.newBuilder().expireAfterwrite(Duration.ofseconds(10)).build();//设置缓存有效期为 10 秒,从最后一次写入开始计时

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

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

实际运用

代码语言:txt
AI代码解释
复制
@RestController
@RequestMapping(value = "/caffeine")
public class CacheCaffeineController {

    private String name;

    @Autowired(required = true)
    @Qualifier("caffeineCache")
    Cache<String,Object> caffeineCache;


    @GetMapping(value = "/get")
    public void get(){
        System.out.println(caffeineCache.estimatedSize());

        String a1 = (String) caffeineCache.getIfPresent("a");
        System.out.println("如果a有缓存的话返回a,没值的话返回null,a1的值:{}");
        System.out.println(a1);

        System.out.println("--------------");
        String a = (String) caffeineCache.get("a",k->{
            System.out.println("key不存在");
            return "val";
        });
        if(StrUtil.isEmpty(a)){
            System.out.println("无缓存");
        }else{
            System.out.println(a);
        }


        System.out.println(caffeineCache.stats());


    }

    @GetMapping(value = "/delAll")
    public void delAll(){
        String[] s  = {"a","b","c","d"};

        caffeineCache.invalidateAll((Iterable<String>) () -> Arrays.stream(s).iterator());

        caffeineCache.invalidate("a");
    }

    @GetMapping(value = "/set/{key}/{val}")
    public void set(
            @PathVariable String key,
            @PathVariable String val
    ){
        caffeineCache.put(key,val);
    }
}

本地缓存: ConcurrentHashMap

ConcurrentHashMapHashMap一样,是一个存放键值对。使用hash算法来获取值的对应的地址,因此时间复杂度是O(1)。查询非常快。

ConcurrentHashMap是线程安全的,ConcurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。

所以经常通过 ConcurrentHashMap 来进行本地缓存。下面是使用案例

代码语言:txt
AI代码解释
复制

@Component
public class CacheConcurrentHashMapFactory {
    
    /**
    * 默认缓存时长
    */
    public static final int DEFAULT_CACHE_TIMEOUT = 30;
    
    /**
    * 默认初始化容积
    */
    public static final int DEFAULT_INIT_CAPACITY = 1000;
    
    /**
    * 存储数据
    */
    public static Map<String,Object> data;
    
    public static ScheduledExecutorService executorService;
    
    static {
        data =  new ConcurrentHashMap<>(DEFAULT_INIT_CAPACITY);
        executorService = new ScheduledThreadPoolExecutor(2);
    }
    
    /**
    * 增加缓存
    * @param k
    * @param v
    */
    public static void put(String k , Object v){
        data.put(k,v);
        executorService.schedule(new TimerTask() {
            @Override
            public void run() {
                data.remove(k);
            }
        },DEFAULT_CACHE_TIMEOUT, TimeUnit.SECONDS);
    }
    
    /**
    * 增加缓存:自定义时间
    * @param k
    * @param v
    * @param timeOut
    */
    public static void put(String k , Object v,int timeOut) {
        data.put(k,v);
        executorService.schedule(()->data.remove(k),timeOut,TimeUnit.SECONDS);
    }
    
    /**
    * 获取缓存
    * @param k
    * @return
    */
    public static Object get(String k) {
        return data.get(k);
    }
    
    /**
    * 获取所有缓存
    * @return
    */
    public static Set<String> getAll(){
        return data.keySet();
    }
    
    private CacheConcurrentHashMapFactory(){
    
    }
}

总结

以上介绍了从 浏览器缓存、nginx使用lua操作redis缓存、nginx本地缓存、jvm本地缓存。以此来构建了一个多级缓存。

当然有很多细节没在这里介绍。这只是一个基础的入门。让大家明白多级缓存是怎么回事以及有哪些缓存节点。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 传统缓存的问题
  • 多级缓存是哪些级别的缓存?
  • 浏览器缓存
  • nginx缓存
  • jvm进程缓存
    • Caffeine
      • 依赖
      • 配置Caffeine
      • 一些方法介绍
      • Caffeine提供了三种缓存驱逐策略
      • 实际运用
  • 本地缓存: ConcurrentHashMap
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档