多级缓存设计和实战应用

生活场景

星期天的上午,布布和一二在家里畅谈人生,突然觉得生活太乏味,一二提出要去吃一碗新鲜的麻辣烫。

于是他们来到一家当地有名的麻辣烫店。

图片[1]-多级缓存设计和实战应用-不念博客

刚进门,店里摆放了一个自助取餐具柜。

当时排队人数较多,如果大家都从取餐柜中拿餐盘,每个人都需要开一次柜取一个。

这样开柜门、关柜门都需要消耗不必要的时间。但是当我们把餐具摆到外边,大家随手可以获取。

这样可以避免频繁的开关柜门,从而节省很多时间。过程描述如下:

  1. 顾客从外放的餐具处拿餐具(缓存)
  2. 如果外放餐具没有,服务员从餐柜拿出一摞补充到原来位置(从磁盘加载到内存,避免频繁I/O)
  3. 如果服务员比较忙,顾客自助从取餐柜拿餐具(磁盘)

缓存设计模型

先不深入理解缓存设计的深层原理,我们先从这个生活例子抽象出一个简易的缓存设计模型:

图片[2]-多级缓存设计和实战应用-不念博客

在计算机领域,关于磁盘、内存、缓存、I/O的概念

  1. 磁盘(Disk):
    • 是什么: 就像电脑的大型存储仓库,用于持久保存数据。
    • 怎么玩: 数据在磁盘上存储并可以长期保存,即使电脑关闭也不会丢失。
    • 举栗子: 就像一个大衣柜,你可以把不经常用的东西放在那里,随时取出。
  2. 内存(Memory):
    • 是什么: 类似于电脑的短期记忆,用于存储当前正在使用的程序和数据。
    • 怎么玩: 数据在内存中存储,电脑运行时会迅速读取和写入,但一旦关闭电脑,内存就会被清空。
    • 举栗子: 就像桌子上的工作空间,学习工作时放东西,工作完把东西收拾带走。
  3. 缓存(Cache):
    • 是什么: 一种更小但更快速的存储,用于临时存储经常访问的数据,以提高计算机的性能。
    • 怎么玩: 缓存存储了最常用的数据,使得计算机能够更快速地获取信息。
    • 举栗子: 就像你桌子上的备忘录,记录着你最常用的信息,不用每次都去找。
  4. I/O(Input/Output):
    • 是什么: 涉及计算机与外部世界之间的数据传输,包括输入(从外部获取数据)和输出(向外部发送数据)
    • 怎么玩: 例如,从磁盘读取文件(输入)或将数据显示在屏幕上(输出)。
    • 举栗子: 就像你从书上读取信息(输入),或者将你的想法写在纸上(输出)。

小结

综上,我们对缓存有了概念上的理解。在实战项目中,不同的系统承载的流量有所不同,峰值状态:几万、几十万、几百万、亿级流量都有可能。

缓存设计就是是为了避免频繁I/O、提高吞吐量、增强系统性能

接下来,我们逐一探究实际项目实战链路中,都会用到那些缓存设计来提高系统性能?

传统缓存设计

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

图片[3]-多级缓存设计和实战应用-不念博客

存在下面的问题:

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

多级缓存

什么是多级缓存?

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

如图,是多级缓存架构基本组成:

图片[4]-多级缓存设计和实战应用-不念博客

我们简单描述下,当请求进入系统后的工作流程:

  1. 浏览器访问静态资源时,优先读取浏览器本地缓存
  2. 访问非静态资源(ajax查询数据)时,访问服务端
  3. 请求到达Nginx后,优先读取Nginx本地缓存
  4. 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)
  5. 如果Redis查询未命中,请求进入Tomcat后,优先查询JVM进程缓存
  6. 如果JVM进程缓存未命中,则查询数据库

标注:
在多级缓存架构中,基于OpenResty框架Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑,因此这样的nginx服务不再是一个反向代理服务器,而是一个编写业务的Web服务器。因此这样的业务Nginx服务也需要搭建集群来提高并发,再有专门的nginx服务来做反向代理

