Java面试题五:缓存

六、缓存

1、Memcached。

答:特点是多线程、异步IO、KV存储、内存存储没有持久化、采用LRU(Least Recent Used)淘汰算法。

内存管理(Slab结构):内存按照1MB的大小分页,页中的内存分割为具有相同大小的内存块。一个新的记录到来时,Memcached根据记录的大小选择存储的Slab类型,如果没有该类型空闲的Slab块,会创建一个新的页。当记录大小发生变化时,存储的位置有可能发生变化。以此来解决内存碎片问题。

如果每个服务器只部署一个Memcached实例,由于各个实例被选中的概率基本相同,较大内存的Memcached实例无法被充分利用,因此可以在具有较大内存的服务器部署多个Memcached实例。

Slab钙化:当需要插入一条99K的数据而Memcached已经没有足够空间再次分配一个Slab实例的时候,不会释放具有300K块大小的Slab,而是在100K块大小的各个Slab中找到要释放的块,将数据添加到该块中。解决方法:1)重启Memcached实例,重新分配slab class;2)过期淘汰其它slab class数据;3)通过slab_reassign、slab_authmove参数控制,自动重分配。

2、Redis对象系统(redisObject)

答:redisObject中

1)type是对象的类型:

#define OBJ_STRING 0    //字符串对象

常用命令:APPEND、INCR、INCRBY、DECR、DECRBY、GET、SET

实现方式:SDS

SDS:

定义:本质上是char*,有一个Header结构用来存放sds,记录了已用空间长度、剩余空间长度和char buf[],好处是不会遇到'\0'结束、直接获取字符串长度、不会出现缓冲区溢出。

内存分配策略:空间预分配,小于1MB,分配与len长度相同的free;大于1MB,分配1MB的free。

内存释放策略:惰性空间释放,不是重分配缩短字节,而是使用free成员将这些字节记录起来,等待将来使用。

encoding:如果value能保存为整数,则encoding修改为OBJ_ENCODING_INT,并将redisObject的指针(ptr)指向对应值。如果不能则保持OBJ_ENCODING_RAW。

#define OBJ_LIST 1      //列表对象

常用命令:LPUSH、RPUSH、LPOP、RPOP、LLEN

encoding:OBJ_ENCODING_LINKEDLIST(desperated)、OBJ_ENCODING_ZIPLIST、OBJ_ENCODING_QUICKLIST。

ziplist是一个连续的内存块,由表头、若干entry节点、尾部标识符组成,插入和删除节点要频繁realloc(),同时进行数据移动,效率低。
ziplist类似数组,但是entry的大小不是相同的,entry结构中包含prevlength、encoding、data。
entry将整形分为了6类(1~13,8位,16位,24位,32位,64位),将字符串分为3类(2^6、2^14、2^32)。

quicklist是一个双向链表,每个节点都是一个ziplist(默认最大为8kb),双向链表插入、删除效率高。
quicklist插入:插入已存在的Entry前或后,如果遇到在ziplist的头部或尾部,且不存在前驱或后驱节点,则创建一个新的ziplist。
push/pop:push/pop quicklist头结点或尾节点ziplist的头部或尾部;

#define OBJ_SET 2       //集合对象

常用命令:SADD、SREM、SCARD、SISMEMBER

encoding:当set中的值都为数值且小于512时使用intset,超过则使用hashtable

inset:按顺序将整数存储在数组中,以二分法查找,节约空间;当数据量大时,由于查找时间复杂度位Olog(n),因此转为hashtable。

#define OBJ_ZSET 3      //有序集合对象

常用命令:APPEND、INCR、NCR、INCRBY、DECR、DECRBY、GET、SET

encoding:当zset中元素长度都小于64B且zset键值对小于128个时使用ziplist,超过时会使用skiplist

skiplist插入时随机层数,随机的方法是如果一个节点有第i(i>=1)层指针,那么它有(i+1)层指针的概率是p,redis中p为1/4,MaxLevel=32

#define OBJ_HASH 4      //哈希对象

常用命令:HSET、HGET、HEXISTS

encoding:当hash中的K/V字符串长度都小于64B且键值对小于512个时使用ziplist(key/value各占一个entry),超过时使用hashtable

2)encoding是对象的存储方式:

#define OBJ_ENCODING_RAW 0     /* Raw representation */     //原始表示方式,字符串对象是简单动态字符串
#define OBJ_ENCODING_INT 1     /* Encoded as integer */         //long类型的整数
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */      //字典
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */          //不在使用
#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */  //双端链表,不在使用
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */         //压缩列表
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */          //整数集合
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */      //跳跃表和字典
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */   //embstr编码的简单动态字符串
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */   //由压缩列表组成的双向列表-->快速列表

3、Redis内存分配。

答:jemalloc是Redis默认的内存分配器,将内存划分为许多内存块单位,Redis存储数据时会选择合适的内存块进行存储。

jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。

Redis的数据存储结构是Dict,内存主要由dictEntry和键值对所需要的bucket空间。例如90000个键值对估算内存占用:90000*80(dictEntry)+131072(大于90000的2^n)*8(bucket即指针大小)。

4、Redis优化内存占用。

答:1)使用整形/长整型代替字符串,使用合适的数据类型;2)可以利用共享对象适当调整REDIS_SHARED_INTEGERS(默认是0-9999);3)避免过度设计;4)关注内存碎片率;5)无用的键值设置过期时间。

5、Redis持久化(RDB和AOF)。

答:RDB持久化:是指在制定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,详见数据集写入临时文件,写入成功后,在替换之前的文件,用二进制压缩。优点是只包含一个文件,灾难恢复,性能、启动效率会更高;缺点是实时性、高可用性差。

AOF持久化:以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。优点:更高的数据安全性(每秒同步、每修改同步和不同步),写入过程中即使出现宕机也不会破坏日志文件,rewrite机制,记录所有修改信息;缺点:恢复速度、运行效率不如RDB。

(主从环境下)持久化策略:master完全关闭持久化,让master性能达到最好;slave关闭RDB,开启AOF,定时对持久化文件进行备份;关闭AOF自动重写,添加定时任务,在Redis闲时调用bgrewriteaof(防止AOF文件过大)。

6、fork阻塞、AOF追加阻塞。

答:fork阻塞是由主从切换时,Redis内存过大,fork子进程挂在时拷贝内存页表耗时大引起;AOF追加阻塞是由于硬盘负载过高,fsync操作超过1s,硬盘负载越来越大,IO资源消耗更快,Redis进程异常退出而丢失数据,Redis的处理策略是AOF对比上一次fsync成功的时间超过2s阻塞主线程直到fsync同步完成。

7、缓存常见问题。

答:1)缓存不一致:原因,同步更新失败或异步更新;方案,增加重试、补偿任务、最终一致性;

2)缓存穿透:原因,并发查询一个缓存不存在的数据;方案,空对象缓存,bloomfilter过滤器;

3)缓存击穿:原因,热点key失效;方案:互斥更新,随机退避(随机等待时间),增加失效时间;

4)缓存雪崩:原因,缓存宕机;方案:快速失败熔断、主从模式、集群模式。

8、主从同步。

答:通过发布订阅机制实现,作用是数据冗余、故障恢复、负载均衡。

全量同步:一般发生在Slave初始化阶段,通过Master生成RDB文件Slave载入的方式实现;

增量同步:Master写操作时,向每个Slave发送相同的写命令。

9、哨兵(Sentinel)。

答:用于解决故障恢复自动化。

实现原理:

1)定时任务:通过向主从节点发送info命令获取最新的主从结构;通过发布订阅功能获取其它哨兵节点信息;通过向其他节点发送ping命令进行心跳检测。

2)主观下线:心跳检测时如果超过一定时间没有回复,哨兵节点会将其标记为主观下线。

3)客观下线:哨兵节点对主节点主观下线后,会询问其它哨兵节点该主节点状态,达到一定数量后对其标记为客观下线。客观下线只针对主节点。

4)选举领导者哨兵节点:Raft算法,哨兵A向哨兵B发送成为领导者请求,如果B没有同意过其它哨兵,则A成为领导。

5)故障转移:由领导者哨兵进行故障转移:

从备选node中,按照如下顺序选择新的master
1、较低的slave_priority(这个是在配置文件中指定,默认配置为100)
2、较大的replication offset(每个slave在与master同步后offset自动增加)
3、较小的runid(每个redis实例,都会有一个runid,通常是一个40位的随机字符串,在redis启动时设置,重复概率非常小)
4、如果以上条件都不足以区别出唯一的节点,则会看哪个slave节点处理之前master发送的command多,就选谁

a.在从节点中选择与主节点相似度最高的成为主节点;

b.让剩余从节点成为他的从节点;

c.原来的主节点更新为从节点,当其恢复后挂到新的主节点上。

10、集群。

答:实现原理:

1)主观下线:集群中每个节点定期向其他节点发送心跳检测,如果一段时间一直失败,发送节点把接收节点标记为主观下线(pfail);

2)客观下线:主观下线状态在集群内传播,如果其他节点解析出pfail,则把该节点加入下线报告链表,超过半数的主节点都标记了节点pfail,则标记该节点客观下线;向集群广播一条消息,通知集群所有节点也将其标记为客观下线,同时通知其从节点触发故障转移流程;

3)故障恢复:

a.资格检查(从节点与主节点断线超过一定时间无资格)

b.选举:从与主节点数据最一致的从节点开始一次选举,只有每个主节点有一票,从节点获取到半数以上票就可以成为主节点

c.替换主节点,并通知集群内所有节点。

11、基于Redis实现延迟消息队列。

答:使用案例:a.订单下单之后超过30分钟用户未支付,需要取消订单;b.订单一些评论,如果48h用户未对商家评论,系统会自动产生一条默认评论;c.点我达订单下单后,超过一定时间订单未派出,需要超时取消订单等。。。

实现:将消息以kv形式存储,使用zset做优先队列,使用list做为消费队列,先进先出消费,zset和list存储消息key。每个list对应不同主题,自定义路由建立zset和list的关系,使用定时器维持路由。

12、基于Redis实现分布式锁。

答:加锁:SET my_key my_value NX PX milliseconds。Redis提供了一个只有在某个key不存在情况下才会设置key值的原子命令(NX),这样可以避免一个锁同一时间被多个客户端获取;通过PX设置过期时间防止因为服务器宕机而无法释放锁的问题。

释放锁:为防止释放了别的客户端申请的锁,每个客户端可以给value设置一个随机值,删除之前先判断value是否一致。由于释放锁getKey和deleteKey不是原子操作,因此有可能期间别的客户端申请了锁,导致被误释放,因此可以使用Lua脚本,在执行脚本过程中,其它客户端命令需要等待该Lua脚本执行完才能执行。

13、如何保证Redis的高并发和高可用?

答:高并发:主从读写分离,多从库,多端口实例以及集群;

master负责写数据,slave负责读和复制,且复制时不会阻塞读;主从结构下master必须做持久化,防止master宕机数据被清空。

高可用:哨兵机制、集群;

数据丢失:1)master挂了,数据还未被写入slave;2)脑裂,双master;解决方法:min_slaves_to_write(至少有一个slave)、min_slaves_max_lag(数据同步最大延迟,slave落后了mater超过这个延迟,master不接受写请求,此时需要在客户端做降级,暂时写入本地缓存或者临时的kafka队列)。

14、Redis的失效机制是怎样的,Redis有哪些淘汰策略?

答:失效机制:定期删除(默认每隔100ms随机抽取一些设置过期时间的key,检查是否过期并删除)+惰性删除(查询时删除);淘汰策略:noeviction、allkeys-lru(最常用)、allkeys-random、volatile-lru、volatile-random、volatile-ttl(更早过期时间的key优先移除)。

15、Redis的并发竞争问题如何解决,了解Redis事务的CAS操作吗?

答:利用Redis的原子操作、CAS乐观锁、直接在代码中加锁。

watch指令在redis事务中提供了CAS的行为。为了检测被watch的keys在是否有多个clients同时改变引起冲突,这些keys将会被监控。如果至少有一个被监控的key在执行exec命令前被修改,整个事务将会回滚,不执行任何动作,从而保证原子性操作,并且执行exec会得到null的回复。

16、Redis的线程模型是什么。

答:基于多路复用IO,可以同时连接多个File Descriptor,遍历这些FD对准备好的进行IO操作。

17、简述本地缓存和集中式缓存和优缺点。

答:本地缓存的优点是读取速度快,没有网络请求的时间消耗;集中式缓存可以通过同步,保证数据的一致性,且数据安全性高。

18、如何实现一个LRU,Redis lru原理?

答:自己实现:hashmap+双向链表:save(key),在hashmap中查找key,在header插入数据,如果缓存空间不足,就淘汰tail指向内容,并更新hashmap中key的索引value;get(key):直接通过hashmap找到key对应的位置,同时更新LRU指向关系。

redis lru:上一种实现方式需要消耗大量存储空间,redis是每个redisObject维护一个lru时间和refcount引用计数,随机取出若干个key,按照访问时间排序后,淘汰最不常用使用的。

19、Redis集群相关方案。

答:1)主从模式:可用性低,主从数据都是一样的;2)哨兵模式:无法支持在线扩容,需要预留空间;3)集群模式。

20、Redis单线程模型。

答:建联、AE_READABLE、AE_WRITEABLE->IO多路复用器->压入队列->文件分派器->连接应答处理器、命令请求处理器、命令回复处理器。

21、Redis高效的原因?

答:1、纯内存操作(文件分派器、相关处理器);2、基于非阻塞多路复用;3、单线程避免上下文切换。

22、hash与一致性hash区别?

答:一致性hash使用环形结构,顺时针寻找最近的节点,所以一个节点失效只会影响该节点,而不会导致全部数据失效。

23、redis优化?

答:mget/mset, setex, pipeline, hash tag。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值