缓存的作用是减轻数据库的压力,缩短服务相应的时间,从而提高整个并发的能力,Redis单节并发以及很高了,但是依然有上限,随着互联网的发展,用户体量越来越大,比如淘宝京东的流量能达到数亿级别的流量。那么多级缓存就是为了应对多级缓存高并发。
用户请求到达Tomcat服务器,然后优先查询redis,如果redis命中,直接返回。未命中就访问数据库。 也能很大的程度减少数据库压力。
问题:
多级缓存就是充分利用请求处理的每个缓存,分别添加缓存,减轻Tomcat的压力,提升服务性能:
因为浏览器可以把返回的静态资源缓存到本地的,那么下次用户访问服务器时,只需要检查有没有变化,没有变化服务器直接返回304状态码,不用返回数据了。304:说明本地有。直接渲染本地存着的页面。 减少数据的传输,提高渲染和相应的速度。
在Nginx内部去实现对redis、Tomcat的访问等等的编写,不再单单是业务代理服务器了,变成了web业务服务了,在里面写业务逻辑了。
做成集群:一个Nginx做反向代理,集群做本地缓存,做业务的Nginx的服务器。
解决的问题:
Tomcat服务内部添加缓存, 业务进来以后优先查询进程缓存,缓存未命中,在去查数据库。
后期做数据同步需要用到MySQL的主从功能,所以需要大家在虚拟机中,利用Docker来运行一个MySQL容器。
# 进入/tmp目录
cd /tmp
# 创建文件夹
mkdir mysql
# 进入mysql目录
cd mysql
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.25
docker查看当前运行的容器。docker ps
-a是查看所有容器
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
添加后:并重启mysql: docker restart mysql
CREATE TABLE `tb_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',
`title` varchar(264) NOT NULL COMMENT '商品标题',
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '商品名称',
`price` bigint(20) NOT NULL COMMENT '价格(分)',
`image` varchar(200) DEFAULT NULL COMMENT '商品图片',
`category` varchar(200) DEFAULT NULL COMMENT '类目名称',
`brand` varchar(100) DEFAULT NULL COMMENT '品牌名称',
`spec` varchar(200) DEFAULT NULL COMMENT '规格',
`status` int(1) DEFAULT '1' COMMENT '商品状态 1-正常,2-下架,3-删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `status` (`status`) USING BTREE,
KEY `updated` (`update_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=50002 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='商品表';
CREATE TABLE `tb_item_stock` (
`item_id` bigint(20) NOT NULL COMMENT '商品id,关联tb_item表',
`stock` int(10) NOT NULL DEFAULT '9999' COMMENT '商品库存',
`sold` int(10) NOT NULL DEFAULT '0' COMMENT '商品销量',
PRIMARY KEY (`item_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
为什么要做两张表格呢?
一个商品的数量是非常多的。
一方面数据解耦,字段太多查询效率太低了。
另一方面需要数据需要缓存,那么一条整个数据去做缓存,如果一个字段做了修改,那么整条数据全部失效了,就得所有信息都得去数据库做加载。
例如:将所有信息存入一张表中,那么库存改了以后,就得重新加载该条数据。 那么如果将分为多个表,当库存修改了以后,只需要将库存表进行加载即可,不需要获取所有的商品信息了。
基于mybatis-plus快速实现单表的增删改查:
修改后,启动服务,访问:http://localhost:8081/item/10001即可查询数据
商品查询是购物页面,与商品管理的页面是分离的。
商品查询页面放在Nginx反向代理服务器上面,作为静态资源服务器,用户来请求商品页面的时候,Nginx返给用户。先返回的只是静态页面,数据再从后台查询了【Nginx本地、redis、Tomcat、数据库】
部署方式如图:
我们需要准备一个反向代理的nginx服务器,来部署静态资源。如上图红框所示,将静态的商品页面放到nginx目录中。
页面需要的数据通过ajax向服务端(nginx业务集群)查询。
将其拷贝到一个非中文目录下,运行这个nginx服务。
运行命令:
start nginx.exe
然后访问 http://localhost/item.html?id=10001即可:
请求地址是:localhost/api/item/10001
并没有添加端口,请求到了80端口的nginx服务器了
用户请求被Nginx反向代理拿到了,它不处理,而是代理到nginx业务集群中去,在去做多级缓存。 所以要完成反向代理的配置
nginx/conf/nginx.conf
location /api
以api开头的请求,就被Nginx配置拦截到了,反向代理到负载均衡的配置:http://nginx-cluster
nginx-cluster:是业务集群,本地缓存、redis缓存、Tomcat缓存
现在,页面是假数据展示的。我们需要向服务器发送ajax请求,查询商品数据。
打开控制台,可以看到页面有发起ajax查询数据:
而这个请求地址同样是80端口,所以被当前的nginx反向代理了。
查看nginx的conf目录下的nginx.conf文件:
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
# nginx的业务集群,做Nginx本地缓存、redis缓存、Tomcat缓存
upstream nginx-cluster{
server 192.168.75.111:8081;
}
server {
listen 80;
server_name localhost;
location /api {
proxy_pass http://nginx-cluster;
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:
分布式缓存:例如redis
独立于Tomcat之外的,Tomcat访问redis时要发起网络请求,所以说有网络开销。
进程本地缓存:例如hashMap、GuavaCache
专业的进程缓存技术。
Caffeine是一个基于java8开发的,提供了近乎最佳命中率的高性能的本地缓存。目前Spring内部的缓存使用的就是Caffeine。
官网:ttps://github.com/ben-manes/caffeine
官方读写性能测试:
通过下面的例子进行学习Caffeine的使用:
/*
基本用法测试
*/
@Test
void testBasicOps() {
// 创建缓存对象 。 创建工厂,在build构建出缓存对象。
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据 gril frend女朋友的意思 key,value的形式
cache.put("gf", "迪丽热巴");
// 取数据,不存在则返回null
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 取数据,不存在则去数据库查询
// 根据Key查缓存未命中,再去查数据库。存入数据库、在返回
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}
Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。
Caffeine提供了三种缓存驱逐策略:
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1) // 设置缓存大小上限为 1
.build();
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
// 设置缓存有效期为 10 秒,从最后一次写入开始计时
.expireAfterWrite(Duration.ofSeconds(10))
.build();
注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
案例:实现商品的查询本地进程缓存
利用Caffeien实现下列需求:
在item-service的com.heima.item.config
包下定义CaffeineConfig
类:
package com.heima.item.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CaffeineConfig {
@Bean
public Cache<Long, Item> itemCache(){
return Caffeine.newBuilder()
// 初始化大小为100
.initialCapacity(100)
// 上限是10000,分隔符方便读数据
.maximumSize(10_000)
.build();
}
@Bean
public Cache<Long, ItemStock> itemStockCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
}
com.heima.item.web
包下的ItemController类,添加缓存逻辑: @Autowired
private Cache<Long,Item> itemCache;
@Autowired
private Cache<Long,ItemStock> itemStockCache;
//根据id查询商品
@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id){
// .get(先去缓存查数据,未中去数据库查缓存) key==id
return itemCache.get(id,key->itemService.query()
// ne 是不等于。状态码不等于3的
.ne("status", 3)
//eq 是等于 数据库中的“id" == key 也就是等等与前端传过来的Id
.eq("id", key)
// 查一条数据过来
.one()
);
}
//根据id查询库存
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id){
return itemStockCache.get(id,key->stockService.getById(key));
}
}
当添加缓存之前,每当刷新一下页面,就会去数据库进行查询一次:
添加缓存之后,只去数据库查询一次,再重新刷新页面,不会去数据库进行查询了,而是直接返回缓存信息即可。