缓存和nosql整体面试题
使用缓存的优缺点
优点:
1.减轻数据库压力(核心)
2.提高应用的响应速度,提高用户体验
3.增强系统的并发能力
缺点:数据不一致(延迟更新)
缓存的分类
缓存大体可以分为三类:
客户端缓存
服务端缓存
网络中缓存
客户端缓存
对于BS架构的互联网应用来说客户端缓存主要分为页面缓存和浏览器缓存两种,对于APP而言主要是自身所使用的缓存。
网络中缓存
网络中的缓存主要是指代理服务器对客户端请求数据的缓存,主要分为WEB代理缓存和边缘缓存(CDN边缘缓存)
服务端缓存
对于服务端缓存而言,从系统的架构上面区分可以将缓存分为
服务器本地缓存(localCache)(位于服务本机的内存中)
分布式缓存(Redis、Memcached等nosql)
数据库缓存(数据库在设计的时候也有缓存操作,更改相关参数开启查询缓存)
在操作本地缓存的时候不需要网络IO不需要文件IO,直接从本机内存中读取数据,因此读写速度最快。
在实际的开发中可以自己实现简单的本地缓存也可以使用开源的本地缓存框架,比如:ehcache、JBoss Cache等
缓存中的常见术语
1.缓存命中:当客户端请求的数据在缓存中,这个缓存中的数据就会被使用,这一行为被称为缓存命中
2.没有命中:缓存中没有查询到数据,并且数据库中可以查到此数据,并将数据放到缓存中
3.缓存穿透:是指查询一个缓存中一定不存在的数据。即缓存中不存在,并且数据库中也不存在,并且在数据库中没有查询到数据的情况下,不会去写缓存,这样就导致每次对于此数据的查询都会去查询数据库,这样就导致缓存失去了意义。
4.存储成本:缓存没有命中的时候,从其他数据源取出数据并放到缓存中的时间成本和空间成本就是存储成本。
5.缓存失效:当缓存中的数据已经更新时,则此数据已经失效
6.替代策略:当缓存没有命中的时,并且缓存容量已满,就需要在缓存中去除一条旧数据,然后加入一条新数据,而应该去除哪些数据,就由替代策略来决定。
常用的替代策略有:LRU、LFU等。在使用缓存算法的时候,通常会考虑使用频率、获取成本、缓存容量和时间等因素。
7.缓存雪崩:当缓存中大量的数据在某一时刻同时过期失效时,所有的请求都会直接落到数据库上。可能导致数据库服务器的崩溃。缓存雪崩通常是由于设置了相同的过期时间或者缓存服务器宕机等原因导致的。
缓存穿透、缓存雪崩、缓存击穿
缓存穿透:
请求不存在的数据,造成大量请求访问数据库
解决方案:
- 当没有数据时,也将查询结果放入到缓存中,缺点:会多存储很多没有数据的结果,我们可以把它的有效期设置的短一点
- 使用redis提供的布隆过滤器
缓存击穿:
指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。
解决方案:
- 设置热点数据永远不过期
- 多级缓存比如首先访问ehcache,没有再访问redis
- 使用互斥锁,当缓存没有命中时,先加锁,之后db取数据放入缓存,再解锁,其他线程过来有锁的情况下先sleep一会,再获取
- 布隆过滤器
布隆过滤器:
- 布隆过滤器主要用于检索一个元素是否在一个集合中。它的核心思想是利用多个哈希函数将元素映射到位向量中的不同位置,并将这些位置的值设为1。当需要判断一个元素是否存在于集合中时,只需检查该元素通过哈希函数映射到的所有位置是否都为1,如果都为1,则认为元素可能存在;否则,元素一定不存在。在实际应用中,布隆过滤器常用于缓存穿透、大数据去重、垃圾邮件过滤等场景。
- 布隆过滤器的优点主要体现在空间效率和查询时间上,其性能远超过一般的算法。然而,布隆过滤器也存在一些缺点。由于哈希冲突的存在,布隆过滤器可能会产生误判,即某个不存在的元素可能会被误判为存在。此外,布隆过滤器不支持删除操作,一旦元素被加入,就无法从过滤器中移除。
缓存雪崩:
是指缓存大量失效,导致大量的请求都直接向数据库获取数据,造成数据库的压力。缓存大量失效的原因可能是缓存服务器宏机,或者大量Redis的键设置的过期时间相同。
解决缓存雪崩我们也有两种解决方案:
- 在设置Redis键的过期时间时,加上一个随机数,这样可以避免。
- 部署分布式的Redis服务,当一个Redis服务器挂掉了之后,进行故障转移。
- 使用互斥锁
缓存热key问题
热key,指访问频率特别高的key,可能导致缓存服务器的流量在短时间内过于集中,增加了单个节点的负载,从而可能引发缓存服务器宕机或性能下降的风险。如(电商的商品秒杀活动)
处理方案:
使用消息队列进行流量消峰:
避免一次处理太多请求
负载均衡:
将热点key分散为多个子key,然后存储到缓存集群的不同机器上,这些子key对应的value都和热点key是一样的。当通过热点key去查询数据时,通过某种hash算法随机选择一个子key,然后再去访问缓存机器,将热点分散到了多个子key上。(缺点数据维护代价比较高)
读写分离:对于读操作频繁的热key,可以考虑将读请求引导到从节点上,从而减轻主节点的压力。
限流熔断保护:限流是限制访问服务器的访问量,超过多少拒绝或返回一定结果,熔断是达到多少请求量过滤掉百分之多少的请求
如何做到动态的判断热key和处理热key
可以监控redis端口的数据,抓到数据可以放到kafka之后做出统计出热key,往zookeeper里头的某个节点里写。然后你的业务系统监听该节点,发现节点数据变化了,就代表发现热key。最后往本地缓存里写
redis如何判断热key:
- 你可以使用像
Redis Commander、Redis Enterprise或Redis Insight这样的图形化工具来监控Redis的性能和流量。这些工具通常能够显示每个key的访问频率,从而帮助你识别热key。 - 对于命令行爱好者,可以使用
MONITOR命令来实时查看Redis服务器接收到的所有命令。通过分析这些命令,你可以找到哪些key被频繁访问。 - 如果你的Redis服务器配置了日志记录,你可以分析这些日志文件来查找频繁访问的key。
- hotkeys参数,redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可。但是该参数在执行的时候,如果key比较多,执行起来比较慢。
- 也可以自己编写程序抓redis的包进行统计分析
- 凭借业务经验,进行预估哪些是热key
缓存不一致问题
更新数据更新缓存:
更新完数据就更新缓存,会影响写数据的性能,也不符合懒加载的思想,一般不采用。
更新缓存更新数据:
缓存更新成功,数据更新失败,数据不一致的风险较高,一般不采用。
删除缓存更新数据:
A清除完缓存后还没更新数据前,B线程读取了更新前的数据,并把老数据读取到了缓存,之后A更新了数据。导致缓存中是老数据。一般不采用,因为读一般比写快出现不一致的记录高。
更新数据删除缓存:
A查询缓存无数据,查询数据库得到旧值,B更新数据,B删除缓存,A将旧值更新到缓存。发生在读比写慢的情况,这种是小概率事件
一般采用先更新数据之后删除缓存,如果要求一致性要求的高可以配合分布式锁和事务回滚处理
分布式系统的本地缓存更新
发布订阅消息系统:实时性受限于消息消费速度
定时任务更新
大key问题
大key:大key问题是指value值太大,获取时影响缓存服务器性能,比如redis存储大value,redis是单线程,容易造成阻塞
具体场景:大V的粉丝列表。
将一个大的KEY拆分为多个小的KEY, 变成value1,value2… valueN,打散分不到不同的分片中。使用mGet一次获取多个key中的值
- 按字段拆分:如果value是一个对象或结构体,可以按其字段拆分成多个小key。
- 按范围拆分:如果value是一个有序集合或列表,可以按范围拆分成多个小集合或小列表。
- 按时间拆分:如果value包含时间序列数据,可以按时间窗口拆分成多个小key。
避免使用列表(list)或集合(set)等可能无限增长的数据结构来存储大量数据。
如何发现大key:
redis有提供工具或命令发现大key如:
- 工具redis-rdb-tools工具。redis实例上执行bgsave,然后对dump出来的rdb文件进行分析,找到其中的大KEY。优点在于获取的key信息详细、可选参数多、支持定制化需求,结果信息可选择json或csv格式,后续处理方便,其缺点是需要离线操作,获取结果时间较长。
- 命令:redis-cli --bigkeys命令。优点是可以在线扫描,不阻塞服务;缺点是信息较少,内容不够精确。扫描结果中只有string类型是以字节长度为衡量标准的。List、set、zset等都是以元素个数作为衡量标准,元素个数多不能说明占用内存就一定多。
- 命令:memory usage mykey命令,时间复杂度:O(N) ,n是元素数量;返回的结果是key的值以及为管理该key分配的内存总字节数
- 自己做redis端口监控
- 监控Redis的内存使用情况,如果某个key占用的内存远超过其他key,那么它可能是一个大key。
多级缓存的使用
本地缓存作为一级缓存,分布式缓存作为二级缓存;当用户获取数据时,先从一级缓存中获取数据,如果一级缓存有数据则返回数据,否则从二级缓存中获取数据。如果二级缓存中有数据则更新一级缓存,然后将数据返回客户端。如果二级缓存没有数据则去数据库查询数据,然后更新二级缓存,接着再更新一级缓存,最后将数据返回给客户端。
多级缓存的数据一致性:
说一下nosql
nosql全称是Not Only SQL泛指非关系型的数据库,
在项目中可以结合关系型数据库一块使用
nosql数据库的应用场景
对数据的高并发读写
对海量数据的高效率存储和访问
对数据的高扩展性和高可用性
nosql与关系型数据库区别
nosql数据库没有固定的表结构,关系型数据库利用二维表格模型
nosql的存储格式是key,value形式、文档形式、图片形式等等,所以可以存储基础类型以及对象或者是集合等各种格式,而关系型数据库则只支持基础类型。
nosql与关系型数据库不是对立关系,应该是互补关系。
nosql扩展性更强。
nosql不提供对sql的支持
nosql对事务支持不是很好
nosql数据库的分类
键值(Key-Value)存储数据库:
相关产品: memcache、Redis、Voldemort、Berkeley DB 典型应用: 内容缓存,主要用于处理大量数据的高访问负载。 数据模型: 一系列键值对 优势: 快速查询 劣势: 存储的数据缺少结构化
列存储数据库:
相关产品:Cassandra, HBase, Riak 典型应用:分布式的文件系统 数据模型:以列簇式存储,将同一列数据存在一起 优势:查找速度快,可扩展性强,更容易进行分布式扩展 劣势:功能相对局限
文档型数据库:
相关产品:CouchDB、MongoDB 典型应用:Web应用(与Key-Value类似,Value是结构化的) 数据模型: 一系列键值对 优势:数据结构要求不严格 劣势: 查询性能不高,而且缺乏统一的查询语法
图形(Graph)数据库:
相关数据库:Neo4J、InfoGrid、Infinite Graph 典型应用:社交网络 数据模型:图结构 优势:利用图结构相关算法。 劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案。
列举常见的nosql说明其特点
我比较了解的Memcached, Mongodb,Redis,HBase
Memcached:
挥发性(临时性)的键值存储
一般作为关系型数据库的缓存来使用
具有非常快的处理速度
由于存在数据丢失的可能,所以一般用来处理不需要持久保存的数据
用于需要使用expires时(需要定期清除数据)
使用一致性散列(Consistent Hashing)算法来分散数据
MongoDB:
- 面向无需定义表结构的文档数据
- 具有非常快的处理速度
- 通过BSON的形式可以保存和查询任何类型的数据
- 无法进行JOIN处理,但是可以通过嵌入(embed)来实现同样的功能
- 使用sharding(范围分割)算法来分散数据
Redis:
高性能,持久存储,适应高并发的应用场景
Redis可以实现持久化,主从复制,实现故障恢复。
适用于数据变化快且数据库大小可遇见(适合内存容量)的应用程序。
例如:股票价格、数据分析、实时数据搜集、实时通讯。
HBase:
HBase基于Google的BigTable构建
处理大量数据,应对极高写负载,高可用
Memcache与redis对比
Memcache不支持持久化,redis支持持久化
Memcache是多线程,redis是单线程
memcache 仅支持简单的key-value结构的数据记录,redis支持很多种数据类型的存储
memcache 本身不支持主从(可以靠外部工具实现),redis支持主从
Memcache本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcache的分布式存储。Redis集群方案可以实现分布式存储
二者的性能都已经足够高了,无论你使用哪一个,每秒处理请求的次数都不会成为瓶颈。(比如瓶颈可能会在网卡)
说一下ehcache
ehcache是一个纯Java的进程内缓存框架(直接在jvm虚拟机中缓存),Hibernate中默认使用ehcache提供二级缓存。
支持内存和磁盘存储。
属于服务器本地缓存
ehcache提供的缓存方式:
堆内存储(Memory store):
不要求实现序列化接口。速度快,但是容量有限。受GC管理。
堆外存储(Off-heap store):
只在企业版本的Ehcache中提供,原理是利用nio的DirectByteBuffer来实现,
比存储到磁盘上快,而且不受GC的影响,但是必须以字节数组的方式存储,对
象在存储过程中进行序列化,读取则进行反序列化操作,它的速度大约比堆内
存储慢一个数量级。
磁盘存储(Disk store):
要求实现序列化接口。
使用流程一般是:先从本地缓存ehcache中取,取不到再从分布式缓存redis里取,再取不到就从DB里取
ehcache与redis比较
速度快,效率高,缓存共享麻烦,集群分布式应用不方便。
如果是单个应用或者对缓存访问要求很高的应用,用ehcache。
如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用redis。
redis是通过socket访问到缓存服务,效率比ecache低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。
ehcache的使用
导入jar包,通过xml文件设置缓存策略(持久化位置,缓存大小,缓存名称等),利用ehCacheCacheManager来取得指定名称缓存的对象,利用指定缓存对象来操作缓存
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache" />
<!-- defaultCache,是默认的缓存策略 -->
<!-- 如果你指定的缓存策略没有找到,那么就用这个默认的缓存策略 -->
<!-- external:缓存对象是否一直存在,如果设置为true的话,那么timeout就没有效果,缓存就会一直存在,一般默认就是false -->
<!-- maxElementsInMemory:内存中可以缓存多少个缓存条目 -->
<!-- overflowToDisk:如果内存不够的时候,是否溢出到磁盘 -->
<!-- diskPersistent:是否启用磁盘持久化的机制,在jvm崩溃的时候和重启之间 -->
<!-- timeToIdleSeconds:对象最大的闲置的时间,如果超出闲置的时间,可能就会过期 单位:秒 当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大-->
<!-- timeToLiveSeconds:对象最多存活的时间 单位:秒 当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是存活时间无穷大-->
<!-- memoryStoreEvictionPolicy:当缓存数量达到了最大的指定条目数的时候,需要采用一定的算法,从缓存中清除一批数据,LRU,最近最少使用算法,最近一段时间内,最少使用的那些数据,就被干掉了 -->
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="300"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LRU" />
<!-- 手动指定的缓存策略 -->
<!-- 对不同的数据,缓存策略可以在这里配置多种 -->
<cache
name="local"
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="300"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
CacheManager create = CacheManager.create(this.getClass().getResourceAsStream("/ehcache.xml"));
Cache cache = create.getCache("local");
//存
Element element = newElement(cacheKey, result);
cache.put(element);
//取:
sf.ehcache.Element e = cache.get(cacheKey);
return e.getValue();
也可以使用注解的方式使用ehcache
@Service
public class EhcahceServiceImpl implements EhcahceService {
private static final String CACHE_STRATEGY = "local";
@CachePut(value=CACHE_STRATEGY,key="'key_'+#info.getProduct_id()")
@Override
public ProductInfo saveProductInfo(ProductInfo info) throws Exception {
return info;
}
//@CacheEvict(value="myCache", key="'get'+#userNo")
////allEntries为true表示清除value中的全部缓存,默认为false
// @CacheEvict(value="myCache", allEntries=true)
@Cacheable(value=CACHE_STRATEGY,key="'key_'+#id")
@Override
public ProductInfo getProductInfoById(Long id) throws Exception {
return null;
}}
注解说明:
- @Cacheable:调用方法时会先从缓存中取,如果没有就执行方法,然后将结果存入缓存
- @CacheEvict:方法执行后会清空缓存
- @CachePut:无论有没有缓存都会执行方法,然后将结果存入缓存
ehcache在hibernate中的应用
- 1、是sessionFactory级别的缓存,它是属于进程范围的缓存
- 2、存放的是公有数据:共享数据(1、一般情况下保持不变2、所有的人都能访问3、访问的频率比较高4、安全性不是特别高的数据)
- 3、二级缓存的生命周期和SessionFactory的生命周期一致。
- 二级缓存的使用需要配置
- Hibernate本身对二级缓存没有提供实现,是借助第三方插件实现的。
- 二级缓存分为类的二级缓存和集合的二级缓存
- 只有为某个类或集合配置了第二级缓存,Hibernate在运行时才会把它的实例加入到第二级缓存中。
- 一个sessionFactory的多个session使用同一块缓存空间,用到相同数据可以从缓存中取。
使用:
在hibernate的配置文件中指定使用ehcache缓存,并开启二级缓存,在需要缓存的类的映射文件中也要指定开启二级缓存,这样就可以使用了ehcache作为二级缓存了,还可以建立ehcache.xml文件来配置缓存策略
说一下oscache
oscache是一个高性能的javaee缓存框架
可以用于
缓存java对象
页面局部缓存
页面全局缓存
oscache的使用
第一步:导入jar包
第二步:把oscache安装目录下的/etc/oscache.properties 文件放入 /WEB-INF/classes目录.开发阶段,我们可以把该文件放置在src目录.
缓存对象:
java类中可以使用GeneralCacheAdministrator类可以操作缓存,缓存对象
页面局部缓存:
在页面上使用Oscache的标签<oscache></oscache>来对标签内的内容做缓存,标签属性可以指定缓存策略,缓存默认存放在application范围,缓存时间默认为3600秒,即1小时,默认缓存的key将以请求的URI+查询字串组成
页面全局缓存:
需要在web.xml中配置CacheFilter过滤器,来对映射到的页面做全局缓存

浙公网安备 33010602011771号