首页 > 数据库 >Redis(四)

Redis(四)

时间:2023-06-15 23:33:07浏览次数:50  
标签:缓存 item -- lua Redis Lua nginx

5.多级缓存

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

image-20210821075259137

存在的问题

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

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

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

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

image-20210821075558137

在多级缓存架构中,Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑,因此这样的nginx服务不再是一个反向代理服务器,而是一个编写业务的Web服务器了

因此这样的业务Nginx服务也需要搭建集群来提高并发,再有专门的nginx服务来做反向代理

image-20210821080511581

另外,我们的Tomcat服务将来也会部署为集群模式:

image-20210821080954947

多级缓存的关键有两个

  • 在nginx中编写业务,实现nginx本地缓存、Redis、Tomcat的查询

  • 在Tomcat中实现JVM进程缓存

其中Nginx编程则会用到OpenResty框架结合Lua这样的语言。

6.JVM进程缓存

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

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

Caffeine

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

Caffeine的性能非常好,下图是官方给出的性能对比

image-20210821081826399

缓存使用的基本API

@Test
void testBasicOps() {
    // 构建cache对象
    Cache<String, String> cache = Caffeine.newBuilder().build();
    // 存数据
    cache.put("gf", "迪丽热巴");
    // 取数据
    String gf = cache.getIfPresent("gf");
    System.out.println("gf = " + gf);
    // 取数据,包含两个参数:
    // 参数一:缓存的key
    // 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
    // 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
    String defaultGF = cache.get("defaultGF", key -> {
        // 根据key去数据库查询数据
        return "柳岩";
    });
    System.out.println("defaultGF = " + defaultGF);
}

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

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

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1) // 设置缓存大小上限为 1
        .build();
    
  • 基于时间:设置缓存的有效时间

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder()
        // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
        .expireAfterWrite(Duration.ofSeconds(10)) 
        .build();
    
    
  • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

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

7.Lua

官网:https://www.lua.org/

Nginx编程需要用到Lua语言,因此我们必须先入门Lua的基本语法。

7.1.初识

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

image-20210821091437975

Lua经常嵌入到C语言开发的程序中,例如游戏开发、游戏插件等。

Nginx本身也是C语言开发,因此也允许基于Lua做拓展。

7.2.快速入门

CentOS7默认已经安装了Lua语言环境,所以可以直接运行Lua代码。

1.在Linux虚拟机的任意目录下,新建一个hello.lua文件

image-20210821091621308

2.添加下面的内容

print("Hello World!")  

3.运行

image-20210821091638140

7.3.变量和循环

7.3.1.Lua的数据类型

Lua中支持的常见数据类型包括

image-20210821091835406

Lua还提供了type()函数来判断一个变量的数据类型

image-20210821091904332

7.3.2.声明变量

Lua声明变量的时候无需指定数据类型,而是用local来声明变量为局部变量

-- 声明字符串,可以用单引号或双引号,
local str = 'hello'
-- 字符串拼接可以使用 ..
local str2 = 'hello' .. 'world'
-- 声明数字
local num = 21
-- 声明布尔类型
local flag = true

Lua中的table类型既可以作为数组,又可以作为Java中的map来使用。数组就是特殊的table,key是数组角标而已:

-- 声明数组 ,key为角标的 table
local arr = {'java', 'python', 'lua'}
-- 声明table,类似java的map
local map =  {name='Jack', age=21}

Lua中的数组角标是从1开始,访问的时候与Java中类似:

-- 访问数组,lua数组的角标从1开始
print(arr[1])

Lua中的table可以用key来访问:

-- 访问table
print(map['name'])
print(map.name)

7.3.3.循环

对于table,我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。

遍历数组:

-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍历数组
for index,value in ipairs(arr) do
    print(index, value) 
end

遍历普通table

-- 声明map,也就是table
local map = {name='Jack', age=21}
-- 遍历table
for key,value in pairs(map) do
   print(key, value) 
end

7.4.条件控制、函数

Lua中的条件控制和函数声明与Java类似。

7.4.1.函数

定义函数

function 函数名( argument1, argument2..., argumentn)
    -- 函数体
    return 返回值
end

例如,定义一个函数,用来打印数组:

function printArr(arr)
    for index, value in ipairs(arr) do
        print(value)
    end
end

7.4.2.条件控制

类似Java的条件控制,例如if、else语法:

if(布尔表达式)
then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end

与java不同,布尔表达式中的逻辑运算是基于英文单词:

image-20210821092657918

7.4.3.案例

需求:自定义一个函数,可以打印table,当参数为nil时,打印错误信息

function printArr(arr)
    if not arr then
        print('数组不能为空!')
    end
    for index, value in ipairs(arr) do
        print(value)
    end
end

8.实现多级缓存

8.1.安装OpenResty

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