小结

可见,全链路多级缓存有几个关键点:

  • 浏览器本地缓存
  • nginx本地缓存(运用OpenResty中Nginx+Lua编程)
  • Redis缓存
  • JVM进程本地缓存

本文重点聊聊JVM进程缓存和Redis缓存应用

缓存对系统性能影响至关重要,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。

我们把缓存分为两类:

  • 分布式缓存,例如Redis:
    • 优点:存储容量更大、可靠性更好、可以在集群间共享
    • 缺点:访问缓存有网络开销
    • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
  • 进程本地缓存,例如ConcurrentHashMap、Guava Cache:
    • 优点:读取本地内存,没有网络开销,速度更快
    • 缺点:存储容量有限、可靠性较低、无法共享
    • 场景:性能要求较高,缓存数据量较小

今天我们利用Caffeine框架来实现JVM进程缓存。

JVM进程缓存(本地缓存)

JVM进程缓存 Caffeine 介绍

Caffeine 是一个基于Java8开发的,提供了近乎最佳命中率的高性能本地缓存库。

目前Spring内部的缓存使用的就是Caffeine。

官方介绍

图片[5]-多级缓存设计和实战应用-不念博客

性能测试报告对比(官方测试数据,当然不同机器结果也不同):

图片[6]-多级缓存设计和实战应用-不念博客

简单学习Caffeine使用:

  1. 导入依赖
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
</dependency>
  1. 基本用法
@Test
void testBasicOps() {
    // 创建缓存对象
    Cache<String, String> cache =
            Caffeine.newBuilder()
                    .expireAfterWrite(Duration.ofSeconds(10)) // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
                    .build();

    // 存数据
    cache.put("code", "码易有道");

    // 取数据,不存在则返回null
    String val = cache.getIfPresent("code_01"); //null

    // 取数据,不存在则去数据库查询
    String dbVal = cache.get("ping", key -> {
        // 这里可以查询数据库根据 key查询value
        return "pong";
    });
}
  1. 真实案例编写

利用Caffeine实现下列需求:

  • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
  • 缓存初始大小为100
  • 缓存上限为10000

配置config

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }
}

业务逻辑:

@GetMapping("/{id}")
    public Item findById(@PathVariable("id") Long id) {
        //查本地缓存 --> 如果没有,查数据库: 状态不为:2 ,且查询条件是ID等于指定的键值,返回一个结果
        return itemCache.get(id, key -> itemService.query()
                .ne("status", 2)
                .eq("id", key)
                .one()
        );
    }

演示结果:

访问:http://localhost:8081/item/10002,返回正确结果

图片[7]-多级缓存设计和实战应用-不念博客

第一次访问,打印出SQL,说明查询了数据库:

21:28:49:535 DEBUG 3332 --- [nio-8081-exec-1] c.asdc.item.mapper.ItemMapper.selectOne  : ==>  Preparing: SELECT id,name,title,price,image,category,brand,spec,status,create_time,update_time FROM tb_item WHERE (status <> ? AND id = ?)
21:28:49:536 DEBUG 3332 --- [nio-8081-exec-1] c.asdc.item.mapper.ItemMapper.selectOne  : ==> Parameters: 2(Integer), 10002(Long)
21:28:49:544 DEBUG 3332 --- [nio-8081-exec-1] c.asdc.item.mapper.ItemMapper.selectOne  : <==      Total: 1

第二次访问,未打印出SQL,说明直接查询的本地缓存。

因此,利用Caffeine实现缓存功能生效。

Redis缓存

redis 到底有多快?

根据官方数据,Redis 的 QPS 可以达到约 100000(每秒请求数),感兴趣的可参考:

官方Redis benchmark

以下摘自官网:

