0
点赞
收藏
分享

微信扫一扫

面试题之java缓存总结,从单机缓存到分布式缓存架构

唯米天空 2022-02-14 阅读 34

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增加地理位置的坐标,可以批量添加地理位置
XLENstream流中的消息数量
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以普通模式启动,主要是启动从服务器的方式

  1. 命令行

    #连接需要实现从节点的rediS,执行下面的命令
    slaveof [ip] [port]
    
  2. redis.conf配置文件

    #配置文件中增加
    slaveof [ip] [port]
    #从服务器是否只读(默认yes)
    slave-read-only yes
    
  3. 退出主从集群的方式

    slaveof no one

3、检查主从复制

#redis客户端执行
info replication

4、主从复制流程

5、主从复制核心知识

6、应用场景

7、注意事项

  1. 读写分离场景:

  2. 全量复制情况下:

  3. 复制风暴:

  4. 写能力有限

  5. master故障情况下:

  6. 带有效期的key:

5.1.7 哨兵模式

1、哨兵(Sentinel)机制核心作用

客户端 询问主redis地址> redis哨兵 监控、提醒、故障转移(主从切换)> 主redis(master) 主从复制关系> 从redis(slave)

2、核心运作流程

服务发现和健康检查流程

故障切换流程

3、哨兵如何知道Redis主从信息

4、什么是主观下线(sdown)

5、什么是客观下线(odown)

6、哨兵之间如何通信

  1. 哨兵之间的自动发现:发布自己的信息,订阅其他哨兵消息(pub/sub)
  2. 哨兵之间通过命令进行通信:直连发送命令
  3. 哨兵之间通过订阅发布进行通信:相互订阅指定主题(pub/sub)

7、哨兵领导选举机制

基于Raft算法实现的选举机制,流程简述如下:

  1. 拉票阶段:每个哨兵节点希望自己成为领导者;
  2. Sentinel节点收到拉票命令后,如果没有收到或同意过其他sentinel节点的请求,就同意该sentinel节点的请求(每个sentinel只持有一个同意票数)
  3. 如果sentinel节点发现自己的票数已经超过一半的数值,那么它将成为领导者,去执行故障转移
  4. 投票结束后,如果超过failover-timeout的时间内,没进行实际的故障转移操作,则重新拉票选举。

8、slave选举方案

9、最终主从切换的过程

10、哨兵服务部署方案

5.1.8 redis集群分片存储

1、为什么要分片存储

2、官方集群方案

3、搭建集群

4、集群关心的问题

  1. 增加了slot槽的计算,是不是比单机性能差?

  2. redis集群大小,到底可以装多少数据?

  3. 集群节点间是怎么通信的?

  4. ask和moved重定向的区别

    重定向包括两种情况

  5. 数据倾斜和访问倾斜的问题

    倾斜导致集群中部分节点数据多,压力大。解决方案分为前期和后期:

  6. slot手动迁移怎么做?

  7. 节点之间会交换信息,传递的消息包括槽的信息,带来带宽消耗。注意:避免使用大的一个集群,可以分多个集群。

  8. Pub/Sub发布订阅机制:对集群内任意的一个节点执行pubish发布消息,这个消息会在集群中进行传播,其他节点都接收到发布的消息。

  9. 读写分离:

5.1.9 redis监控

1、monitor命令

monitor是一个调试命令,返回服务器处理的每个命令。对于发现程序的错误非常有用。出于安全考虑,某些特殊管理命令CONFIG不会记录到MONITOR输出。

注意:运行一个MONITOR命令能够降低50%的吞吐量,运行多个MONITOR命令降低的吞吐量更多。

2、info命令

INFO命令以一种易于理解和阅读的格式,返回关于Redis服务器的各种信息和统计数值。

info命令返回信息
serverRedis服务器的一般信息
clients客户端的连接部分
memory内存消耗相关信息
persistence持久化相关信息
stats一般统计
replication主/从复制信息
cpu统计CPU的消耗
commandstatsRedis命令统计
clusterRedis集群信息
keyspace数据库的相关统计

可以通过section返回部分信息,如果没有使用任何参数时,默认为detault。

3、图形化监控工具: Redis-Live

5.2 memcached入门

由于memcached慢慢淡出了人们的视野,使用的公司越来越少,所以这里只是做个入门介绍。

1、简介

是一个免费开源的、高性能的、具有分布式内存对象的缓存系统,它通过减轻数据库负载加速动态web应用。

2、设计理念

  1. 简单的键/值存储:服务器不关心你的数据是什么样的,只管数据存储

  2. 服务端功能简单,很多逻辑依赖客户端实现

  3. Memcached实例之间没有通信机制

  4. 每个命令的复杂度为0(1):慢速机器上的查询应该在1ms以下运行。高端服务器的吞吐量可以达到每秒数百万

  5. 缓存自动清除机制

  6. 缓存失效机制

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、服务端配置

  1. 命令行参数

  2. init脚本

  3. 检查运行配置

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,直击数据库。

常用解决方案

举报

相关推荐

0 条评论