1.redis的key是顶层模型,它的value是扁平化的。Redis中,所有的value都是一个Object。
typedef struct redisObject {
unsigned [type] 4;
unsigned [encoding] 4;
unsigned [lru] REDIS_LRU_BITS;
int refcount;
void *ptr;
} robj;
(1)type:数据类型,就是我们熟悉的string、hash、list等。
(2)encoding:内部编码,其实就是本文要介绍的数据结构。指的是当前这个value底层是用的什么数据结构。因为同一个数据类型底层也有多种数据结构的实现,所以这里需要指定数据结构。
(3)REDIS_LRU_BITS:当前对象可以保留的时长。跟键的过期策略有关。
(4)refcount:对象引用计数,用于GC。
(5)ptr:指针,指向以encoding的方式实现这个对象的实际地址。
2.String,在Redis内部,String类型有两种底层存储结构。Redis会根据存储的数据及用户的操作指令自动选择合适的结构。
- int:存放整数型
- SDS:存放浮点、字符串、字节类型
SDS简单动态字符串,simple dynamic string
SDS的内部数据结构
typedef struct sdshdr {
// buf中已经占用的字符长度
unsigned int len;
// buf中剩余可用的字符长度
unsigned int free;
// 数据空间
char buf[];
}
可见,底层是一个char数据。buf的最大容量是512M,里面可以放字符串、浮点数和字节。
为什么直接用数组?因为buf会有动态扩容和缩容的需求,那每次对字符串的修改都会导致重新分配内存,效率很低。
SDS扩容方案
当字符串长度小于1M时,扩容都是加倍现有的空间。如果超过1M,扩容时一次只会多扩容1M的空间。
3.list底层有两种数据结构:链表linkedlist和压缩列表ziplist。当list元素个数极少且元素内容不长时,采用ziplist实现,否则使用linkedlist。
链表
redis使用的链表是双向链表。
4.hash
hash的底层实现有两种实现:压缩列表和字典(dict)。
**字典
字典其实就是类似于java语言中的map。与java中的HashMap类似,redis底层也是使用散列表作为字典的实现,解决hash冲突使用的是链表法。
5.set
set存储的如果是整数类型,就直接使用整数集合intset。使用二分查找来辅助,不过插入的时候,由于要移动元素,时间复杂度是O(N)
6.zset
zset是可排序的set,与hash的实现类似,如果元素个数不多且不大,就使用压缩列表ziplist来存储。不过由于zset包含了score的排序信息,所以在ziplist内部,是按照score排序递增来存储的,意味着每次插入数据都要移动之后的数据。
7.总结:
redis对外暴露的是对象(数据类型),而每个对象都是用一个redisobject持有,通过不同的编码,映射到不同的数据结构,不同对象的底层可能使用同一种数据结构,比如压缩列表或字典。
string:int或dsd
list:ziplist或linkedlist
hash:ziplist或哈希表(hashtable)(内部结构dict类似java map)
set:intset或哈希表(hashtable)
zetset:ziplist或skiplist(跳跃表)
8.redis内存划分
(1)数据,这部分占用的内存会统计在used_memory中。Redis在存储对象时,不是直接将数据扔进内存,认识将数据包装成redisObject对象,底层存储有int、dsd、ziplist、hashtable、intset、skiplist等数据结构。
(2)进程本身运行需要的内存。redis主进程本身运行肯定需要占用内存,如代码、常量池等。除了主进程外,redis创建的子进程也会占用内存,如redis执行AOF、RDB重写时创建的子进程
(3)缓存内存。包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等。
(4)内存碎片
9.redis快的原因:
(1)完全基于内存,绝大部分请求是基于纯粹的内存操作,非常快速。
(2)数据结构简单,对数据操作也简单。
(3)采用单线程,避免了不必要的上下文切换和竞争。
(4)使用多路I/O复用模型,非阻塞IO。
10.redis持久化数据和缓存怎么做扩容。
(1)使用切片集群扩容
(2)如果redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定就不能变化。否则的话(即redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有redis集群可以做到这样。
11.redis的过期键的删除策略:
(1)定时过期:每个设置过期时间的key都需要创建一个定时容器,到过期时间就会立即清除。虽然可立即清除过期数据,但会占用大量cpu资源处理过期数据,影响缓存的响应时间和吞吐量。
(2)惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。节省cpu资源,对内存不友好,极端情况可能出现大量过期key没有再次被访问,从而不被清除,占用大量内存。
(3)定期删除:每个一定时间,臊面一地你给数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。
15.redis的内存淘汰策略:
reids的内存淘汰策略是指在redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
全局的键空间选择性移除
(1)noevictioin:当内存不足以容纳新写入数据时,新写入操作会报错。
(2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最少使用的key。(这个最常用)
allkeys-random:当内存不足以容纳写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
(1)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
(2)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
(3)volatitle-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key有限移除。
16.reid事务支持隔离性(Isolation)吗?
redis是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完事务队列中的命令为止。
17.redis事务保证原子性吗,支持回滚吗?
redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
18.哨兵模式的功能:
(1)集群监控:负责监控redis master和slave进程是否正常工作。
(2)消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
(3)故障转移:如果master node挂掉了,会自动转移到slave node上。
(4)配置中心:如果故障繁盛了,通知client客户端新的master地址。
19.哨兵核心知识:
(1)哨兵至少需要3个实例来保证自己的健壮性。
(2)哨兵+redis主从的部署架构,是不保证数据零丢失的,只能保证redis集群的高可用性。
(3)对于哨兵+reids主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行重组的测试和演练。
19.redis集群模式的工作原理以及redis的key如何寻址?分布式寻址都有哪些算法?了解一致性hash算法吗?
(1)redis cluster是一种服务端sharding技术,3.0版本开始正式提供。Redis Cluster 并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384(2^14)个槽。将请求发送到任意节点,接收到请求的节点会查询请求发送到正确的节点上执行。
(2)分布式寻址算法:
- hash算法(大量缓存重建)
- 一致性hash算法(自动缓存迁移) + 虚拟节点(自动负载均衡)
- redis cluster的hash slot算法
20.redis主从复制的核心原理
(1)当从库和主库建立MS关系后,会向主数据库发送SYNC命令
(2)主库接收到SYNC命令后,会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来。
(3)当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
(4)从Redis接收到后,会将快照写入磁盘并加载到内存,并执行收到的缓存命令。
(5)主redis每当接收到写命令就会将命令发送到从redis,从而保证数据的一致。
21.zookeeper分布式锁的实现
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。