文章目录
- 1、redis数据结构
- 2、redis适用场景
- 3、redis内部实现
- 4、redis如何实现rehash
- 5、redis String类型实现原理
- 6、基于redis的分布式锁
- 7、基于redis消息队列
- 8、redis和memcache的对比
- 9、redis数据淘汰机制
- 10、redis为什么把数据都放入内存
- 11、redis集群方案设计
- 12、如何保证redis中的数据都是热点数据
- 13、Jedis和Redisson的对比
- 14、redis hash槽的概念
- 15、redis集群节点之间复制
- 16、理解redis事务
- 17、redis内存优化
- 18、redis分区实现方案
- 19、redis分区的缺点
- 20、redis持久化方式
- 21、如何选择合适的持久化方式
- 22、AOF和RDB对比及优缺点
- 23、redis和数据库一致性解决方案
- 25、什么是缓存穿透,缓存雪崩,热点key问题
- 26、redis跳表实现原理
- 27、如何保证redis高可用性
- 28、如何保证redis数据一致性
- 29、redis如何动态扩缩容
- 30、redis哨兵机制
- 31、一致性hash算法在redis使用
- 32、redis aof重写
1、redis数据结构
Redis作为Key-Value型的内存数据库, 其Value有多种类型:
1.1、String
redis的string可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
应用场景:
(1)缓存:经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
(2)计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
(3)session:常见方案spring session + redis实现session共享。
示例:
127.0.0.1:8371> set a a
OK
127.0.0.1:8371> get a
"a"
127.0.0.1:8371> del a
(integer) 1
127.0.0.1:8371> get a
(nil)
127.0.0.1:8371> get counter
(nil)
127.0.0.1:8371> set counter 1
OK
127.0.0.1:8371> get counter
"1"
127.0.0.1:8371> incr counter
(integer) 2
127.0.0.1:8371> get counter
"2"
1.2、Hash
是一个Mapmap,指值本身又是一种键值对结构,如 value={{field1,value1},......fieldN,valueN}}
应用场景:
(1)缓存:相比string更节省空间,如用户信息,视频信息等。
示例:
127.0.0.1:8371> hmset user name zhangsan age 10
OK
127.0.0.1:8371> hgetall user
1) "name"
2) "zhangsan"
3) "age"
4) "10"
127.0.0.1:8371> hget user age
"10"
127.0.0.1:8371> hset user name lisi
(integer) 0
127.0.0.1:8371> hgetall user
1) "name"
2) "lisi"
3) "age"
4) "10"
1.3、List
链表(双端链表实现)、有序、value可重复,可通过下标取出对应value值,两边都能进行插入和删除。
应用场景:
(1)timeline:例如微博时间轴,有人发布微博,用lpush加入时间轴,展示新列表信息。使用:
Stack(栈):lpush+lpop
Queue(队列):lpush+rpop
Capped Collection(有限集合):lpush+ltrim(对列表进行修剪,让列表只保留指定区间内的元素,不在指定区间内元素将被删除)
Message Queue(消息队列):lpush+brpop(在消息队列尾阻塞地取出消息,参数0表示一直阻塞下去直到有消息在list中)
示例:
127.0.0.1:8371> lpush mylist 1 2 3 4 5 6
(integer) 6
127.0.0.1:8371> lrange mylist 0 -1
1) "6"
2) "5"
3) "4"
4) "3"
5) "2"
6) "1"
127.0.0.1:8371> lpop mylist //最新元素出来,栈的方式
"6"
127.0.0.1:8371> lpop mylist
"5"
127.0.0.1:8371>
127.0.0.1:8371>
127.0.0.1:8371> rpop mylist // 最旧原始出来,队列的方式
"1"
127.0.0.1:8371> rpop mylist
"2"
127.0.0.1:8371> rpop mylist
"3"
127.0.0.1:8371> rpop mylist
"4"
127.0.0.1:8371> rpop mylist
(nil)
1.4、Set
无序集合
特点:
不允许有重复的元素;
集合中的元素是无序的,不能通过索引下标获取元素;
支持集合间的操作,可以取多个集合取交集、并集、差集。
应用场景:
(1)标签、用户标签、给消息添加标签,同一标签或者类似标签推荐关注事或者关注人。
(2)点赞、点踩、收藏等
命令:
ismember 命令判断成员元素是否是集合的成员
smembers 命令返回集合中的所有的成员
sadd 向集合添加一个或多个成员
scard 获取集合的成员数
srem 移除集合中元素
示例:
127.0.0.1:8371>
127.0.0.1:8371> sadd myset a1 a2 a3 a4 a5 a6
(integer) 6
127.0.0.1:8371>
127.0.0.1:8371> smembers myset
1) "a3"
2) "a2"
3) "a1"
4) "a6"
5) "a5"
6) "a4"
127.0.0.1:8371> sismember myset a1
(integer) 1
127.0.0.1:8371> srem myset a3
(integer) 1
127.0.0.1:8371> smembers myset
1) "a2"
2) "a6"
3) "a5"
4) "a4"
5) "a1"
127.0.0.1:8371>
1.5、ZSet
有序集合对象的编码可以是ziplist或者skiplist。同时满足以下条件时使用ziplist编码:
- 元素数量小于128个;
- 所有member的长度都小于64字节;
- 其他:不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。
对于一个 REDIS_ENCODING_ZIPLIST 编码的 Zset, 只要满足以上任一条件, 则会被转换为REDIS_ENCODING_SKIPLIST 编码。
应用场景:
- 排行榜:有序集合经典使用场景。如小说、视频等网站需对用户上传小说、视频做排行榜,榜单可按照用户关注数、更新时间、字数等打分排行。
- 命令:zadd、zrange、zscore
127.0.0.1:8371>
127.0.0.1:8371> zadd myzset 100 11 99 33 95 22
(integer) 3
127.0.0.1:8371> zrange myzset 0 -1
1) "22"
2) "33"
3) "11"
127.0.0.1:8371> zscore myzset 33
"99"
127.0.0.1:8371>
2、redis适用场景
2.1、缓存
- 合理利用缓存能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略。
2.2、排行榜
- 网站排行榜应用,如京东月度销量榜单、商品按时间上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。
2.3、计数器
- 电商网站商品浏览量、视频网站视频播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。
2.4、分布式会话
- 集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
2.5、分布式锁
- 对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大场景可以使用数据库悲观锁、乐观锁来实现;但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
2.6、社交网络
- 点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。
最新列表Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
2.7、 消息系统
- 消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
3、redis内部实现
- Redis内部维护一个db数组,每个db都是一个数据库,默认情况下Redis会创建16个数据库。可以通过select命令来切换数据库,如select1切换到数据库号为1的数据库。select实现是通过修改客户端的db指针,通过指针指向不同的数据库来实现数据库的切换操作的。
4、redis如何实现rehash
- 使用了一种叫做渐进式哈希(rehashing)的机制来提高dict的缩放效率。
- 主动方式:调用dictRehashMilliseconds执行一毫秒。在redis的serverCron里调用,看名字就知道是为redis服务端准备的定时事件,每次执行1ms的dictRehash,简单粗暴。
被动方式:字典的增删改查(CRUD)调用dictAdd,dicFind,dictDelete,dictGetRandomKey等函数时,会调用_dictRehashStep,迁移buckets中的一个非空bucket。
int dictRehashMilliseconds(dict *d, int ms) {
long long start = timeInMilliseconds();
int rehashes = 0;
while(dictRehash(d,100)) {//每次最多执行buckets的100个链表rehash
rehashes += 100;
if (timeInMilliseconds()-start > ms) break;//最多执行ms毫秒
}
return rehashes;
}
static void _dictRehashStep(dict *d) {//只rehash一个bucket
//没有安全迭代器绑定在当前dict上时才能rehash,下文讲"安全迭代器"时会细说,这里只需要知道这是rehash buckets的1个链表
if (d->iterators == 0) dictRehash(d,1);
}
/* 哈希表节点 */
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
/* 哈希表
* 每个字典都使用两个哈希表,以实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
// 可以看作是:一个哈希表数组,数组的每个项是entry链表的头结点(链地址法解决哈希冲突)
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
/* 字典 */
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
dictht::table:哈希表内部的table结构使用了链地址法来解决哈希冲突,一个指向数组的指针,数组中的每一项都是entry链表的头结点。
dictht ht[2]:在dict的内部,维护了两张哈希表,作用等同于是一对滚动数组,一张表是旧表,一张表是新表,当hashtable的大小需要动态改变的时候,旧表中的元素就往新开辟的新表中迁移,当下一次变动大小,当前的新表又变成了旧表,以此达到资源的复用和效率的提升。
rehashidx:因为是渐进式的哈希,数据的迁移并不是一步完成的,所以需要有一个索引来指示当前的rehash进度。当rehashidx为-1时,代表没有哈希操作。
rehash主要代码实现(直接取自github的最新版本):
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
//判断dict是否正在rehashing,只有是,才能继续往下进行,否则已经结束哈希过程,直接返回。
if (!dictIsRehashing(d)) return 0;
// 分n步进行的渐进式哈希主体部分(n由函数参数传入)
// 对.used旧表中剩余元素数目的观察,增加安全性
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
// 断言保证一下渐进式哈希的索引没有越界。
assert(d->ht[0].size > (unsigned long)d->rehashidx);
// 为了跳过空桶,同时更新剩余可以访问的空桶数
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
// 所有元素都迁移到ht[1]
// 因为桶内的元素通常只有一个,或者不多于某个特定比率
// 所以可以将这个操作看作 O(1)
while(de) {
uint64_t h;
// 临时记录当前节点下一个节点
nextde = de->next;
/* Get the index in the new hash table */
// 计算在ht[1]中的hash值
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
// 添加节点到ht[1],调整指针
// 插入到链表的最前面,省时间
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
// 更新了两张表的当前元素数目
d->ht[0].used--;
d->ht[1].used++;
// 旧表指针后移
de = nextde;
}
// 每一步rehash结束,都要增加索引值,并且把旧表中已经迁移完毕的bucket置为空指针
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
// 判断一下旧表是否全部迁移完毕,若是,则回收空间,重置旧表,重置渐进式哈希的索引,否则用返回值告诉调用方,dict内仍然有数据未迁移
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
5、redis String类型实现原理
简单动态字符串 SDS,追加字符串,redis做什么动作?
- 计算出大小是否足够;
- 开辟空间满足所需大小。开辟与已使用大小len相同长度的空闲free空间(如果len < 1M)开辟1M长度的空闲free空间。
struct sdshrd{
int len;//记录已使用的长度
int free; //记录空闲未使用的长度
char buf[]; //字符数组
}
redis字符串的优势:
- 快速获取字符串长度,O(1)
- 避免缓冲区溢出
- 降低空间分配次数提升内存使用效率
- 空间预分配
- 对于追加操作来说,Redis不仅会开辟空间至够用而且还会预分配未使用的空间(free)来用于下一次操作。至于未使用的空间(free)的大小则由修改后的字符串长度决定。
- 当修改后的字符串长度len < 1M,则会分配与len相同长度的未使用的空间(free)
- 当修改后的字符串长度len >= 1M,则会分配1M长度的未使用的空间(free)
- 对于追加操作来说,Redis不仅会开辟空间至够用而且还会预分配未使用的空间(free)来用于下一次操作。至于未使用的空间(free)的大小则由修改后的字符串长度决定。
- 惰性空间回收
- 适用于字符串缩减操作
6、基于redis的分布式锁
分布式锁特性:
- 互斥性,保证只有一个线程持有锁;
- 避免死锁,不允许一个锁被永久持有的可能性,即使服务奔溃了,也要保证锁在一定期间内会被安全释放;
- 分区容错性,除非整个分布式系统瘫痪,只要有服务存活,都允许获取和释放锁;
- 谁上的锁由谁释放,不能被误解锁。
7、基于redis消息队列
- 广播订阅模式:基于redis的Pub/Sub机制,一旦有客户端往某个key里面publish一个消息,所有subscribe的客户端都会触发事件。
- 集群订阅模式:基于redis List双向 + 原子性 + BRPOP;redis宕机后,消息可能会丢失。
原理:
- redis的list队列性质:从left插入元素,从Right Pop元素(LPUSH、RPOP)
- 原子性
8、redis和memcache的对比
对比 | redis | memcached |
---|---|---|
持久化 | 是 | 否 |
支持类型 | 支持类型 list,hash,set等 | 支持k,v |
过期策略 | 多种过期策略 | 最大30天,策略LRU:1.惰性删除;2.LRU原则 |
灾难恢复 | 可以持久化恢复 | 无法恢复 |
9、redis数据淘汰机制
过期策略:
- 定时删除:设置过期时间同时,为key创建一个定时器(内存友好,cpu不友好)
- 惰性删除:对输入键进行检查是否过期;如果已经过期,则删除键(内存不友好,cpu友好)
- 定期删除:定时和惰性的折中;规定时间内,分多次遍历服务器各个数据库,从过期字典中随机检查一部分键的过期时间,并删除其中的过期键(减少对CPU的影响,减少内存浪费;操作时间频繁或过长,会退化成定时删除策略;操作次数太少获时间过短,会退化成惰性策略)
定期删除:设置键的过期时间同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
数据淘汰策略:
- 所有的key中
- allkeys-random:随机-random
- allkeys-lru:最近是否有访问-lru(最近最少使用)
- 有过期时间的key
- volatile-random:随机-random
- volatile-lru:最近是否访问-lru(最近最少使用)
- volatile-ttl:剩余过期时间少的-ttl
- no-enviction:不淘汰;所有申请内存命令会报错
一般情况:(1)只缓存:allkeys-lru;(2)既缓存又持久化:volatile-lru
10、redis为什么把数据都放入内存
为了达到最快的读写,将数据存放到内存中,并通过异步的方式将数据写到磁盘;如果不将数据放到内存中,磁盘的I/O速度会严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。如果设置了最大使用的内存,则数据已有记录数达到内存限值后将不能继续插入新值。
11、redis集群方案设计
- 高可用:3主3从;
- 数据量和访问量:估算应用需要的数据量和访问量,结合每个节点的容量和能承受的访问量,计算需要的主节点数量;
- 节点数量限制:redis官方节点数量限制为1000,主要考虑节点间的通信带来的消耗;大集群—》多个小集群;
- 适度的冗余。
哈希Slot和节点主从:
(1)节点主从
- 1个master,多个slaver;
- 读写分离,master只负责写和同步数据给slaver;
- slaver先将master那边获到的信息压入磁盘,再load进内存,client端从内存读取信息。
当新的slaver加入时,会找到相应的master,master发现新的从节点后将全量数据发送给新的slaver。
优点:读写分离,增加slave增加并发读。
缺点:master写能力是瓶颈。
(2)哈希槽:
- 使用CRC16算法哈希到指定的node上;
- 每个node被平均分配一个slot段,对应0-16384,slot不能重复也不能缺失,否则导致对象重复存储或无法存储;
- node之间也互相监听,一旦有node退出或者加入,会按照slot为单位做数据的迁移。
优点:提高写并发能力,扩容简单。
缺点:每个node相互监听,工作任务繁重。
(3)hash逻辑分节点,每个逻辑节点分主从。
12、如何保证redis中的数据都是热点数据
考察数据的分布情况:
- 如果数据呈现幂律分布,一部分数据访问频率高,一部分数据访问频率低,则可以使用allkeys-lru或allkeys-lfu;
- 如果数据呈现平等分布,所有的数据访问频率都相同,则使用allkeys-random。
13、Jedis和Redisson的对比
- Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
- Jedis中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。而Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用。
- 可伸缩性
- Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
- Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。
- 数据结构
- Jedis仅支持基本的数据类型如:String、Hash、List、Set、Sorted Set。
- Redisson不仅提供了一系列的分布式Java常用对象,基本可以与Java的基本数据结构通用,还提供了许多分布式服务
14、redis hash槽的概念
redis集群内置16384个哈希槽,当需要往redis集群中放入一个key-value时,redis先对key使用crc16算法算出一个结果,然后把结果对16384求余数,每个key对应到0-16383之间的哈希槽。
redis集群没有使用一致性hash算法,而是使用了哈希槽;使用CRC16(key) mod 16384的效果已经很不错。
哈希槽的有好处便于添加或移出节点:
- 添加节点时,只需要把其它节点的哈希槽挪到新的节点就可以了
- 移出节点时,只需要移出节点上的哈希槽挪到其它节点就行了
集群总共有2的14次方,16384个哈希槽,每个哈希槽存储的key和value是什么?
加入key,crc16(key) mod 16384计算分布到哪个hash slot中,一个hash slot中会有很多key和value;可以认为是表的分区,单节点redis只有一个表,所有key都在里面;redis集群会自动生成16384个分区表,insert时会根据上面的算法决定你的key应该再哪个分区
redis数据库所有键值存储在redisDb.dict中,dict的结构如下:
typedef struct dict {
// 特定于类型的处理函数
dictType *type;
// 类型处理函数的私有数据
void *privdata;
// 哈希表(2个)
dictht ht[2];
// 记录 rehash 进度的标志,值为-1 表示 rehash 未进行
int rehashidx;
// 当前正在运作的安全迭代器数量
int iterators;
} dict;
redis 的字典使用哈希表作为其底层实现。dict 类型使用的两个指向哈希表的指针,其中 0 号哈希表(ht[0])主要用于存储数据库的所有键值,而1号哈希表主要用于程序对 0 号哈希表进行 rehash 时使用,rehash 一般是在添加新值时会触发,这里不做过多的赘述。所以redis 中查找一个key,其实就是对进行该dict 结构中的 ht[0] 进行查找操作。
既然是哈希,那么我们知道就会有哈希碰撞,那么当多个键哈希之后为同一个值怎么办呢?redis采取链表的方式来存储多个哈希碰撞的键。也就是说,当根据key的哈希值找到该列表后,如果列表的长度大于1,那么我们需要遍历该链表来找到我们所查找的key。当然,一般情况下链表长度都为是1,所以时间复杂度可看作o(1)。
15、redis集群节点之间复制
16、理解redis事务
- MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
- EXEC:执行事务中的所有操作命令。
- DISCARD:取消事务,放弃执行事务块中的所有命令。
- WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
- UNWATCH:取消WATCH对所有key的监视。
为什么Redis不支持事务回滚?
多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。
17、redis内存优化
- redisObject对象
- 缩减键值对象
- 共享对象池
- 字符串优化
- 编码优化
- 控制key的数量
18、redis分区实现方案
分区:如何把数据存储到多个Redis实例中?
分区就是把你的数据分割到多个Redis实例中的一个过程,因此每个实例仅仅包含部分键。这篇文章第一部分介绍分区概念,第二部分将介绍Redis分区的用法。
为什么分区是非常有用的?
- 分区利用多台机器的内存构建一个更大数据库。如果不使用分区,数据库大小受限于单个计算机内存。
- 分区可以在多核和多计算机之间弹性扩展计算能力,并且分区可以在多计算机和网络适配器之间弹性扩展网络带宽。
分区基础:
有多种的分区标准。假设我们有4个Redis实例 R0,R1,R2,R3,很多表示用户的键例如 user:1,user:2等等,我们可以找到不同方式选择实例存储指定的键。换句话说有不同的系统映射一个指定的键到一个给定的Redis服务器。
一个最简单的方法是使用范围分区,并且通过映射某一范围的对象到特定的Redis实例。例如,我可以指定ID 0到10000的用户存储到实例R0,而ID 10001到20000的用户存储到实例R2等等。
该方案实际上是可以应用在实践中的,尽管他的缺点是需要一张映射对象范围与实例的表。这张表需要进行维护,并且我们需要为每种类型对象建立一张表,所以范围分区在Redis中常常是不受欢迎的,因为比其他分区方法更低效。
一个范围分区替代方法是哈希分区。此方案适用于任何形式键,无需键格式形如object_name:,就是这么简单:使用哈希方法(例如crc32哈希方法)将键名转换成数字。例如一个键名是foobar,crc32(foobar)输出结果形如93024922。
我是使用取模操作将该数字转换成0到3的数字,以便映射到四个Redis实例中的一个。93024922对4取余数等于2,这样我知道foobar键应该存储到R2实例中。注意:模操作返回除法运算的余数,大部分编程语言使用%(取余)就可以了。
存储数据 or 缓存数据?
- 如果Redis用来缓存数据,那么用一致性hash是比较容易实现扩展的。
- 如果Redis用来存储数据,那么key常对应固定的Redis实例,所以节点必须是固定的并且不能改变。
19、redis分区的缺点
- 不支持多个键的操作。比如你不能操作映射在两个Redis实例上的两个集合的交叉集。(其实可以做到这一点,但是需要间接的解决).
- Redis不支持多个键的事务。
- Redis是以键来分区,因此不能使用单个大键对数据集进行分片,例如一个非常大的有序集。如果使用分区,数据的处理会变得复杂,比如你必须处理多个RDB和AOF文件,在多个实例和主机之间持久化你的数据。
- 添加和删除节点也会变得复杂。例如通过在运行时添加和删除节点,Redis集群通常支持透明地再均衡数据,但是其他系统像客户端分区或者代理分区的特性就不支持该特性。不过Pre-sharding(预分片)可以在这方面提供帮助
20、redis持久化方式
- AOF和RDB(默认)
-
RDB三种触发机制
- save(同步):客户端向redis发送save命令来创建一个快照文件;【不消耗额外内存,但阻塞客户端命令】
- bgsave(异步):发送bgsave命令,redis调用fork创建一个子进程,子进程负责将快照写入硬盘,父进程继续处理命令请求;【消耗内存,但不阻塞客户端命令】
- 自动 :save 900 1 save 300 10 save 60 1000【通过bgsave创建】
-
AOF的三种策略
- always:每条redis写命令都同步写入硬盘【不丢数据,IO开销大】
- everysec:每秒执行一次同步,将多个命令写入硬盘【每秒一次fsync,丢1s数据】
- no:由操作系统决定何时同步
-
21、如何选择合适的持久化方式
- 达到足以的数据安全性:同时使用两种持久化功能;redis重启===》载入AOF文件;
- 非常关心数据,可以承受分钟内的丢失,只使用RDB的方式;
- 不推荐只使用AOF的持久化方式,定时生成的RDB快照非常便于数据库备份,RDB恢复速度比AOF要快。
22、AOF和RDB对比及优缺点
-
AOF:
- 优点:数据更完整、安全性更高,最多丢失1s数据;是一个只进行追加的日志文件,写入操作是以redis协议的格式保存,内容可读适合误删紧急恢复;
- 缺点:相同数据集,AOF文件大于RDB文件,数据恢复比较慢。aof重写,启动一个子进程,load已有的数据到磁盘文件上,并将这段时间的数据追加进去,写完后修改名称
-
RDB:
- 优点:压缩过的紧凑的文件,保持某个时间点的数据,适合做备份;最大化redis性能,fork子进程完成RDB文件创建;相比AOF恢复大数据集更快;
- 缺点:安全性不如AOF;服务宕机可能丢失几分钟的数据;redis数据集大时,fork子进程完成快照耗费CPU、耗时。定时导出全量的数据到磁盘上,可能会丢失数据;适合定时间的数据恢复;
23、redis和数据库一致性解决方案
方案1:采用延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
具体步骤:
- 先删除缓存
- 再写数据库
- 休眠500毫秒
- 再次删除缓存
方案2:异步更新缓存(基于订阅binlog的同步机制)
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis:
- 读Redis:热数据基本都在Redis
- 写MySQL:增删改都是操作MySQL
- 更新Redis数据:MySQL的数据操作binlog,来更新到Redis
由于数据库层面的读写并发,引发的数据库与缓存数据不一致的问题(本质是后发生的读请求先返回了),可能通过两个小的改动解决:
- 修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上
- 修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的
25、什么是缓存穿透,缓存雪崩,热点key问题
- 缓存雪崩:大量缓存同时失效,打爆数据库;
- 缓存穿透:查询不存在的key时,每次查询直接打到DB上,DB中也没有数据;
- 缓存击穿:缓存中没有,但是数据库中有数据;
26、redis跳表实现原理
什么是跳跃表?
- 跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他的几点指针,从而达到快速访问队尾目的。跳跃表的效率可以和平衡树想媲美了,最关键是它的实现相对于平衡树来说,代码的实现上简单很多。
跳跃表用在哪?
- 说真的,跳跃表在 Redis 中使用不是特别广泛,只用在了两个地方。一是实现有序集合键,二是集群节点中用作内部数据结构。
跳跃表原理?
和层级的关系很大,
加入1,level1:1->null
加入8,到level2:1->8->null
27、如何保证redis高可用性
28、如何保证redis数据一致性
主从复制工作机制:
当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照文件和命令后加载快照文件和缓存的执行命令。复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性
29、redis如何动态扩缩容
三主三从的集群,Redis集群扩容可以分为如下步骤:
- 准备新节点
- 加入集群
- 迁移槽和数据
// 配置信息
port 6385 //端口
cluster-enabled yes //开启集群模式
cluster-config-file nodes-6385.conf //集群内部的配置文件
cluster-node-timeout 15000 //节点超时时间,单位毫秒
// 其他配置和单机模式相同
// 启动两个节点
sudo redis-server conf/redis-6385.conf
sudo redis-server conf/redis-6386.conf
// 节点加入到集群
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6385
OK
127.0.0.1:6379> CLUSTER NODES
cb987394a3acc7a5e606c72e61174b48e437cedb 127.0.0.1:6385 master - 0 1496731333689 8 connected
......
迁移槽和数据:
- 可以使用redis-trib.rb工具,也可以通过手动命令的方式,但是一般要确保每个主节点负责的槽数是均匀的,因此要使用redis-trib.rb工具来批量完成,但是我们只是为了演示迁移的过程,所以接下来手动使用命令进行迁移。
收缩集群
- 收缩集群以为着缩减规模,需要从集群中安全下线部分节点。需要考虑两种情况:
(1) 确定下线的节点是否有负责槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个槽节点映射的完整性。
(2)当下线节点不在负责槽或着本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有节点忘记该节点后就可以正常关闭。
30、redis哨兵机制
哨兵是一个独立的进程,独立运行;原理为发送命令,等待redis服务器响应,从而监控运行的多个redis实例
作用:
- 通过发送命令,监控redis的主从服务器的状态。当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
故障切换过程:
- 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的
优缺点:
- 优点:有效解决主从模式主库异常手动主从切换的问题
- 缺点:运维复杂,哨兵选举期间,不能对外提供服务
31、一致性hash算法在redis使用
一致性hash算法的使用;缓存key均匀地映射到redis服务器上面
- 寻找slot块:redis内存的存储本质上是KV结构,那么对于一个key,进过crc16(key) % 13684就能够找到对应的slot块
- 寻找机器?
分片和一致性哈希相比:
- 它并不是闭合的,key的定位规则是根据CRC-16(key)%16384的值来判断属于哪个槽区,从而判断该key属于哪个节点,而一致性哈希是根据hash(key)的值来顺时针找第一个hash(ip)的节点,从而确定key存储在哪个节点。
- 一致性哈希是创建虚拟节点来实现节点宕机后的数据转移并保证数据的安全性和集群的可用性的。redis cluster是采用master节点有多个slave节点机制来保证数据的完整性的,master节点写入数据,slave节点同步数据。当master节点挂机后,slave节点会通过选举机制选举出一个节点变成master节点,实现高可用。但是这里有一点需要考虑,如果master节点存在热点缓存,某一个时刻某个key的访问急剧增高,这时该mater节点可能操劳过度而死,随后从节点选举为主节点后,同样宕机,一次类推,造成缓存雪崩。
32、redis aof重写
AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以AOF文件的大小随着时间的流逝一定会越来越大;
影响包括但不限于:对于Redis服务器,计算机的存储压力;AOF还原出数据库状态的时间增加;
为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。
- AOF重写的目的是为了解决AOF文件体积膨胀的问题,使用更小的体积来保存数据库状态,整个重写过程基本上不影响Redis主进程处理命令请求;
- AOF重写其实是一个有歧义的名字,实际上重写工作是针对数据库的当前状态来进行的,重写过程中不会读写、也不适用原来的AOF文件;
- AOF可以由用户手动触发,也可以由服务器自动触发。