OpenResty®是一个基于Nginx的高性能Web平台,用于方便地搭建能够处理超高并发、扩展性极高的动态Web应用、Web服务和动态网关。

特点

  • 具备Nginx的完整功能
  • 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
  • 允许使用Lua自定义业务逻辑自定义库

8.1.1.安装开发库

首先要安装OpenResty的依赖开发库
yum install -y pcre-devel openssl-devel gcc --skip-broken

8.1.2.安装OpenResty仓库

在的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包

yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

如果提示命令不存在,则运行

yum install -y yum-utils 

8.1.3.安装OpenResty

安装软件包

yum install -y openresty

8.1.4.安装opm工具

opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。

yum install -y openresty-opm

8.1.5.配置nginx的环境变量

打开配置文件

vi /etc/profile

在最下面加入两行:

export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH

NGINX_HOME:后面是OpenResty安装目录下的nginx的目录

然后让配置生效

source /etc/profile

8.1.6.启动和运行

OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致:

image-20210811100653291

所以运行方式与nginx基本一致:

# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop

8.1.7.修改配置文件

nginx的默认配置文件注释太多,影响后续我们的编辑,这里将nginx.conf中的注释部分删除,保留有效部分。

修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:


#user  nobody;
worker_processes  1;
error_log  logs/error.log;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8081;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

然后启动nginx进行访问

image-20230615165041295

8.2.OpenResty快速入门

我们希望达到的多级缓存架构如图:

yeVDlwtfMx

其中:

  • windows上的nginx用来做反向代理服务,将前端的查询商品的ajax请求代理到OpenResty集群

  • OpenResty集群用来编写多级缓存业务

8.2.1.反向代理流程

image-20210821094447709

8.2.2.OpenResty监听请求

OpenResty的很多功能都依赖于其目录下的Lua库,需要在nginx.conf中指定依赖库的目录,并导入依赖:

1.添加对OpenResty的Lua模块的加载

修改/usr/local/openresty/nginx/conf/nginx.conf文件,在其中的http下面,添加下面代码:

#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  

2.监听/api/item路径

修改/usr/local/openresty/nginx/conf/nginx.conf文件,在nginx.conf的server下面,添加对/api/item这个路径的监听:

location  /api/item {
    # 默认的响应类型
    default_type application/json;
    # 响应结果由lua/item.lua文件来决定
    content_by_lua_file lua/item.lua;
}

这个监听,就类似于SpringMVC中的@GetMapping("/api/item")做路径映射。

content_by_lua_file lua/item.lua则相当于调用item.lua这个文件,执行其中的业务,把结果返回给用户。相当于java中调用service。

8.2.3.编写item.lua

1.在/usr/loca/openresty/nginx目录创建文件夹:lua

image-20210821100755080

2.在/usr/loca/openresty/nginx/lua文件夹下,新建文件:item.lua

image-20210821100801756

3.编写item.lua,返回假数据

item.lua中,利用ngx.say()函数返回数据到Response中

ngx.say('{"内容"}')

4.重新加载配置

nginx -s reload

8.3.请求参数

OpenResty中提供了一些API用来获取不同类型的前端请求参数

image-20210821101433528

8.4.Redis缓存预热

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

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

缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。

9.缓存同步

9.1.数据同步策略

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

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

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

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

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

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

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

而异步实现又可以基于MQ或者Canal来实现:

1.基于MQ的异步通知:

image-20210821115552327

解读:

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

依然有少量的代码侵入。

2.基于Canal的通知

image-20210821115719363

解读:

  • 商品服务完成商品修改后,业务直接结束,没有任何代码侵入
  • Canal监听MySQL变化,当发现变化后,立即通知缓存服务
  • 缓存服务接收到canal通知,更新缓存

代码零侵入

9.2.Canal

Canal,译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。

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

image-20210821115914748

  1. MySQL master 将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events
  2. MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)
  3. MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

而Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。

image-20210821115948395

9.3.监听Canal

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。

image-20210821120049024

我们可以利用Canal提供的Java客户端,监听Canal通知消息。当收到变化的消息时,完成对缓存的更新。

不过这里我们会使用GitHub上的第三方开源的canal-starter客户端。

与SpringBoot完美整合,自动装配,比官方客户端要简单好用很多。

9.3.1.引入依赖

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

9.3.2.编写配置

canal:
  destination: heima # canal的集群名字,要与安装canal时设置的名称一致
  server: 192.168.150.101:11111 # canal服务地址

9.3.3.修改Item实体类

通过@Id、@Column、等注解完成Item与数据库表字段的映射:

@Data
@TableName("tb_item")
public class Item {
    @TableId(type = IdType.AUTO)
    @Id
    private Long id;//商品id
    @Column(name = "name")
    private String name;//商品名称
    private String title;//商品标题
    private Long price;//价格(分)
    private String image;//商品图片
    private String category;//分类名称
    private String brand;//品牌名称
    private String spec;//规格
    private Integer status;//商品状态 1-正常,2-下架
    private Date createTime;//创建时间
    private Date updateTime;//更新时间
    @TableField(exist = false)
    @Transient
    private Integer stock;
    @TableField(exist = false)
    @Transient
    private Integer sold;
}

9.3.4.编写监听器

通过实现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());
    }
}

在这里对Redis的操作都封装到了RedisHandler这个对象中,是我们之前做缓存预热时编写的一个类,内容如下:

@Component
public class RedisHandler implements InitializingBean {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;
    private static final ObjectMapper MAPPER = new ObjectMapper();
    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化缓存
        // 1.查询商品信息
        List<Item> itemList = itemService.list();
        // 2.放入缓存
        for (Item item : itemList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }
        // 3.查询商品库存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入缓存
        for (ItemStock stock : stockList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
        }
    }
    public void saveItem(Item item) {
        try {
            String json = MAPPER.writeValueAsString(item);
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
    public void deleteItemById(Long id) {
        redisTemplate.delete("item:id:" + id);
    }
}

标签:缓存,item,--,lua,Redis,Lua,nginx
From: https://www.cnblogs.com/Myvlog/p/17484489.html

相关文章

  • springboot 中使用 redis 处理接口的幂等性
    什么是接口幂等性?数学中:在一次元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同;在二次元运算为幂等时,自己重复运算的结果等于它自己的元素。计算机学中:幂等指多次操作产生的影响只会跟一次执行的结果相同,通俗的说:某个行为重复的执行,最终获取的结果是相同的,不会因......
  • 2023-06-15:说一说Redis的Key和Value的数据结构组织?
    2023-06-15:说一说Redis的Key和Value的数据结构组织?答案2023-06-15:全局哈希表Redis使用哈希表作为保存键值对的数据结构,通过哈希函数将Key映射为哈希表中的一个索引位置,使得Key-Value可以在O(1)时间复杂度内被快速访问。在Redis中,哈希表是由多个哈希桶(也称为槽位/数组元素)组成......
  • SpringBoot整合Redis
    第一步:导入坐标 第二步:在application.yml中进行相关配置第三步:使用对应的API对操作接口进行操作 操作 key-value格式的 操作 hash格式的  ......
  • redis学习八:数据类型命令及落地运用 (Zset)
    有序,附带分数,适用于排行榜1.zaddkeyscore1v1score2v2新增键值对;zrangezsetstartend查看对应范围值zrangekeystartendwithscores带着分数查看;zrevrangekey倒序查看,用法和zrange类似; 2.zrangebyscorekeyminmax取分数范围内的value;也可以在前面加上(是不......
  • Spring boot集成Redis实现sessions共享时,sessions过期时间问题分析
    Springboot鼓励零配置的方式,帮你做好大部分重复劳动的事,好到不能再好;具体的Redis安装方法和Springboot集成Redis方法,可以去搜索相关文章或参考该文。 当做用户权限管理时,一般都设置一个session过期时间,以确保用户长时间不操作时自动退出系统。在springboot中设置該值的方法如下: 1......
  • Redis集群公网访问
    背景因业务需求,应用程序需要跨机房从公网地址访问Redis集群,但是无法正常访问。因为程序通过公网IP加端口访问到Redis集群,然后Redis返回集群信息(就是clusternodes命令的返回),程序再根据返回的集群信息去读写Redis集群。而当前集群监听在主机内网地址上,并且是通过内网地址创建的......
  • Redis 的主从复制
    1.Redis主从复制1.1.简介Redis的主从复制是指将一个Redis实例(称为主节点)的数据复制到其他Redis实例(称为从节点)的过程。主从复制可以实现数据备份、读写分离、负载均衡等功能。主机数据更新后根据配置和策略,自动同步到从机的master/slave机制,Master以写为主,Slave以......
  • Redis(二)
    进阶篇1.缓存1.1.缓存介绍1.1.1.介绍缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据1.1.2.作用缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力实际开发过程中,企业的数据量,少则几十万,......
  • 谈论关于Redis产生的分布式ID获取为空问题
    一:事故在项目测试中,遇到一个事件创建失败问题,追踪日志发现分布式ID的获取值为空,导致后续表写入异常。经排查锁定相关方法,具体方法经简化如下:@TransactionalpublicStringtestRedisTrans(){redisTemplate.setEnableTransactionSupport(t......
  • Redis集群
    Redis集群本章是基于CentOS7下的Redis集群教程,包括:单机安装RedisRedis主从Redis分片集群1.单机安装Redis首先需要安装Redis所需要的依赖:yuminstall-ygcctcl然后将课前资料提供的Redis安装包上传到虚拟机的任意目录:例如,我放到了/tmp目录:解压缩:tar-xvfredis-6......