With high-end configurations, the number of client connections is also an important factor. Being based on epoll/kqueue, the Redis event loop is quite scalable. Redis has already been benchmarked at more than 60000 connections, and was still able to sustain 50000 q/s in these conditions. As a rule of thumb, an instance with 30000 connections can only process half the throughput achievable with 100 connections. Here is an example showing the throughput of a Redis instance per number of connections:

客户端连接数压测报告:

图片[8]-多级缓存设计和实战应用-不念博客

X轴:客户端连接数,Y轴:QPS

生产实践中如何使用redis做缓存?

基本流程:

图片[9]-多级缓存设计和实战应用-不念博客

程序实现:

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置连接

spring:
  redis:
    host: your_ip 
    database: 1
    username: your_username
    password: your_password

3、代码编写:

@Autowired
private StringRedisTemplate redisTemplate;

//JSON 转化
private static final ObjectMapper MAPPER = new ObjectMapper();

@GetMapping("/redis/{id}")
public String queryRedisItemData(@PathVariable("id") Long id) throws JsonProcessingException {
    // 查询Redis缓存
    String cachedResult = redisTemplate.opsForValue().get("item:id:" + id);
    if (Strings.isNotBlank(cachedResult)) {
        System.out.println("从Redis缓存中获取数据:" + cachedResult);
        return cachedResult;
    } else {
        // 1.查询数据库
        Item item = itemService.query().ne("status", 2).eq("id", id).one();
        //转化成json
        String dbResult = MAPPER.writeValueAsString(item);
        System.out.println("从数据库中获取数据:" + dbResult);
        // 将查询结果存入缓存
        redisTemplate.opsForValue().set("item:id:" + id, dbResult);
        return dbResult;
    }
}

4、演示效果:

4.1. 测试前:redis中没有对应key:item:id:10002

图片[10]-多级缓存设计和实战应用-不念博客

4.2. 访问:http://localhost:8081/item/10002,返回正确结果

图片[11]-多级缓存设计和实战应用-不念博客

4.3. 查看控制台SQL打印情况,显然第一次访问查询了数据库

