多级缓存实战

关键词:高性能、缓存、Redis、I/O
导语:从生活场景到项目实战中缓存设计的一些思考。希望本文是你技术进阶的基石、也是茶余饭后的消遣。

文章导读

picture.image

生活场景

  星期天的上午,布布和一二在家里畅谈人生。突然觉得生活太乏味,一二提出要去吃一碗新鲜的麻辣烫。于是他们来到一家当地有名的麻辣烫店。

picture.image

刚进门,店里摆放了一个自助取餐具柜。当时排队人数较多,如果大家都从取餐柜中拿餐盘,每个人都需要开一次柜取一个。这样开柜门、关柜门都需要消耗不必要的时间。但是当我们把餐具摆到外边,大家随手可以获取。这样可以避免频繁的开关柜门,从而节省很多时间。过程描述如下:

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

缓存设计模型

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

picture.image

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

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

小结

  综上,我们对缓存有了概念上的理解。在实战项目中,不同的系统承载的流量有所不同,峰值状态:几万、几十万、几百万、亿级流量都有可能。缓存设计就是是为了避免频繁I/O、提高吞吐量、增强系统性能。接下来,我们逐一探究实际项目实战链路中,都会用到那些缓存设计来提高系统性能?

传统缓存设计

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

picture.image

存在下面的问题:

•请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈

•Redis缓存失效时,会对数据库产生冲击

多级缓存

什么是多级缓存?

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

picture.image

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

  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。

Caffeine GitHub地址

官方介绍picture.image

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

picture.image

简单学习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,返回正确结果

picture.image

第一次访问,打印出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:

客户端连接数压测报告:

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

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

基本流程:

picture.image

程序实现:

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🆔10002

picture.image

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

picture.image

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中数据

picture.image

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 ,均正常保存。代表缓存预热已实现。

picture.image

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


        
          
    @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异步消息

基本流程如下:

picture.image

缓存同步流程描述:

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

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

2)基于Canal的通知

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

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

picture.image

  • 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对应的客户端,完成对数据库的同步。

picture.image

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

基本流程如下:picture.image

流程描述:

  • 业务数据发生变更
  • 更新数据库
  • 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  ]  

      

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

picture.image

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\\..*  

      

最终配置完成:

picture.image

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:

picture.image

查看控制台:


        
          
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安装后的目录结构:

picture.image

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

  综上,缓存设计方案能极大提高应用程序的性能和可扩展性。需要说明的是:后台系统最好注意对缓存的管理、监控和更新。此外,我们知道任何的设计都是根据需要的,因为引入新的技术就会带来新的问题。所以也要避免过度设计。这里讲的是生产实践中的一些思路,可以说多一种选择、多一种可能。

  对于缓存设计,重点以Redis缓存设计为主。在使用中,可能遇到多种问题。如:缓存雪崩、缓存击穿、缓存穿透等。限于篇幅,后期可能会更新一篇集中介绍,敬请期待。

0
0
0
0
评论
未登录
暂无评论