缓存的作用其实是减轻对数据库的压力,缩短服务响应的时间,从而提高整个并发的能力,多级缓存就是来应对亿级流量并发
传统缓存
多级缓存
- 一级缓存-浏览器客户端缓存:浏览器缓存,用户可以通过手机或浏览器访问服务端,得到数据并进行渲染,这里就可以形成第一级缓存--即:浏览器客户端缓存。因为浏览器是可以把服务器返回的静态资源缓存在本地的,这样一来,下次再去访问这些静态资源时候,我们的服务器只要检查一下数据有误变化,没有变化,直接返回304状态码,不用返回数据,浏览器一看304,说明本地有,那就直接把本地渲染,用户就能直接看到了---这样就能减少很多的数据传输,从而提高渲染和响应的速度;事实上,对于页面的请求,90%的请求都是这样一种静态资源请求,响应速度就会大大提升
- 二级缓存-nginx本地缓存:对于一些非静态的数据就不得不访问服务端了,比如说请求到达了n'ginx服务端--这里就要形成二级缓存,称为:nginx本地缓存,请求来了看看nginx有没有,有的话直接返回
- 三级缓存-Redis缓存:如果nginx没有,就直接去redis查,形成三级缓存--即:Redis缓存
- 四级缓存-tomact进程缓存:如果Redis也没有命中,才会到达tomct,形成第四级缓存--即:tomact进程缓存,我们会在服务器的内部,比如说map这样的形式,形成一个进程缓存,这样的缓存保存在tomact本地。当请求到达以后,会先去读取缓存,进程缓存如果命中了,那直接就返回了。从而解决当Redis缓存失效时,直接打到数据库的问题
- 如果还未到达,才会请求数据库
形成多级缓存解决了两个问题:
- 1、请求大多数情况下由nginx处理了,不用tomact,tomact压力就大大减轻了,使得tomact不会成为整个系统的瓶颈
- 2、当Redis缓存失效时,我们还有tomact进程缓存作为缓冲,不会直接打到数据库,避免了一个对数据库的冲击
此时所有的压力其实都集中到了nginx,我们需要在nginx内部去实现对于redis访问,对Tomacr访问等等这样的业务编写。其实nginx此时,就不在是一个反向代理服务器了,就真正变成了一个web服务了,在里边去编写业务逻辑了。因此:将来nginx就需要部署成集群,才能应对更高的一个并发,而我们还可以准备一个单独的nginx用来做反向代理。也就是说,请求到达了单独的nginx后,它在反向代理到我们多个这样的本地缓存编写业务的服务器
当然了,我们的redis、tomact、mysql也都可以去做集群
以上就是一个多级缓存的完整架构方案了,对我们服务端 来说,只需要关注nginx开始到mysql
需要掌握:
- 1、在tomact编写进程缓存---JVM进程缓存
- 2、nginx内部做编程---Lua语言
- 3、实现多级缓存方案
- 4、数据库要与缓存之间还要做数据同步--缓存同步策略
一、JVM进程缓存
1、导入商品案例 -- 老师是虚拟机安装的,我是本地安装,这步暂时不需要,否则端口冲突
进入到mysql目录后执行下面docker命令
-v 三个是数据的挂载。1V:配置文件目录、2V:日志文件目录、3V:mysql的数据目录
-e 指定mysql的root账号+密码123V
docker run \
-p 3306:3306 \
--name mysql \
-v $PWD/conf:/etc/mysql/conf.d \
-v $PWD/logs:/logs \
-v $PWD/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123 \
--privileged \
-d \
mysql:5.7.17
我这里因为本地装了mysql在执行上面的代码会报Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:3306 -> 0.0.0.0:0:
listen tcp 0.0.0.0:3306: bind: address already in use.,端口已被占用
1.3.修改配置
在/tmp/mysql/conf目录添加一个my.cnf文件,作为mysql的配置文件:
# 创建文件 touch /tmp/mysql/conf/my.cnf
文件的内容如下:
[mysqld] skip-name-resolve character_set_server=utf8 datadir=/var/lib/mysql server-id=1000
1.4.重启
配置修改后,必须重启容器:
docker restart mysql
倒入demo工程
修改配置文件,启动项目
浏览器输入:http://localhost:8081/item/10001
mac 安装nginx,利用brew命令:Mac下安装Nginx_danielcaisz的博客-CSDN博客_mac 安装nginx
- 修改配置文件nginx.conf
- open /opt/homebrew/etc/nginx/..
- 将html下的文件,放到自己的html下导向的www下
- open /opt/homebrew/Cellar/nginx/..
- 安装完后,nginx -s reload重新启动
- 输入:http://localhost/item.html?id=10001即可打开页面
- nginx 启动
- nginx -s stop 停止
- 配置文件位置:利用 which nginx 命令查看配置文件位置
- 打开nginx 的html位置:open /opt/homebrew/Cellar/nginx
检查nginx是否配置成功,浏览器直接输入localhost,即成功,直接找到了nginx的html
浏览器输入:http://localhost/item.html?id=10001,访问某一个商品,带上商品的id信息。
注意:目前这些数据都是假数据,直接写死的,将来这些数据应该是向服务器去做查询
http://localhost/item.html?id=10001请求后端,返回对象item给前端
前端路由页面请求路径:http://localhost/api/item/10001,找不到报错,因为现在还没有配置集群
如下图,当前端发送请求的时候,因为还没有实现这个接口,没有做集群,这里就报错了。
localhost/api/item/10001,并没有端口,是直接请求到了nginx上了,默认是80端口;也就是说,前端请求被nginx拿到了,但是它不能处理,它就把请求代理到后端的nginx业务集群里去,由nginx业务集群完成后续的业务多级缓存处理;
所以还需要在反向代理服务器完成反向代理的配置,即在nginx的conf里编写反向代理-负载均衡到nginx集群的配置
nginx完成反向代理 -nginx集群的配置如下
server 下,location /api ,监听的是listen 80端口请求,
当发现路径是 api下的,会反向代理->找到负载均衡下的路径,该server就是集群了,我自己的conf需要改成我本地的,图片上的server ip+端口是老师的
2、初始Caffeine;它·是一个专业的进程缓存技术
本地内存,是存储在tomact下了,重启或服务宕机,数据就丢失了
咖啡因缓存
案例:
该案例的方法2就是,去缓存取数据,如果有,直接返回,如果没有那就去查数据库
Caffeine 用github的
/*
基本用法测试
*/
@Test
void testBasicOps() {
// 创建缓存对象,newBuilder 构建一个工厂,去创建build一个对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("gf", "迪丽热巴");
// 取数据,不存在则返回null 取数据方法1
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 取数据,不存在则去数据库查询 取数据方法2
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value,函数式编程,return 结果会存到cache 缓存对象里。
// 这个function的作用就是 根据一个key 去找 缓存的值,也就是说可以根据这个key去数据库查询数据了
// 并且可以返回信息
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}
设置缓存了,如果不清理的话,一定会满的,所以要设置定期驱逐策略
- 基于容量策略-清理的是最近不怎么使用的
- 基于时间
注意,两种缓存都不是立即就清理的,需要一点点时间的
咖啡因缓存可以定义一个静态的工具类,将cache暴露出来,可供别人去使用
基于大小设置驱逐策略:要给清理的时间,不给时间会全部打印,是直接运行完,JVM就退出了,根本就没有机会;也就是不是立即清理的
void testEvictByNum() throws InterruptedException {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
// 设置缓存大小上限为 1
.maximumSize(1)
.build();
// 存数据
cache.put("gf1", "柳岩");
cache.put("gf2", "范冰冰");
cache.put("gf3", "迪丽热巴");
// 延迟10ms,给清理线程一点时间,如果不给时间是全都打印了的,还没来得及清理
Thread.sleep(10L);
// 获取数据
System.out.println("gf1: " + cache.getIfPresent("gf1"));
System.out.println("gf2: " + cache.getIfPresent("gf2"));
System.out.println("gf3: " + cache.getIfPresent("gf3"));
}
基于时间设置驱逐缓存
/*
基于时间设置驱逐策略:
*/
@Test
void testEvictByTime() throws InterruptedException {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒
.build();
// 存数据
cache.put("gf", "柳岩");
// 获取数据
System.out.println("gf: " + cache.getIfPresent("gf"));
// 休眠一会儿
Thread.sleep(1200L);
System.out.println("gf: " + cache.getIfPresent("gf"));
}
3、实现进程缓存
新建缓存配置类
/**
* 新建缓存配置类,注入spring里
* 初始化
*/
@Configuration
public class CaffeineConfig {
@Bean
public Cache<Long, Item> itemCaffe() {
// _ 是分读符号
return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();
}
@Bean
public Cache<Long, ItemStock> stockCache() {
return Caffeine.newBuilder().initialCapacity(100).maximumSize(10000).build();
}
}
将方法名注入控制类,并修改查询的代码
@Resource
private Cache<Long, Item> itemCache;
@Resource
private Cache<Long, ItemStock> stockCache;
添加本地缓存
@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id){
// id 其实就是key
return itemCache.get(id, key ->
itemService.query()
// key 其实就是id,只不过在lambda表达式重新命名一下,不然就和id冲突了
.ne("status", 3).eq("id", key)
.one());
}
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id){
return stockCache.get(id, key -> stockService.getById(key));
}
http://localhost:8081/item/10001 浏览器输入;
再第一次执行查询的时候直接查数据库放入本地缓存,以后再执行的话,就直接从本地缓存里找了;
这样我们的JVM进程缓存就实现了
以上我们就实现了Tom的进程缓存
二、Lua语法入门
实现nginx集群业务缓存,利用新的一种语言Lua
1、初识Lua
首先安装LUa
Lua: download 执行如下命令即可安装Lua
我这里分开执行的,一次执行一行语句,最后一行执行权限不足的话就sudo
curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
tar zxf lua-5.4.4.tar.gz
cd lua-5.4.4
make all test
字符串可以是单引号,也可以是双引号
打开控制台,利用vim 命令既可,我们 用touch命令 ,vim编写的时候就创建了
2、变量和循环
table既可以是数组,也可以是map键值对,只不过当以数组的时候,key其实就是下标,不过是从1开始的
输入lua 可以直接进入lua的控制台,方便编写一些命令
变量
- 字符串也是可以拼接的,lua中字符串拼接是用 .. 来拼接的
- local这里代表的是局部变量,就是控制台一行结束后,就没有办法再访问了,不写local就是全局的
循环
- do代表循环开始,end代表循环结束
- iparis代表我要解析这个数组
- 解析数组的时候要形成键值对--index键 value值,区别在于,数组的键其实就是索引,所以用index来表示
- 数组用ipairs,map用pairs,index 、key相当于key,自己定义,这样更清晰明了,让人知道数组的key是index索引下标,map的key那就是个key,可以理解成字符串
在lua.hello里编写脚本
打印,我们发现数组的key就是1,2,3即下标索引
3、条件控制、函数
封装函数。可以返回,也可以不返回 return
案例:
为了健壮性,比如防止空指针等,添加逻辑判断
then代表大括号开始,end代表结束
条件判断为nil也代表false
案例:自定义一个函数,可以打印table,当参数为nil时,打印错误信息
- 如果传了一个nil,那么not nil 就是true;
- nil就是一个无效的值,if语句可以作为false
以上,我们学习Lua的目的就是在Nginx里边去编程,这样就可以实现Nginx里边的业务,比如查询Redis,查询tomact等等,当然这些业务逻辑的编写,我们还需要依赖其他组件,这些组件,我们需要利用到一个工具openResty来帮我们实现
三、多级缓存
1、安装openResty
openResty也不安装了,以后有需要再说吧。
老师的openResty虚拟机地址192.168.150.101:端口是8081
2、OpenResty快速入门
如何在openResty里接收:localhost/api/item/100001 这个请求呢?
openResty可以实现查询Redis和tomact
如何访问?得加载模块,以.lua结尾的,就是访问lua模块,固定写法
写数据到浏览器,ngx.say(),就相当于利用Response将结果接到浏览器,写一个假数据
写一个假数据
总结配置文件conf,就是编写一个listen 去监听某一个端口,编写一个location 去监听某一个路径,监听路径以后去做反向代理,这里我们监听以后是交给lua文件去处理。然后再lua文件里去编写业务逻辑,
监听路径--可以理解为一个controller
监听到了以后-- lua文件就相当于业务层service
3、请求参数处理
如何从openResty里去获取请求参数呢?
1、~ 波浪线代表的就是后边跟的是正则表达式匹配,而且注意两边都要有空格()是一组,如果将来正则表达式有多个,即多个参数,那么数组的值就有多个,如果1个,那么数组就有一个,下标从1开始,利用数组来获取路径中的参数
2、返回值是一个table,键值对,key-value形式
案例
路径占位符获取参数,正则形式,添加波浪线,添加正则
参数会存到一个变量里,回到lua文件 ,从数组中获取参数,下标从1开始 ,注意,前边两个 ..是拼接前边的字符串,后边的两个 .. 是拼接后边的字符串
以上即可实现请求参数的获取
4、查询Tomact
实现对商品数据的查询
1、我们的前端请求映射到nginx上,反向代理到openResty上,准备好了
2、tomact查询数据库本地缓存也准备好了
但是openResty接收到请求,要查询数据,从哪里查呢?按照我们的逻辑是先查缓存,缓存没有了再查tomact。但是呢,我们的缓存数据又从何而来呢?得先查了tomact后,把数据存到 缓存里去
所以我们先不做缓存,先实现对tomct的查询,将来再把查到的数据放到缓存里去
openResty集群是在虚拟机上,而tomact实在windows上,ip地址是不一样的
总结:不管虚拟机地址是什么,只要管它的前3位就行了即:这里的是192.168.150,然后只要把最后一位替换成1,那么一定得到的就是我们windows电脑的地址,前提是防火墙得关闭,一定成立
案例:查询分成了两步,因为是两张表,但是页面上要的所有数据,所以还需要组装这两张表的数据
capture 捕获,就是捕获一个请求,两个参数
- 第一个参数path,就是请求的路径
- 第二个参数是table,注意,get/post方式只能二选一
返回值是一个response的对象resp
而且需要注意,/path这个请求不包含ip+端口,也就是说它没有发送到任何一个地方,而是被nginx自己监听到了,而我们的最终目的是发个tomact,即是发个tomact所在的ip和端口,然后在反向代理到tomact的ip和端口就ok了;
resp.body就是返回响应内容json的字符串
openResty的niginx 的conf需要再添加一个监听 location /item 即,所有item的请求都会被我监听到。即:凡是向item请求,一定会到达我们的tomact
以后凡是请求以item开头,请求一定会到达tomact的,因为如下
这套代码我们会经常用,最少这里就得两次,查商品一次,查库存一次,那就封装成一个函数
如下图是封装的lua模块,这个模块下的lua都会被加下,所以我们再lualib里定义一个文件,后缀名是lua,则该文件也一定会被加载 ,而lua文件里的函数,也一定会被加载,这就相当于变成了一个通用的工具了,所以叫common.lua
最后 一步,将方法导出。。ngx.exit(404),退出并返回状态码
以上主要是学习,如果在openResty里来发起一个http请求
luaLib下的包都是库,lua库,我们编写的方法就可以放进去,引入库的话,如果是在lualib下直接引入文件名即可,如果还有目录就得再加上它自己的目录
如图,我们的common.lua是直接在lualib下的,所以可以之久拿,如果是在ngx下,还需要加上nvx/文件名
拼接两个json需要一个工具包,就是上图的cjson.so,反序列化就是转成table
item.lua文件代码如下
这样销量和库存就都有了
以上代码即可实现从openResty向tomact发送http请求 ,返回页面完成渲染
tomact实现负载均衡
以上代码是有一台tomact的情况,如果有多台tomact就需要做集群,而tomact集群是有轮巡机制的,就是说某一个请求从第一台开始访问,再来就是第二台,再来就是第三台,而tomact缓存是不共享的,每次来都得重新查库,除非轮巡完了所有的tomact,再次从第一台开始才会拿到缓存
而且多台tomact都缓存一个数据,那冗余就太多了。
如果说来了一个请求,第一次查询以后,永远都有缓存,那么必须保证该请求每次都指向同一台tomact服务器,这样才能保证缓存一直生效
就是说1001请求来了以后第一次走的是第一台服务器,那么它永远都走第一台服务器;1002来了轮巡走第二台服务器,那么它永远都走第二台服务器,(当然也可以一样的服务器)我们现在是轮巡肯定做不到,那么就需要修改nginx负载均衡的算法了
添加负载均衡 添加一行代码:hash $request_uri,request_uri就是请求路径,hash就是对请求路径做hash运算,得到hash值后再对tomact服务器的数量取余,那么只要路径不变,服务器的数量不变那么一个请求访问的永远是同一台服务器了,注意,这里数组都是从1开始
修改配置文件如下,添加tomact集群负载均衡 ,tomact进程缓存就是JVM缓存
以上配置即可实现对tomact负载均衡,并实现缓存永远生效
5、Redis缓存预热
openResty应该优先查询Redis,在Redis缓存未命中时,再去查询tomact
定义一个配置类bean,实现一个spring的一个bean,继承的方法就是在项目启动时,就会执行
项目在启动的时候,就会将数据写到redis当中了
- initializineBean 凡是实现这个接口的,就要实现afterPropertiesSet这个方法,这个方法会在bean创建完(项目启动的时候创建),@Autowird注入成功后,去执行,他就可以在项目启动的时候去执行了。就可以是实现缓存预热效果了
- ObjectMapper是spring的默认json序列化工具
6、查询Redis缓存
openResty是如何去操作Redis的呢?
导入redis库,因为不是根目录了,需要加层级目录;和导入cjson是一样的道理
三个1000分别是: 建立请求的、发送响应的、接收请求的超时时间,单位毫秒,也就是1秒
成功返回ok,失败返回的是nil err是返回错误信息
下边的red:get(key)只能实现最简单的字符串查询
定义方法
方法定义好了需要暴漏出来
将查询商品和库存都封装成一个方法
测试可以将tomact停调,也能正常查询,就是走的redis了
7、Nginx本地查询
如何在openResty里添加一个本地缓存呢?
注意,该字典只能实现一个openResy的多台nginx共享,如果部署了多个openResty ,那么多个openResty之间是不共享的
添加共享词典
item.lua导入共享词典,业务都是在item.lua写的
优先查nginx本地缓存,没有去查redis,没有去查tomact,没有去查db
查到了后将缓存写到nginx里
启动发现报错,记录日志需要加
修改一下,错误日志都需要家ngx.ERR
修改参数,添加本地缓存过期时间
再刷新页面,执行查看id=10001查看日志,第一次nginx肯定没有
再次刷新页面查看日志,就不会再打这个日志了,说明从nginx本地成功拿到了缓存
四、缓存同步策略
实现了多级缓存,大大提高了性能,但是缓存在提高性能的同时,也带来了一致性的问题
比如说如果数据库进行了修改,那么缓存还用原来的就有了不一致性,如何来保证数据库与缓存的一致性呢?
常见的三种方式:
canal 异步通知,它是0侵入的,不用动代码,效率也高
我们这里不用MQ,canal可以监听数据库的变化
Canal
原理:是基于mysql的主从同步来实现的
什么是mysql主从同步?
- 主节点在做数据增删改时,就会去记录到二进制日志文件-就是记录的执行业务sql
- salve会开启一个线程不断去读区该日志文件,读取来之后,放到自己的日志文件里
- slave会在开启一个线程去不断重放(执行)这些日志文件里的操作(也就是说主节点做了哪些sql,从节点也会相应去执行这些sql)
安装Canal
重启以后,发现 多了一个mysql-bin.000001,这个二进制文件会越来越多,编号也会越来越大
主从同步还需要给slave设置一个权限
-- 创建一个全新的用户
create user canal@'%' IDENTIFIED by 'canal';
-- 授权
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
-- 刷新权限
FLUSH PRIVILEGES;
position就是一个便宜量,从库需要知道主库偏移量的位置,必须是小于等于,说明需要同步
以上即开启了主从同步了
然后运行命令创建Canal容器:
docker run -p 11111:11111 --name canal \
-- canal 集群名称,canal也是可以搭建集群的
-e canal.destinations=heima \
-- mysql的主节点地址,这里没有写ip是因为上面操作的mysql和canal在同一个网络
-- 而docker容器在同一个网络时可以用容器名字互联
-e canal.instance.master.address=mysql:3306 \
-- 以下是做数据同步时用户名 等 这些信息我们在上边mysql执行创建用户时已经创建好了
-e canal.instance.dbUsername=canal \
-- 密码
-e canal.instance.dbPassword=canal \
-- 编码
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false \
-- canal 将来做主从同步时,监听哪个库,
-- \..* \黑马库下所有的表
-e canal.instance.filter.regex=heima\\..* \
-- 连上黑马的网络,
--network heima \
-d canal/canal-server:v1.1.5
命令行输入:docker logs -f canal 查看日志,启动,启动成功
启动成功后如何知道canal 与 mysql有没有建立连接呢?
通过:docker exec -it canal bash 进入cannl容器内部,可以去查看日志
canal如何取通知更新呢?
destinaltion 集群名称
编写监听器,监听,完成redis缓存的增删改
JVM缓存的增删改,也放到这里
先写JVM缓存,JVM缓存效率更高
课程总结
openResty集群也是轮巡操作的,多台不共享,可以增加配置,即:同一个id永远访问的是同一台openResty,和tomact集群配置一个道理