23:25:33:162 DEBUG 20032 --- [nio-8081-exec-1] c.asdc.item.mapper.ItemMapper.selectOne  : ==>  Preparing: SELECT id,name,title,price,image,category,brand,spec,status,create_time,update_time FROM tb_item WHERE (status <> ? AND id = ?)
23:25:33:163 DEBUG 20032 --- [nio-8081-exec-1] c.asdc.item.mapper.ItemMapper.selectOne  : ==> Parameters: 2(Integer), 10002(Long)
23:25:33:171 DEBUG 20032 --- [nio-8081-exec-1] c.asdc.item.mapper.ItemMapper.selectOne  : <==      Total: 1
从数据库中获取数据:{"id":10002,"name":"脱脂牛奶","title":"安佳脱脂牛奶 新西兰进口轻欣脱脂250ml*24整箱装*2","price":68600,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t25552/261/1180671662/383855/33da8faa/5b8cf792Neda8550c.jpg!q70.jpg.webp","category":"牛奶","brand":"安佳","spec":"{\"数量\": 24}","status":1,"createTime":1706198400000,"updateTime":1706198400000,"stock":null,"sold":null}

4.4. 查看当前redis库db1中数据

图片[12]-多级缓存设计和实战应用-不念博客

4.5. 再次访问:http://localhost:8081/item/10002,控制台打印:

从Redis缓存中获取数据:{"id":10002,"name":"脱脂牛奶","title":"安佳脱脂牛奶 新西兰进口轻欣脱脂250ml*24整箱装*2","price":68600,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t25552/261/1180671662/383855/33da8faa/5b8cf792Neda8550c.jpg!q70.jpg.webp","category":"牛奶","brand":"安佳","spec":"{\"数量\": 24}","status":1,"createTime":1706198400000,"updateTime":1706198400000,"stock":null,"sold":null}

显然,本次查询走redis缓存,验证通过。这也印证了上边的流程。

缓存预热怎么做?

通常,Redis缓存会面临冷启动问题:

冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有业务数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。

缓存预热:在实际开发中,为避免上述问题,我们一般会利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。这个过程成为缓存预热。

方案一、在项目启动时初始化Bean时候完成

这里我们利用InitializingBean接口来实现,因为InitializingBean可以在对象被Spring创建并且成员变量全部注入后执行。

这里我们是测试数据(模拟热点数据),全量加载到redis中。

程序实现:

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private IItemService itemService;

    //JSON 转化
    private static final ObjectMapper MAPPER = new ObjectMapper();


    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化缓存
        // 1.从数据库获取热点数据
        List<Item> itemList = itemService.list();
        // 2.放入缓存
        for (Item item : itemList) {
            // item序列化为JSON
            String json = MAPPER.writeValueAsString(item);
            // 存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }
    }
}

启动并测试:

23:42:08:477 DEBUG 13200 --- [           main] c.a.item.mapper.ItemMapper.selectList    : ==>  Preparing: SELECT id,name,title,price,image,category,brand,spec,status,create_time,update_time FROM tb_item
23:42:08:507 DEBUG 13200 --- [           main] c.a.item.mapper.ItemMapper.selectList    : ==> Parameters: 
23:42:08:534 DEBUG 13200 --- [           main] c.a.item.mapper.ItemMapper.selectList    : <==      Total: 5

查看redis中,key:item:10001~10005 ,均正常保存。代表缓存预热已实现。

图片[13]-多级缓存设计和实战应用-不念博客

方案二、在项目启动完成后,通过访问接口同步

 @GetMapping("/loadRedis")
    public String loadRedis() throws JsonProcessingException {
        // 初始化缓存
        try {
            // 1.从数据库查询
            List<Item> itemList = itemService.list();
            // 2.放入缓存
            for (Item item : itemList) {
                // item序列化为JSON
                String json = MAPPER.writeValueAsString(item);
                // 存入redis
                redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
            }
        } catch (Exception e) {
            return "loadRedis failed!";
        }
        return "loadRedis sucess!";
    }

缓存数据同步如何做?

数据同步策略

缓存数据同步的常见方式有三种:

设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新

  • 优势:简单、方便
  • 缺点:时效性差,缓存过期之前可能不一致
  • 场景:更新频率较低,时效性要求低的业务

同步双写:在修改数据库的同时,直接修改缓存

  • 优势:时效性强,缓存与数据库强一致
  • 缺点:有代码侵入,耦合度高;
  • 场景:对一致性、时效性要求较高的缓存数据

异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

  • 优势:低耦合,可以同时通知多个缓存服务
  • 缺点:时效性一般,可能存在中间不一致状态
  • 场景:时效性要求一般,有多个服务需要同步

异步通知方案:

1)基于MQ异步消息

基本流程如下:

图片[14]-多级缓存设计和实战应用-不念博客

缓存同步流程描述:

  • 业务服务完成对数据的修改后,只需要发送一条消息到MQ中。
  • 缓存服务监听MQ消息,然后完成对缓存的更新

缺点:依然有少量的代码侵入。

2)基于Canal的通知

  Canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅和消费。GitHub的地址:https://github.com/alibaba/canal

Canal是基于mysql的主从同步来实现的,MySQL主从同步的原理如下:

图片[15]-多级缓存设计和实战应用-不念博客
  • MySQL master 将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events
  • MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)
  • MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

而Canal类似于MySQL的一个slave节点,从而监听master的binary log变化。

再把得到的变化信息通知给Canal对应的客户端,完成对数据库的同步。

图片[16]-多级缓存设计和实战应用-不念博客

本次我们利用Canal同步数据库和redis.

基本流程如下:

图片[17]-多级缓存设计和实战应用-不念博客

流程描述:

  • 业务数据发生变更
  • 更新数据库
  • Canal监听Mysql binlog 变化
  • 通知缓存服务更新
  • 更新缓存

程序实现:

1、引入依赖

<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>

2、Mysql主从核心配置

$ vim /etc/my.cnf

