1、缓存定义
2、为什么要用缓存(读多写少,高并发)
3、缓存分类
3.1、单机缓存(localCache)
实现方案
1、基于JSR107规范自研(了解即可):
2、基于ConcurrentHashMap实现数据缓存
3.2、分布式缓存(redis、Memcached)
4、单机缓存
1、自己实现一个单机缓存
创建缓存类
/**
* @author yinfeng
* @description 本地缓存实现:用map实现一个简单的缓存功能
* @since 2022/2/8 13:54
*/
public class MapCacheDemo {
/**
* 在构造函数中,创建了一个守护程序线程,每5秒扫描一次并清理过期的对象
*/
private static final int CLEAN_UP_PERIOD_IN_SEC = 5;
/**
* ConcurrentHashMap保证线程安全的要求
* SoftReference <Object> 作为映射值,因为软引用可以保证在抛出OutOfMemory之前,如果缺少内存,将删除引用的对象。
*/
private final ConcurrentHashMap<String, SoftReference<CacheObject>> cache = new ConcurrentHashMap<>();
public MapCacheDemo() {
//创建了一个守护程序线程,每5秒扫描一次并清理过期的对象
Thread cleanerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(CLEAN_UP_PERIOD_IN_SEC * 1000);
cache.entrySet().removeIf(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(CacheObject::isExpired).orElse(false));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
cleanerThread.setDaemon(true);
cleanerThread.start();
}
public void add(String key, Object value, long periodInMillis) {
if (key == null) {
return;
}
if (value == null) {
cache.remove(key);
} else {
long expiryTime = System.currentTimeMillis() + periodInMillis;
cache.put(key, new SoftReference<>(new CacheObject(value, expiryTime)));
}
}
public void remove(String key) {
cache.remove(key);
}
public Object get(String key) {
return Optional.ofNullable(cache.get(key)).map(SoftReference::get).filter(cacheObject -> !cacheObject.isExpired()).map(CacheObject::getValue).orElse(null);
}
public void clear() {
cache.clear();
}
public long size() {
return cache.entrySet().stream().filter(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(cacheObject -> !cacheObject.isExpired()).orElse(false)).count();
}
/**
* 缓存对象value
*/
private static class CacheObject {
private Object value;
private final long expiryTime;
private CacheObject(Object value, long expiryTime) {
this.value = value;
this.expiryTime = expiryTime;
}
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
}
写个main方法测试一下
public static void main(String[] args) throws InterruptedException {
MapCacheDemo mapCacheDemo = new MapCacheDemo();
mapCacheDemo.add("uid_10001", "{1}", 5 * 1000);
mapCacheDemo.add("uid_10002", "{2}", 5 * 1000);
System.out.println("从缓存中取出值:" + mapCacheDemo.get("uid_10001"));
Thread.sleep(5000L);
System.out.println("5秒钟过后");
// 5秒后数据自动清除了
System.out.println("从缓存中取出值:" + mapCacheDemo.get("uid_10001"));
}
2、谷歌guava cache缓存框架
2.1、简介
2.2 简单使用
/**
* @author yinfeng
* @description guava测试,https://github.com/google/guava
* @since 2022/2/8 14:13
*/
public class GuavaCacheDemo {
public static void main(String[] args) throws ExecutionException {
//缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
LoadingCache<String, User> userCache
//CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置写缓存后8秒钟过期
.expireAfterWrite(8, TimeUnit.SECONDS)
//设置写缓存后1秒钟刷新
.refreshAfterWrite(1, TimeUnit.SECONDS)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
//设置要统计缓存的命中率
.recordStats()
//设置缓存的移除通知
.removalListener(notification -> System.out.println(notification.getKey() + " 被移除了,原因: " + notification.getCause()))
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(
new CacheLoader<String, User>() {
@Override
public User load(String key) {
System.out.println("缓存没有时,从数据库加载" + key);
// TODO jdbc的代码~~忽略掉
return new User("yinfeng" + key, key);
}
}
);
// 第一次读取
for (int i = 0; i < 20; i++) {
User user = userCache.get("uid" + i);
System.out.println(user);
}
// 第二次读取
for (int i = 0; i < 20; i++) {
User user = userCache.get("uid" + i);
System.out.println(user);
}
System.out.println("cache stats:");
//最后打印缓存的命中率等 情况
System.out.println(userCache.stats().toString());
}
@Data
@AllArgsConstructor
public static class User implements Serializable {
private String userName;
private String userId;
@Override
public String toString() {
return userId + " --- " + userName;
}
}
}
5、分布式缓存
5.1 redis
5.1.1 介绍
5.1.2通用命令
命令 | 作用 |
---|---|
DEL key | 用于在key存在是删除key |
DUMP key | 序列化给定的key,并返回给定的值 |
EXISTS key | 检查给定key是否存在 |
EXPIRE key seconds | 为给定key设置过期时间,单位秒 |
TTL key | 以秒为单位,返回给定key的剩余生存时间 |
TYPE key | 返回key所存储的值的类型 |
5.1.3 数据结构
1. String
定义
常用命令
命令 | 作用 |
---|---|
Get | 获取指定key的值 |
Set | 设置指定key的值 |
Incr | 将key中储存的数字值增一 |
Decr | 将key中储存的数字值减一 |
Mget | 获取所有(一个或多个)给定key的值 |
2. List
定义
常用命令
命令 | 作用 |
---|---|
Lpush | 将一个或多个值插入到列表头部 |
Rpush | 在列表中添加一个或多个值 |
Lpop | 移出并获取列表的第一个元素 |
Rpop | 移除列表的最后一个元素,返回值为移除的元素 |
Lrange | 获取所有(一个或多个)给定key的值 |
3. Set
定义
常用命令
命令 | 作用 |
---|---|
Lpush | 向集合中添加一个或多个成员 |
Rpush | 移除并返回集合中的一个随机元素 |
Lpop | 返回集合中的所有成员 |
Rpop | 返回所有给定集合的并集 |
4. Sorted set
定义
常用命令
命令 | 作用 |
---|---|
Zadd | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
Zrange | 通过索引区间返回有序集合中指定区间内的成员 |
Zrem | 移除有序集合中的一个或多个成员 |
Zcard | 获取有序集合的成员数 |
5. Hash
定义
常用命令
命令 | 作用 |
---|---|
Zadd | 获取存储在哈希表中指定字段的值 |
Zrange | 将哈希表key中的字段field的值设为value |
Hgetall | 获取在哈希表中指定key的所有字段和值 |
6. GEO
定义
常用命令
命令 | 作用 |
---|---|
GEOADD | 增加地理位置的坐标,可以批量添加地理位置 |
GEODIST | 获取两个地理位置的距离 |
GEOHASH | 获取某个地理位置的geohash值 |
GEOPOS | 获取指定位置的坐标,可以批量获取多个地理位置的坐标 |
GEORADIUS | 根据给定地理位置坐标获取指定范围内的地理位置集合(注意:该命令的中心点由输入的经度和结度决定) |
GEORADIUSBYMEMBER | 根据给定成员的位置获取指定范围内的位置信息集合(注意:该命令的中心点足由给定的位置元素决定) |
7. Stream
定义
常用命令
命令 | 作用 |
---|---|
XADD | 增加地理位置的坐标,可以批量添加地理位置 |
XLEN | stream流中的消息数量 |
XDEL | 删除流中的消息 |
XRANGE | 返回流中满足给定ID范围的消息 |
XREAD | 从一个或者多个流中读取消息 |
XINFO | 检索关于流和关联的消费者组的不同的信息 |
5.1.4 持久化机制
1. 介绍
2. 持久化方式
3.RDB方式
在redis.conf中调整save配置选项,当在规定的时间内,redis发生了写操作的个数满足条件会触发BGSAVE命令
#900秒之内至少一次写操作
save 900 1
#300秒之内至少发生10次写操作
save 300 10
优缺点
优点 | 缺点 |
---|---|
对性能影响最小 | 同步时丢失数据 |
RDB文件进行数据恢复比使用AOF要快很多 | 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间,影响RediS对外提供服务的能力 |
4. AOF持久化方式
**开启AOF持久化 **
appendonty yes
AOF策略调整
#每次有数据修改发生时都会写入AOF文件
appendfsync always
#每秒钟同步一次,该策略为AOF的默认策略
appendfsync everysec
#从不同步。高效但是数据不会被持久化
appendfsync no
优点
优点 | 缺点 |
---|---|
最安全 | 文件体积大 |
容灾 | 性能消耗比RDB高 |
易读,可修改 | 数据恢复速度比RDB慢 |
5.1.5 内存管理
1、内存分配
不同数据类型的大小限制:
最大内存控制:
2、内存压缩
#配置字段最多512个
hash-max-zipmap-entries 512
#配置value最大为64字节
hash-max-zipmap-value 64
#配置元素个数最多512个
lst-max-zipmap-entries 512
#配置value最大为64字节
list-max-zipmap-value 64
#配置元素个数最多512个
set-max-zipmap-entries 512
大小超出压缩范围,溢出后redis将自动将其转换为正常大小
3、过期数据的处理策略
主动处理(redis主动触发检测key足否过期)每秒抗行10次。过程如下:
被动处理:
数据恢复阶段过期数据的处理策略:
Redis内存回收策略:
回收策略 | 说明 |
---|---|
noeviction | 客户端尝试执行会让更多内存被使用的命令直接报错 |
allkeys-lru | 在所有key里执行LRU算法清除 |
volatile-lru | 在所有已经过期的key里执行LRU算法清除 |
allkeys-lfu | 在所有key里执行LFU算法清除 |
volatile-lfu | 在所有已经过期的key里执行LFU算法清除 |
allkeys-random | 在所有key里随机回收 |
volatile-random | 在已经过期的key里随机回收 |
volatile-ttl | 回收已经过期的key,并且优先回收存活时间(TTL)较短的key |
4、LRU算法
LRU(最近最少使用):根据数据的历史访问记录来进行沟汰数据
5、LFU算法
LFU:根据数据的历史访问频率来沟汰数据
5.1.6 主从复制
1、介绍
为什么要主从复制
应用场景分析
2、搭建主从复制
主Redis Server以普通模式启动,主要是启动从服务器的方式
-
命令行
#连接需要实现从节点的rediS,执行下面的命令 slaveof [ip] [port]
-
redis.conf配置文件
#配置文件中增加 slaveof [ip] [port] #从服务器是否只读(默认yes) slave-read-only yes
-
退出主从集群的方式
slaveof no one
3、检查主从复制
#redis客户端执行
info replication
4、主从复制流程
5、主从复制核心知识
6、应用场景
7、注意事项
-
读写分离场景:
-
全量复制情况下:
-
复制风暴:
-
写能力有限
-
master故障情况下:
-
带有效期的key:
5.1.7 哨兵模式
1、哨兵(Sentinel)机制核心作用
客户端 询问主redis地址> redis哨兵 监控、提醒、故障转移(主从切换)> 主redis(master) 主从复制关系> 从redis(slave)
2、核心运作流程
服务发现和健康检查流程
故障切换流程
3、哨兵如何知道Redis主从信息
4、什么是主观下线(sdown)
5、什么是客观下线(odown)
6、哨兵之间如何通信
- 哨兵之间的自动发现:发布自己的信息,订阅其他哨兵消息(pub/sub)
- 哨兵之间通过命令进行通信:直连发送命令
- 哨兵之间通过订阅发布进行通信:相互订阅指定主题(pub/sub)
7、哨兵领导选举机制
基于Raft算法实现的选举机制,流程简述如下:
- 拉票阶段:每个哨兵节点希望自己成为领导者;
- Sentinel节点收到拉票命令后,如果没有收到或同意过其他sentinel节点的请求,就同意该sentinel节点的请求(每个sentinel只持有一个同意票数)
- 如果sentinel节点发现自己的票数已经超过一半的数值,那么它将成为领导者,去执行故障转移
- 投票结束后,如果超过failover-timeout的时间内,没进行实际的故障转移操作,则重新拉票选举。
8、slave选举方案
9、最终主从切换的过程
10、哨兵服务部署方案
5.1.8 redis集群分片存储
1、为什么要分片存储
2、官方集群方案
3、搭建集群
4、集群关心的问题
-
增加了slot槽的计算,是不是比单机性能差?
-
redis集群大小,到底可以装多少数据?
-
集群节点间是怎么通信的?
-
ask和moved重定向的区别
重定向包括两种情况
-
数据倾斜和访问倾斜的问题
倾斜导致集群中部分节点数据多,压力大。解决方案分为前期和后期:
-
slot手动迁移怎么做?
-
节点之间会交换信息,传递的消息包括槽的信息,带来带宽消耗。注意:避免使用大的一个集群,可以分多个集群。
-
Pub/Sub发布订阅机制:对集群内任意的一个节点执行pubish发布消息,这个消息会在集群中进行传播,其他节点都接收到发布的消息。
-
读写分离:
5.1.9 redis监控
1、monitor命令
monitor是一个调试命令,返回服务器处理的每个命令。对于发现程序的错误非常有用。出于安全考虑,某些特殊管理命令CONFIG不会记录到MONITOR输出。
注意:运行一个MONITOR命令能够降低50%的吞吐量,运行多个MONITOR命令降低的吞吐量更多。
2、info命令
INFO命令以一种易于理解和阅读的格式,返回关于Redis服务器的各种信息和统计数值。
info命令 | 返回信息 |
---|---|
server | Redis服务器的一般信息 |
clients | 客户端的连接部分 |
memory | 内存消耗相关信息 |
persistence | 持久化相关信息 |
stats | 一般统计 |
replication | 主/从复制信息 |
cpu | 统计CPU的消耗 |
commandstats | Redis命令统计 |
cluster | Redis集群信息 |
keyspace | 数据库的相关统计 |
可以通过section返回部分信息,如果没有使用任何参数时,默认为detault。
3、图形化监控工具: Redis-Live
5.2 memcached入门
由于memcached慢慢淡出了人们的视野,使用的公司越来越少,所以这里只是做个入门介绍。
1、简介
是一个免费开源的、高性能的、具有分布式内存对象的缓存系统,它通过减轻数据库负载加速动态web应用。
2、设计理念
-
简单的键/值存储:服务器不关心你的数据是什么样的,只管数据存储
-
服务端功能简单,很多逻辑依赖客户端实现
-
Memcached实例之间没有通信机制
-
每个命令的复杂度为0(1):慢速机器上的查询应该在1ms以下运行。高端服务器的吞吐量可以达到每秒数百万
-
缓存自动清除机制
-
缓存失效机制
3、常用命令
分组 | 命令 | 描述 |
---|---|---|
存储命令 | set | 用于将value存储在指定的key中。key已经存在,更新该key所对应的原来的数据。 |
add | 用于将value存储在指定的key中,存在则不更新。 | |
replace | 替换已存在的key的Value,不存在,则替换失败。 | |
append | 用于向已存在key的value后面追加数据 | |
prepend | 向已存在key的value前面追加数据 | |
cas | 比较和替换,比对后,没有被其他客户端修改的情况下才能写入。 | |
检索命令 | get | 获取存储在key中的value,不存在,则返回空。 |
gets | 获取带有CAS令牌存的value,若key不存在,则返回为空 | |
删除 | delete | 删除已存在的key |
计算 | incr/decr | 对已存在的key的数字值进行自增或自减操作 |
统计 | stats | 返回统计信息如PID(进程号)、版本号、连接数等 |
stats items | 显示各个slab中item的数目和存储时长(最后一次访问距离现在的秒数) | |
stats slabs | 显示各个slab的信息,包括chunk的大小、数目、使用情况等。 | |
stats sizes | 显示所有item的大小和个数 | |
清除 | flush_all | 清除所有内容 |
4、客户端使用
客户端支持的特性:集群下多服务器选择,节点权重配置,失败/故障转移,数据压缩,连接管理
5、服务端配置
-
命令行参数
-
init脚本
-
检查运行配置
6、memcached性能
Memcached性能的关键是硬件,内部实现是hash表,读写操作都是0(1)。硬件好,几百万的OPS都是没问题的。
最大连接数限制:内部基于事件机制(类似JAVA NIO)所以这个限制和nio类似,只要内存,操作系统参数进行调整,轻松几十万。
集群节点数量限制:理论是没限制的,但是节点越多,客户端需要建立的连接就会越多。
注意:memcached服务端没有分布式的功能,所以不论是集群还是主从备份,都需要第三方产品支持。
7、服务器硬件需要
CPU要求:CPU占用率低,默认为4个工作线程
内存要求:
网络要求:
8、Memcached应用场景
5.3 互联网高并发缓存架构
5.3.1 缓存架构分析图
5.3.2 缓存雪崩
定义:因为缓存服务挂掉或者热点缓存失效,从而导致所有请求都去查数据库,导致数据库连接不够用或者数据库处理不过来,从而导致整个系统不可用。
常用解决方案:
5.3.2 缓存击穿
定义:查询必然不存在的数据,请求透过Redis,直击数据库。
常用解决方案: