烟台网站搭建,基于h5的个人网站建设,央视优购物官方网站,2015做那些网站能致富哈喽~大家好#xff0c;这篇来看看多级缓存。 #x1f947;个人主页#xff1a;个人主页 #x1f948; 系列专栏#xff1a;【微服务】 #x1f949;与这篇相关的文章#xff1a; JAVA进程和线程JAVA进程和线程-CSDN博客Http… 哈喽~大家好这篇来看看多级缓存。 个人主页个人主页 系列专栏【微服务】 与这篇相关的文章 JAVA进程和线程JAVA进程和线程-CSDN博客HttpClient 入门使用示例HttpClient 入门使用示例-CSDN博客Spring Task 快速入门Spring Task 快速入门-CSDN博客 目录 一、前言
1、什么是多级缓存?
2、集群模式
3、前期准备
二、Caffeine
1、什么是Caffeine
2、缓存使用的基本API
2.1、基于大小设置驱逐策略
2.2、基于时间设置驱逐策略
三、实现多级缓存
1、前期准备
2、反向代理流程
3、OpenResty监听请求
4、代码解析
4.1、获取参数的API
4.2、查询Tomcat
4.3、CJSON工具类
4.4、基于ID负载均衡
4.5、Redis缓存预热
四、缓存同步
1、数据同步策略
2、监听Canal 一、前言
1、什么是多级缓存?
传统的缓存策略一般是请求到达Tomcat后先查询Redis如果未命中则查询数据库这个是没有问题的但是这存在一些问题请求要经过Tomcat处理Tomcat的性能成为整个系统的瓶颈 Redis缓存失效时大量的数据操作会对数据库产生冲击 。
那么多级缓存就是充分利用请求处理的每个环节分别添加缓存减轻Tomcat压力提升服务性能。 浏览器访问静态资源时优先读取浏览器本地缓存 访问非静态资源ajax查询数据时访问服务端 请求到达Nginx后优先读取Nginx本地缓存 如果Nginx本地缓存未命中则去直接查询Redis不经过Tomcat 如果Redis查询未命中则查询Tomcat 请求进入Tomcat后优先查询JVM进程缓存 如果JVM进程缓存未命中则查询数据库
在多级缓存架构中nginx是一个编写业务的Web服务器不是作为反向代理的服务器了。
2、集群模式
也就是说nginx与tomcat服务要部署为集群模式。
3、前期准备
准备好需要的素材部署好nginx注将其拷贝到一个非中文目录下 打开conf里面的nginx.conf配置文件编写好关键配置nginx集群的ip地址:端口号监听/api路径反向代理到nginx集群。
此时 192.168.227.131 是我虚拟机的ip地址这里你写的时候记得换上自己的
二、Caffeine
1、什么是Caffeine
Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址GitHub - ben-manes/caffeine: A high performance caching library for Java
缓存在日常开发中启动至关重要的作用 能大量减少对数据库的访问减少数据库的压力 我们把缓存分为两类 分布式缓存例如Redis 优点存储容量更大、可靠性更好、可以在集群间共享 缺点访问缓存有网络开销 场景缓存数据量较大、可靠性要求较高、需要在集群间共享 进程本地缓存例如HashMap、GuavaCache 优点读取本地内存没有网络开销速度更快 缺点存储容量有限、可靠性较低、无法共享 场景性能要求较高缓存数据量较小
我们的思路是当我们的请求到nginx中首先先查询本地缓存当本地缓存没有时再去查询redisredis没有时再去查询jvm进程当这些都没有命中时再最后查数据库。
2、缓存使用的基本API Test
void testBasicOps() {// 构建cache对象CacheString, String cache Caffeine.newBuilder().build();// 存数据cache.put(gf, ddf);// 取数据String gf cache.getIfPresent(gf);System.out.println(gf gf);// 取数据包含两个参数// 参数一缓存的key// 参数二Lambda表达式表达式参数就是缓存的key方法体是查询数据库的逻辑// 优先根据key查询JVM缓存如果未命中则执行参数二的Lambda表达式String defaultGF cache.get(defaultGF, key - {// 根据key去数据库查询数据return asdSystem.out.println(defaultGF defaultGF);
}
Caffeine提供了三种缓存驱逐策略 基于容量设置缓存的数量上限 // 创建缓存对象
CacheString, String cache Caffeine.newBuilder().maximumSize(1) // 设置缓存大小上限为 1.build(); 基于时间设置缓存的有效时间 // 创建缓存对象
CacheString, String cache Caffeine.newBuilder()// 设置缓存有效期为 10 秒从最后一次写入开始计时 .expireAfterWrite(Duration.ofSeconds(10)) .build(); 基于引用设置缓存为软引用或弱引用利用GC来回收缓存数据。性能较差不建议使用。
2.1、基于大小设置驱逐策略 Testvoid testEvictByNum() throws InterruptedException {// 创建缓存对象CacheString, String cache Caffeine.newBuilder()// 设置缓存大小上限为 1.maximumSize(1).build();// 存数据cache.put(gf1, a);cache.put(gf2, b);cache.put(gf3, c);// 延迟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));}
2.2、基于时间设置驱逐策略 Testvoid testEvictByTime() throws InterruptedException {// 创建缓存对象CacheString, String cache Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒.build();// 存数据cache.put(gf, aaa);// 获取数据System.out.println(gf: cache.getIfPresent(gf));// 休眠一会儿Thread.sleep(1200L);System.out.println(gf: cache.getIfPresent(gf));} 三、实现多级缓存
1、前期准备
多级缓存的实现离不开Nginx编程而Nginx编程又离不开OpenResty。
下载与安装步骤这里就不做过多的描述了OpenResty底层是基于Nginx的查看OpenResty目录的nginx目录所以运行方式与nginx基本一致 # 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop 修改/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;}}
} 2、反向代理流程
打开案例他的请求路径是这个【微服务】
请求地址是localhost端口是80就被windows上安装的Nginx服务给接收到了。然后代理给了OpenResty集群这就是ip为192.168.227.131。
3、OpenResty监听请求
OpenResty的很多功能都依赖于其目录下的Lua库需要在nginx.conf中指定依赖库的目录
修改/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;;; 监听/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)做路径映射而返回类型就是json。
而content_by_lua_file lua/item.lua则相当于调用item.lua这个文件执行其中的业务把结果返回给用户。相当于java中调用service。
在/usr/loca/openresty/nginx目录创建文件夹lua在/usr/loca/openresty/nginx/lua文件夹下新建文件item.lua。
item.lua代码 -- 导入common函数库
local common require(common)
local read_http common.read_http
local read_redis common.read_redis
-- 导入cjson库
local cjson require(cjson)
-- 导入item_cache
local item_cache ngx.shared.item_cache-- 封装查询函数
function read_data(key, expire, path, params)local var item_cache:get(key)if not var thenngx.log(ngx.ERR, 本地缓存查询失败尝试查询redis key: , key)-- 查询redis缓存var read_redis(127.0.0.1, 6379, key)-- 判断查询结果if not var thenngx.log(ngx.ERR, redis查询失败尝试查询http key: , key)-- redis查询失败去查询httpvar read_http(path, params)endend-- 查询成功根据不同的数据设置不同的缓存时间,并且写入到本地缓存item_cache:set(key, var, expire)-- 返回数据return var
end-- 获取路径参数
local id ngx.var[1]-- 查询商品信息
local itemJSON read_data(item:id: .. id, 1800, /item/ .. id, nil)
-- 查询库存信息
local stockJSON read_data(item:stock:id: .. id, 60, /item/stock/ .. id, nil)-- JSON转化为lua的table
local item cjson.decode(itemJSON)
local stock cjson.decode(stockJSON)
-- 组合数据
item.stock stock.stock
item.sold stock.sold-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item)) 在nginx.cpnf里面添加 # 添加反向代理到windows的Java服务# 该指令是用来设置代理服务器的地址可以是主机名称IP地址加端口号等形式。location /item {proxy_pass http://tomcat-cluster;} upstream tomcat-cluster{hash $request_uri;server 192.168.177.196:8081;server 192.168.177.196:8082;} common.lua 代码 -- 导入redis
local redis require(resty.redis)
-- 初始化 redis
local red redis:new()
red:set_timeouts(1000, 1000, 1000)-- 关闭redis连接的工具方法其实是放入连接池
local function close_redis(red)local pool_max_idle_time 10000 -- 连接的空闲时间单位是毫秒local pool_size 100 --连接池大小local ok, err red:set_keepalive(pool_max_idle_time, pool_size)if not ok thenngx.log(ngx.ERR, 放入redis连接池失败: , err)end
end-- 查询redis的方法 ip和port是redis地址key是查询的key
local function read_redis(ip, port, key)-- 获取一个连接local ok, err red:connect(ip, port)if not ok thenngx.log(ngx.ERR, 连接redis失败 : , err)return nilend-- 查询redislocal resp, err red:get(key)-- 查询失败处理if not resp thenngx.log(ngx.ERR, 查询Redis失败: , err, , key , key)end--得到的数据为空处理if resp ngx.null thenresp nilngx.log(ngx.ERR, 查询Redis数据为空, key , key)endclose_redis(red)return resp
end-- 封装函数发送http请求并解析响应( ngx.location.capture)
local function read_http(path, params)local resp ngx.location.capture(path,{method ngx.HTTP_GET,args params,})if not resp then-- 记录错误信息返回404ngx.log(ngx.ERR, http请求查询失败, path: , path , , args: , args)ngx.exit(404)endreturn resp.body
end
-- 将方法导出
local _M { read_http read_http,read_redis read_redis
}
return _M 然后重新加载配置nginx -s reload。
4、代码解析
4.1、获取参数的API
OpenResty中提供了一些API用来获取不同类型的前端请求参数
location ~ /api/item/(\d) { # 默认的响应类型 default_type application/json; # 响应结果由lua/item.lua文件来决定 content_by_lua_file lua/item.lua; }
里面的 ~ /api/item/(\d) 对应的就是 http://localhost/api/item/10003 前端发来的路径这里拿到了商品的id
4.2、查询Tomcat
拿到商品ID后本应去缓存中查询商品信息不过目前我们还未建立nginx、redis缓存。因此这里我们先根据商品id去tomcat查询商品信息。
发送http请求的API
举个例子 local resp ngx.location.capture(/path,{method ngx.HTTP_GET, -- 请求方式args {a1,b2}, -- get方式传参数
}) 返回的响应内容包括 resp.status响应状态码 resp.header响应头是一个table resp.body响应体就是响应数据
注意这里的path是路径并不包含IP和端口。这个请求会被nginx内部的server监听并处理。
但是我们希望这个请求发送到Tomcat服务器所以还需要编写一个server来对这个路径做反向代理 location /path {# 这里是windows电脑的ip和Java服务端口需要确保windows防火墙处于关闭状态proxy_pass http://你自己的ip:8081; } 在item.lua文件当中有这一串 -- 引入自定义common工具模块返回值是common中返回的 _M
local common require(common)
-- 从 common中获取read_http这个函数
local read_http common.read_http
-- 获取路径参数
local id ngx.var[1]
-- 根据id查询商品
local itemJSON read_http(/item/.. id, nil)
-- 根据id查询商品库存
local itemStockJSON read_http(/item/stock/.. id, nil)
ngx.say(itemStockJSON ) 他的作用是接受到请求路径然后根据id来查询数据库返回json数据。
里查询到的结果是json字符串并且包含商品、库存两个json字符串页面最终需要的是把两个json拼接为一个json
这就需要我们先把JSON变为lua的table完成数据整合后再转为JSON序列化与反序列化。
4.3、CJSON工具类
OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。
举个例子
引入cjson模块 local cjson require cjson 序列化 local obj {name jack,age 21
}
-- 把 table 序列化为 json
local json cjson.encode(obj) 反序列化 local json {name: jack, age: 21}
-- 反序列化 json为 table
local obj cjson.decode(json);
print(obj.name) 那么实现Tomcat查询是 -- 导入common函数库
local common require(common)
local read_http common.read_http
-- 导入cjson库
local cjson require(cjson)-- 获取路径参数
local id ngx.var[1]
-- 根据id查询商品
local itemJSON read_http(/item/.. id, nil)
-- 根据id查询商品库存
local itemStockJSON read_http(/item/stock/.. id, nil)-- JSON转化为lua的table
local item cjson.decode(itemJSON)
local stock cjson.decode(stockJSON)-- 组合数据
item.stock stock.stock
item.sold stock.sold-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item)) 4.4、基于ID负载均衡
刚才的代码中我们的tomcat是单机部署。而实际开发中tomcat一定是集群模式因此OpenResty需要对tomcat集群做负载均衡。
如何做
如果能让同一个商品每次查询时都访问同一个tomcat服务那么JVM缓存就一定能生效了。
也就是说我们需要根据商品id做负载均衡而不是轮询。
思路
nginx根据请求路径做hash运算把得到的数值对tomcat服务的数量取余余数是几就访问第几个服务实现负载均衡。
举个例子 我们的请求路径是 /item/10001 tomcat总数为2台8081、8082 对请求路径/item/1001做hash运算求余的结果为1 则访问第一个tomcat服务也就是8081
只要id不变每次hash运算结果也不会变那就可以保证同一个商品一直访问同一个tomcat服务确保JVM缓存生效。
在nginx.conf文件里面添加这一段hash $request_uri; upstream tomcat-cluster{hash $request_uri;server 192.168.177.196:8081;server 192.168.177.196:8082;} 然后修改对tomcat服务的反向代理目标指向tomcat集群 location /item {proxy_pass http://tomcat-cluster;
} 重新加载OpenResty nginx -s reload 4.5、Redis缓存预热
Redis缓存会面临冷启动问题
冷启动服务刚刚启动时Redis中并没有缓存如果所有商品数据都在第一次查询时添加缓存可能会给数据库带来较大压力。
缓存预热在实际开发中我们可以利用大数据统计用户访问的热点数据在项目启动时将这些热点数据提前查询并保存到Redis中。
由于数据较少所以这里将所有的数据都存入缓存中。
具体代码 Component
public class RedisHandler implements InitializingBean {Autowiredprivate StringRedisTemplate redisTemplate;Autowiredprivate IItemService itemService;Autowiredprivate IItemStockService itemStockService;/*** Jackson提供了ObjectMapper来供程序员“定制化控制”序列化、反序列化的过程。* objectMapper在调用writeValue()序列化 或 调用readValue()反序列化方法之前* 往往需要设置 ObjectMapper 的相关配置信息这些配置信息作用在 java 对象的所有属性上* 表示在进行序列化和反序列化时进行一些特殊的处理。*/private static final ObjectMapper MAPPER new ObjectMapper();Overridepublic void afterPropertiesSet() throws Exception {// 查询商品ListItem itemList itemService.list();// 商品集合序列化存入redisfor (Item item : itemList) {String itemJson MAPPER.writeValueAsString(item);redisTemplate.opsForValue().set(item:id: item.getId(), itemJson);}// 查询库存ListItemStock stockList itemStockService.list();// 库存集合序列化存入redisfor (ItemStock stock : stockList) {String stockJson MAPPER.writeValueAsString(stock);redisTemplate.opsForValue().set(item:stock:id: stock.getId(), stockJson);}}public void save(Item item){try {String itemJson MAPPER.writeValueAsString(item);redisTemplate.opsForValue().set(item:id: item.getId(), itemJson);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}public void delete(Long id){redisTemplate.delete(item:id: id);}} InitializingBean接口为bean提供了初始化方法的方式它只包括afterPropertiesSet方法凡是继承该接口的类在初始化bean的时候都会执行该方法。
ObjectMapperJackson提供了ObjectMapper来供程序员“定制化控制”序列化、反序列化的过程。objectMapper在调用writeValue()序列化 或 调用readValue()反序列化方法之前往往需要设置 ObjectMapper 的相关配置信息这些配置信息作用在 java 对象的所有属性上表示在进行序列化和反序列化时进行一些特殊的处理。
四、缓存同步
大多数情况下浏览器查询到的都是缓存数据当我们管理员修改数据时缓存没有及时更新这就会出大问题了。
所以我们必须保证数据库数据、缓存数据的一致性这就是缓存与数据库的同步。
1、数据同步策略
设置有效期给缓存设置有效期到期后自动删除。再次查询时更新 优势简单、方便 缺点时效性差缓存过期之前可能不一致 场景更新频率较低时效性要求低的业务
同步双写在修改数据库的同时直接修改缓存 优势时效性强缓存与数据库强一致 缺点有代码侵入耦合度高 场景对一致性、时效性要求较高的缓存数据
异步通知修改数据库时发送事件通知相关服务监听到通知后修改缓存数据 优势低耦合可以同时通知多个缓存服务 缺点时效性一般可能存在中间不一致状态 场景时效性要求一般有多个服务需要同步
这里我们使用Canal基于Canal的通知
2、监听Canal
Canal提供了各种语言的客户端当Canal监听到binlog变化时会通知Canal的客户端。
我们可以利用Canal提供的Java客户端监听Canal通知消息。当收到变化的消息时完成对缓存的更新。
引入依赖 dependencygroupIdtop.javatool/groupIdartifactIdcanal-spring-boot-starter/artifactIdversion1.2.1-RELEASE/version
/dependency 编写配置 canal:destination: heima # canal的集群名字要与安装canal时设置的名称一致server: 192.168.150.101:11111 # canal服务地址 修改实体类 Data
TableName(tb_item)
public class Item {TableId(type IdType.AUTO)Idprivate Long id;//商品idColumn(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)Transientprivate Integer stock;TableField(exist false)Transientprivate Integer sold;
} TableName(tb_item)要监听的表名
Id告诉他谁是id主键
Column(name name)当DB里面的字段与实体类对应不上时用name对应。
Transient告诉它谁不是表中的字段。
编写监听器
通过实现EntryHandlerT接口编写监听器监听Canal消息。注意两点 实现类通过CanalTable(tb_item)指定监听的表信息 EntryHandler的泛型是与表对应的实体类 CanalTable(tb_item)
Component
public class ItemHandler implements EntryHandlerItem {Autowiredprivate RedisHandler redisHandler;Autowiredprivate CacheLong, Item itemCache;Overridepublic void insert(Item item) {// 写数据到JVM进程缓存itemCache.put(item.getId(), item);// 写数据到redisredisHandler.saveItem(item);}Overridepublic void update(Item before, Item after) {// 写数据到JVM进程缓存itemCache.put(after.getId(), after);// 写数据到redisredisHandler.saveItem(after);}Overridepublic void delete(Item item) {// 删除数据到JVM进程缓存itemCache.invalidate(item.getId());// 删除数据到redisredisHandler.deleteItemById(item.getId());}
} 不积跬步无以至千里趁年轻使劲拼给未来的自己一个交代向着明天更好的自己前进吧