[mysqld]
log-bin=/data/mysql/mysql-bin #设置binary log文件的存放地址和文件名,叫做mysql-bin
binlog-do-db=mydb   # 指定对哪个database记录binary log events,这里记录mydb这个库
server-id=1000  # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

重启mysql

[root@hcss-ecs-143a ~]# service mysql restart
Shutting down MySQL............                            [  OK  ]
Starting MySQL..                                           [  OK  ]

检查主从同步状态和集群名称:

图片[18]-多级缓存设计和实战应用-不念博客

3、yml配置

canal:
  destination: mydb # canal的集群名字,要与安装canal时设置的名称一致
  server: 1.XXX.XXX.47:11111 # canal服务地址

4、Canal 安装和配置

安装步骤请自行查阅,这里只提供核心配置

canal.properties 配置

$ vim /opt/canal/conf/canal.properties

canal.port = 11111
canal.instance.tsdb.dbUsername = canal
canal.instance.tsdb.dbPassword = canal

#################################################
#########   destinations  #############
#################################################
canal.destinations = mydb

instance.properties 配置

安装后会在/opt/canal/conf/example 下 有instance.properties 配置。 参考修改:

# table regex
canal.instance.filter.regex=mydb\\..*

最终配置完成:

图片[19]-多级缓存设计和实战应用-不念博客

5、编写监听器

通过实现EntryHandler<T>接口编写监听器,监听Canal消息。注意两点:

  • 实现类通过@CanalTable("tb_item")指定监听的表信息
  • EntryHandler的泛型是与表对应的实体类
@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {

    @Autowired
    private RedisHandler redisHandler;
    @Autowired
    private Cache<Long, Item> itemCache;

    @Override
    public void insert(Item item) {
        // 写数据到JVM进程缓存
        itemCache.put(item.getId(), item);
        // 写数据到redis
        redisHandler.saveItem(item);
    }

    @Override
    public void update(Item before, Item after) {
        // 写数据到JVM进程缓存
        itemCache.put(after.getId(), after);
        // 写数据到redis
        redisHandler.saveItem(after);
    }

    @Override
    public void delete(Item item) {
        // 删除数据到JVM进程缓存
        itemCache.invalidate(item.getId());
        // 删除数据到redis
        redisHandler.deleteItemById(item.getId());
    }
}

6、演示效果 比如:我们将id = 10002 脱脂牛奶 价格(price)变为998.

修改前:

mysql> select t.id,t.name,t.price from mydb.tb_item t where t.id = '10002' ;
+-------+--------------+-------+
| id    | name         | price |
+-------+--------------+-------+
| 10002 | 脱脂牛奶     | 68600 |
+-------+--------------+-------+
1 row in set (0.00 sec)

执行修改: 访问:http://localhost:8081/item  更新id=10002 对应价格998. 请求参数:

{
    "id":10002,
    "name":"脱脂牛奶",
    "title":"安佳脱脂牛奶 新西兰进口轻欣脱脂250ml*24整箱装*2",
    "price":998,
    "image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t25552/261/1180671662/383855/33da8faa/5b8cf792Neda8550c.jpg!q70.jpg.webp",
    "category":"牛奶",
    "brand":"安佳",
    "spec":"{\"数量\": 30}",
    "status":1,
    "createTime":"2024-01-25T16:00:00.000+00:00",
    "updateTime":"2024-01-25T16:00:00.000+00:00",
    "stock":99999,
    "sold":54981
}

修改后:

查看数据库:

mysql> select t.id,t.name,t.price from mydb.tb_item t where t.id = '10002' ;
+-------+--------------+-------+
| id    | name         | price |
+-------+--------------+-------+
| 10002 | 脱脂牛奶     |   998 |
+-------+--------------+-------+
1 row in set (0.00 sec)

查看redis:

图片[20]-多级缓存设计和实战应用-不念博客

查看控制台:

