Java缓存机制
缓存是什么?
凡是位于速度相差较大的两种硬件/软件之间的,用于协调两者数据传输速度差异的结构,均可称之为 Cache;缓存是空间换时间的方案
缓存分类
操作系统磁盘缓存-减少磁盘机械操作
操作系统会把经常访问到的文件内容放入到内存当中,由文件系统来管理;特殊的应用程序会自己实现disk cache
数据库缓存-减少文件系统I/O
- 数据库至关重要,保存数据量十分大,查询操作十分频繁,有时还很复杂,导致数据库性能低下
- data buffer是数据库数据在内存中的容器,命中率决定数据库性能,越大越好,多多益善,MySQL的innoDB buffer:innodb_buffer_pool_size=2G,mysql建议buffer pool开大到服务器物理内存60-80%
应用程序缓存-减少对数据库的查询
- 对象缓存,由诸如Hibernate等框架提供,细颗粒度缓存查询结果,无需业务代码显式编程,极大降低web对数据库的访问请求
- 查询缓存,对数据库结果集进行缓存,类似数据库的Query Cache,适用一些耗时,但是时效性要求较低的场景。当查询结果集涉及的表记录被修改以后,要注意清理缓存
- 页面缓存
作用:降低服务器压力,提高页面渲染速度,难点在于如何清理过期缓存
分类:动态页面静态化,servlet缓存,页面内部缓存
web服务器缓存-减少应用服务器请求
基于代理服务器模式的web服务器端缓存,如squid/nginx;web服务器缓存技术被用来实现cdn;不需要编程,但仅限于新闻发布类网站,页面实时性要求不高
客户端浏览器缓存-减少对网站的访问
基于AJAX的浏览器缓存,使用AJAX调用的时候,将数据库在浏览器端缓存;只要不离开当前页面,不刷新当前页面,就可以直接读取缓存数据;只适用于使用AJAX技术的页面
Java中使用缓存的方式
1.使用Map集合
推荐使用单例模式,设置map缓存
- LinkedHashMap实现缓存
它是一个非线程安全的集合,它默认是按存储的顺序进行排序,而且还有一个自带的移除方法:
它本身就是返回false的,所以只要在子类中重写这个方法就可以按照我们自己的定义去实现它。同时LinkedHashMap还有一个天然的LRU机制,它支持访问过的数据放在最前面,以下是它的一个源码构造函数protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
默认情况下,accessOrder是false的,如果设置成true就可以达到访问过的数据就排到最前面。public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
1.FIFO实现
package com.brickworkers;
import java.util.LinkedHashMap;
public class FIFOCache<K,V> extends LinkedHashMap<K, V>{
private static final long serialVersionUID = 436014030358073695L;
private final int SIZE;
public FIFOCache(int size) {
super();//调用父类无参构造,不启用LRU规则
SIZE = size;
}
//重写淘汰机制
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
return size() > SIZE; //如果缓存存储达到最大值删除最后一个
}
}
LRU实现
当某一个数据被访问命中就会按照LRU规则放到队列最前面。如果新增一个不存在缓存的数据,会把该数据放到最前面,同时移除最早访问过的数据。
package com.brickworkers;
import java.util.LinkedHashMap;
public class LRUCache<K,V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = 5853563362972200456L;
private final int SIZE;
public LRUCache(int size) {
super(size, 0.75f, true); //int initialCapacity, float loadFactor, boolean accessOrder这3个分别表示容量,加载因子和是否启用LRU规则
SIZE = size;
}
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
return size() > SIZE;
}
}
LFU实现
使用频率最低的元素最先删除
package com.brickworkers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LFUCache{
static class Value implements Comparable<Value>{ //定义一个静态内部类,主要是用于统计命中数
Object key;
Object val;
int hitCount;
public Value(Object v, Object key) {
this.key = key;
this.val = v;
this.hitCount = 1; //第一次进入设置命中为1
}
public void setVal(Object obj){
this.val = obj;
}
public void countInc(){
hitCount++;
}
@Override
public int compareTo(Value o) {
if(o instanceof Value){ //如果比较的类属于Value或者是Value的子类
Value v = (Value) o;
if(this.hitCount > v.hitCount)
return 1;
else
return -1;
}
return 0;
}
}
final int SIZE;
private Map<Object, Value> map = new HashMap<Object, Value>();
public LFUCache(int size) {
SIZE = size;
}
//获取缓存中的数据
public Object get(Object k){
if(k == null)
return null;
//命中+1
map.get(k).countInc();
return map.get(k).val;
}
//存储数据
public void put(Object k, Object v){
//如果本来就存在
if(map.get(k) != null){
map.get(k).countInc();//命中计数
map.get(k).setVal(v);//覆盖结果值
}else{
//如果存储已超过限定值
if(map.size() >= SIZE){
remove();//移除最后一个数据
}
Value value = new Value(v, k);
map.put(k, value);
}
}
//数据移除最后一个数据
public void remove(){
//先拿到最后一个数据
Value v = Collections.min(map.values());
//移除最后一个数据
map.remove(v.key);
}
//获取存储情况
public String showList(){
List<Value> list = new ArrayList<Value>();
list.addAll(map.values());
Collections.sort(list);
String result = "";
for (Value value : list) {
result +=value.key+"="+value.val+" ";
}
return result;
}
}
2.其他JVM内置缓存
用static hashMap基于内存缓存的jvm内置缓存,简单不实用,保对象的有效性和周期无法控制,容易造成内存急剧上升。其他缓存方式有Oscache(主要针对jsp页面),Ehcache(主要针对数据库访问层),Jcache,Jbosscache等等
3.Java操作eache
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9iw9Y0U-1620874927162)(http://image.huawei.com/tiny-lts/v1/images/9b1e82d7b97189bfdff81d874d93ce7b_944x292.png@900-0-90-f.png)]
效率高(不用建立jdbc连接),降低了数据库的压力,缺点是有可能产生数据不一致的情况,清除缓存可解决
ehcache 主要是对数据库访问的缓存,相同的查询语句只需查询一次数据库,从而提高了查询的速度,使用spring的AOP可以很容易实现这一功能。 oscache 主要是对页面的缓存,可以整页或者指定网页某一部分缓存,同时指定他的过期时间,这样在此时间段里面访问的数据都是一样的。
4.Redis
关系型数据库:持久,主外键,编写SQL语句,存放在硬盘
非关系型数据库:一般用于缓存、值存放在内存(所以效率是真的高)、key-vakye形式、容易数据丢失(不过很好解决)
- Redis概述
完全开源免费、最受BSD协议、高性能的key-value非关系型数据库。支持持久化、支持key-value(String)\list\set\zert\hash等数据结构的存储、支持备份;可以有效减轻数据库访问压力;主要用于token生成、session共享、分布式锁、验证码、自增id(订单id) - Redis基本数据类型
字符串;list-简单字符串列表,按照插入顺序排序,可在头和尾插入,最多包含2^32-1个元素;set-是string类型的无序集合,集合成员唯一,不能出现重复数据,哈希表实现,添加,删除,查找的复杂度都是O(1);sorted set-与set一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1);hash-是一个string类型的field和value的映射表(和map差不多,只是兼职都是字符串),hash特别适合用于存储对象。Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿); - 主从复制和哨兵机制理解
一般地,Redis高可用都是一主多从。主从复制,主要是为了减轻单台服务器的压力(比如图中的master),通过多台服务器的冗余来保证高可用(单台宕机容错),实现读写分离、数据备份、集群等。
主服务器宕机了,怎么写?通过哨兵机制,哨兵其实就是一个监听器,一直监听这主服务器,如果主服务器挂了,他就会使用投票(随机)从主服务器中选择一台服务器作为主服务器,此乃高可用。
那如果整个集群挂了呢?有一个东西叫做keepalived监听器(其实就是一个shell写的重启服务的命令脚本),如果监听到某一台服务器挂了,他就会自动重启的(一般30秒内,听说可能不准确)。 - 主从复制实现
原理:通过快照文件(类似mysql的二进制可执行文件),当master有更新时,从服务器slave会实时请求得到该快照文件,进行执行,如果网络问题?别担心过,会重试的。
对于Redis服务器来说,和mysql有很大不同,只要设置好主从服务器之后,主服务器master可读可写,从服务器slave仅可读,不像mysql那样需要分配用户和mycat插件来控制读写分离。
redis主从复制只需要配置从服务器slave就OK,修改redis.conf配置文件,放开以下两行的注释,添加如下内容。两台从服务器都要修改。
slaveof 192.168.245.134 6379
#主服务器的ip和端口号
# If the master is password protected (using the "requirepass" configuration
# directive below) it is possible to tell the slave to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the slave request.
#
masterauth 123456
#主服务器的认证密码
- 哨兵机制实现
原理:哨兵用于管理多个redis服务器,执行以下三个任务:
1、监控(monitoring):哨兵(sentinel)会不断检查你的master和slave是否运作正常
2、提醒(notification):当被监控的某个redis出现问题时,哨兵(sentinel)可以通过API向管理员或者其他应用程序发送通知
3、自动故障迁移(automatic failover):当一个master1不能正常工作时,哨兵会开始一次自动故障迁移操作,他将会失效master1的其中一个slave升级为新的master2,并让失效master1的其他slave的master1改为新的master2。当客户端试图连接失效的master1时,集群也会向客户端返回新的master2地址,使得集群可以使用新的master2代替失效的master1
哨兵是一个分布式系统,可以在一个架构中运行多个哨兵进程,这些进程使用流言协议(gossipprotocols)来接收关于master是否下线的消息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移以及选择哪个slave作为新的master。
每个哨兵会向其他哨兵(sentinel)、master、slave定时发送消息,来确认对方是否还活着,如果对方在指定的时间(可配置)内未响应,则暂时认为对方已挂(主观认为宕机,Subjective Down,sdown)
若哨兵群中的多数sentinel都报告某一个master没响应,系统认为该master彻底死亡(客观真正的宕机,Objective Down,oDwon),通过一定vote算法,从剩下的slave节点中选择提升一台为master,然后自动修改相关配置
虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel).
修改Redis安装目录下的sentinel.conf
sentinel monitor mymaster 192.168.245.134 6379 1
# 主节点名称 主机ip 端口号 选举次数(就是说当有几台sentinel认为master挂了才是真的挂了,因为这里只有一个哨兵,所以为1)
sentinel down-after-milliseconds mymaster 30
#就是说多少ms后没有给老子响应,老子就觉得你挂了。,默认30s,这里设置为30ms,本地测试追求实时
sentinel config-epoch mymaster 1
#这个数字表示在发生主从复制的时候,比如master1向master2切换时,可以同时有多少个slave能对master2执行同步(也就是复制其实),越多越好?>如果太多了那么大家都去复制了,谁来响应客户端的请求?太少?太少的话,每个都要来一遍,怕是要到天黑哦,根据实际情况吧,这里只有三台所以
为设为1.
sentinel auth-pass mymaster 123456
#主服务器密码
- 数据持久化
数据持久化:就是将内存中的数据保存到硬盘,redis支持AOF和RDB两种存储方式
RDB存储:
RDB是指在一个时间点,如果达到所配置的数据修改量,就写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。
有两种保存方式:1、(阻塞)主进程直接拍快照(snapshot),然后阻塞客户端请求写入IO磁盘。2、(非阻塞)当要写入磁盘时,新建(fork)一个子进程,子进程将当前数据库快照写入磁盘,而主进程继续处理客户端请求。
AOF存储:
以日志文件方式存储,其实就是将你“操作+数据”指令格式化后追加到操作日志文件的尾部,必须append(已经写入到文件或者即将写入),才会进行数据的实际变更。“日志文件”保存了历史所有的操作过程;当 server 需要数据恢复时,可以直接 replay 此日志文件,即可还原所有的操作过程。内容是字符串,容易阅读和解析。
缺点:AOF 文件比 RDB 文件大,且恢复速度慢。
只会记录“变更操作”(例如:set/del 等),如果 server 中持续的大量变更操作,将会导致 AOF 文件非常的庞大,意味着 server 失效后,数据恢复的过程将会很长;事实上,一条数据经过多次变更,将会产生多条 AOF 记录,其实只要保存当前的状态,历史的操作记录是可以抛弃的;因为 AOF 持久化模式还伴生了“AOF rewrite”。
因为最多丢失最后一次写入文件的数据,所以很好修复,直接手工更改文件或者重新来一次即可。
修改redis.conf文件:
##此选项为aof功能的开关,默认为“no”,可以通过“yes”来开启aof功能
##只有在“yes”下,aof重写/文件同步等特性才会生效
appendonly yes
##指定aof文件名称
appendfilename appendonly.aof
##指定aof操作中文件同步策略,有三个合法值:always everysec no,默认为everysec每秒
appendfsync everysec
##在aof-rewrite期间,appendfsync是否暂缓文件同步,"no"表示“不暂缓”,“yes”表示“暂缓”,默认为“no”
no-appendfsync-on-rewrite no
##aof文件rewrite触发的最小文件尺寸(mb,gb),只有大于此aof文件大于此尺寸是才会触发rewrite,默认“64mb”,建议“512mb”
auto-aof-rewrite-min-size 64mb
##相对于“上一次”rewrite,本次rewrite触发时aof文件应该增长的百分比。
##每一次rewrite之后,redis都会记录下此时“新aof”文件的大小(例如A),那么当aof文件增长到A*(1 + p)之后
##触发下一次rewrite,每一次aof记录的添加,都会检测当前aof文件的尺寸。
auto-aof-rewrite-percentage 100
- Redis宕机
redis宕机后,值不会消失,redis默认开启RDB存储,如果是直接关闭服务,那么会自动备份,如果是kill或者断电如果没达到RDB配置的要求则不会持久化,而且这个虽然是非阻塞的,但是毕竟全量复制啊,保证了redis的性能,但是CPU可受不了啊。因此实际情况中,最好采用AOP方式,实时而且快,但是就是容易造成文件过大,恢复困难。 - Redis事务:
Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
multi 开启事务 exec提交事务。 - 发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。Redis 客户端可以订阅任意数量的频道
缓存机制(缓存的建立、维护与清除算法)
1.FIFO(First In First Out)
2.LRU(Least Recently Used)
3.LFU(Least Frequently Used)
参考文章
- https://blog.csdn.net/qq_41571417/article/details/108675793?ops_request_misc=&request_id=&biz_id=102&utm_term=Java%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6%E3%80%81&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-2-108675793.pc_search_result_before_js
- https://blog.csdn.net/zhaolang2009/article/details/78815988?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162046515416780271515396%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=162046515416780271515396&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-3-78815988.pc_search_result_before_js&utm_term=Java%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6%E3%80%81