11:14:28:980  INFO 19916 --- [l-client-thread] t.j.c.client.client.AbstractCanalClient  : 获取消息 Message[id=2,entries=[header {
  version: 1
  logfileName: "mysql-bin.000005"
  logfileOffset: 978
  serverId: 1000
  serverenCode: "UTF-8"
  executeTime: 1706325266000
  sourceType: MYSQL
  schemaName: ""
  tableName: ""
  eventLength: 31
}

说明Canal同步缓存数据生效,验证通过。

基于OpenResty的Nginx本地缓存

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

这里粗略给出OpenResty安装后的目录结构:

图片[21]-多级缓存设计和实战应用-不念博客

从目录结构可以看到:OpenResty 内置Nginx + Lua模块

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

 # 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
 lua_shared_dict item_cache 150m; 

2)使用共享字典:

-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')

3)实现本地缓存查询

修改/usr/local/openresty/lua/item.lua文件,添加本地缓存逻辑:

设置了缓存过期时间,过期后nginx缓存会自动删除,下次访问即可更新缓存。

这里给商品基本信息设置超时时间为30分钟。因为库存更新频率较高,如果缓存时间过长,可能与数据库差异较大。

完整的item.lua文件:

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache

-- 封装查询函数
function read_data(key, expire, path, params)
    -- 查询本地缓存
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)
        -- 查询redis
        val = read_redis("1.XXX.XXX.47", 6379, key)
        -- 判断查询结果
        if not val then
            ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
            -- redis查询失败,去查询http
            val = read_http(path, params)
        end
    end
    -- 查询成功,把数据写入本地缓存
    item_cache:set(key, val, expire)
    -- 返回数据
    return val
end

-- 获取路径参数
local id = ngx.var[1]

-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, 1800 ,  "/item/" .. id, nil)

-- JSON转化为lua的table
local item = cjson.decode(itemJSON)

-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))

关于OpenResty 的使用和语法结构,读者请自行查阅。由于笔者属于后端领域,这里暂时简单介绍。

感兴趣的可以深究。

官网地址:https://openresty.org/cn/

总结

当考虑不同的缓存设计方案时,需要根据具体的应用场景和需求来选择。

这里给出项目链路上关于缓存设计的应用场景和建议:

  1. 浏览器缓存:
    • 应用场景: 适用于静态资源如图片、CSS和JavaScript等的缓存,以减少服务器负载和加快页面加载速度。
    • 建议: 使用HTTP标头控制缓存策略,如Cache-Control和Expires,确保资源在客户端浏览器上被有效地缓存,减少对服务器的请求次数。
  2. 基于OpenResty的Nginx本地缓存:
    • 应用场景: 适用于对频繁请求的动态数据进行本地缓存,如API响应数据、网页片段等。
    • 建议: 使用OpenResty结合Nginx的本地缓存功能,通过Lua脚本实现对特定请求响应的本地缓存,可以有效减少后端服务器的负载和提高响应速度。
  3. Redis缓存:
    • 应用场景: 适用于需要分布式缓存、高并发读写、数据结构丰富(如哈希、列表、集合等)的场景,如会话管理、页面片段缓存、数据缓存等。
    • 建议: 使用Redis作为分布式缓存,结合其丰富的数据结构和高性能特性,可以实现高效的缓存管理和数据处理。
  4. JVM进程缓存:
    • 应用场景: 适用于对应用程序内部数据进行缓存,如对象缓存、方法结果缓存等。
    • 建议: 使用基于内存的缓存库,如Caffeine、ConcurrentHashMap、Guava Cache等,将需要频繁访问的数据缓存在JVM进程中,以加快数据访问速度,并减少对外部存储的依赖。

综上,缓存设计方案能极大提高应用程序的性能和可扩展性。

需要说明的是:后台系统最好注意对缓存的管理、监控和更新。

此外,我们知道任何的设计都是根据需要的,因为引入新的技术就会带来新的问题,所以也要避免过度设计。

这里讲的是生产实践中的一些思路,可以说多一种选择、多一种可能。

© 版权声明
THE END