《天净沙·我·Redis篇》 双非菜鸡奇葩,面试项目框架,java java,卑微学子去哪?


别问 问就是为了面试豁出了老命

来来来,讲一讲为什么Redis这么快?

首先,采用了多路复用io阻塞机制
然后,数据结构简单,操作节省时间
最后,运行在内存中,自然速度快

Redis为什么是单线程的?

Redis的瓶颈不是cpu的运行速度,而往往是网络带宽和机器的内存大小。再说了,单线程切换开销小,容易实现既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了

如果万一CPU成为你的Redis瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办

多起几个Redis进程就好了。Redis是keyvalue数据库,又不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。redis-cluster可以帮你做的更好

我们使用单线程的方式是无法发挥多核CPU 性能,有什么办法发挥多核CPU的性能嘛?

我们可以通过在单机开多个Redis

简述一下Redis值的五种类型

  • String 整数,浮点数或者字符串
  • Set 集合
  • Zset 有序集合
  • Hash 散列表
  • List 列表

String

数据结构 => SDS

1
2
3
4
5
6
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; //记录当前字节数组的长度
uint32_t alloc; //记录了当前字节数组总共分配的内存大小
unsigned char flags; //记录了当前字节数组的属性、用来标识到底是sdshdr8还是sdshdr16等
char buf[]; //保存了字符串真正的值以及末尾的一个\0
};

  1. 记录数组的长度,把复杂度从o(n)变成了0(1)
  2. SDS预分配存储空间的方式来减少内存的频繁分配
  3. redis不同长度的字符串用不同的数据结构,因此用flag标记当前是什么类型的数据结构

空间预分配

(sdscat =》给字符串后面再拼接一个字符串)

  • 当sdscat 之后内存小于 1M,字符串长度*2+1 (’\0’)
  • 当sdscat 之后内存大于 1M, 字符串长度 + 1M + 1(’\0’)

    空间懒惰回收

    如果sdstrim(减少字符串),则不急着回收空间,下次如果需要添加长度,直接使用多余的空间。

List

结构是双向链表
由于数据结构的设计,可以更方便的获取链表长度

链表的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void (*free) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match) (void *ptr,void *key);
}list;

ListNode节点数据结构

1
2
3
4
5
6
7
8
typedef  struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode

hash

哈希表

1
2
3
4
5
6
7
8
9
10
11
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;

哈希表节点

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next; // 单链表结构
} dictEntry;

字典

1
2
3
4
5
6
7
8
9
10
11
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

rehash

在字典中存在dictht数组,表明是两个hash表
ht[1]的容量是ht[0]的两倍
把ht[0]中的元素rehash复制到ht[1]中

渐进式rehash

原帖地址

进行读操作:会先去ht[0]中找,找不到再去ht[1]中找。
进行写操作:直接写在ht[1]中。
进行删除操作:与读类似。

但是每一次的增删改查的操作都会把数据从ht[0]转移到ht[1],是为了避免数据迁移导致的cpu负载问题

有序集合的实现方式是哪种数据结构?

跳跃表

Redis怎样防止异常数据不丢失?

RDB 持久化

将某个时间点的所有数据都存放到硬盘上。
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
如果数据量很大,保存快照的时间会很长。

AOF 持久化

将写命令添加到 AOF 文件(Append Only File)的末尾。
使用 AOF 持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:
选项同步频率always每个写命令都同步everysec每秒同步一次no让操作系统来决定何时同步
always 选项会严重减低服务器的性能;
everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量
随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。

讲一讲缓存穿透,缓存雪崩以及缓存击穿吧

  • 缓存穿透:就是客户持续向服务器发起对不存在服务器中数据的请求。客户先在Redis中查询,查询不到后去数据库中查询。
  • 缓存击穿:就是一个很热门的数据,突然失效,大量请求到服务器数据库中
  • 缓存雪崩:就是大量数据同一时间失效。

解决方案

  • 缓存穿透:
    1.接口层增加校验,对传参进行个校验,比如说我们的id是从1开始的,那么id<=0的直接拦截;
    2.缓存中取不到的数据,在数据库中也没有取到,这时可以将key-value对写为key-null,这样可以防止攻击用户反复用同一个id暴力攻击
  • 缓存击穿:
    最好的办法就是设置热点数据永不过期,拿到刚才的比方里,那就是你买腾讯一个永久会员
  • 缓存雪崩:
    1.缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
    2.如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。

集群

主从同步过程

  1. master启动线程生成RDB
  2. 同时master把新增的请求放到内存中
  3. slaver先将RDB文件写入磁盘
  4. slaver把RDB写入磁盘后再加载到内存中
  5. 最后master将缓存的请求再发送给slaver

同步过程细述

  1. 客户端向服务器发送SLAVEOF命令,让当前服务器成为Slave;

    从节点执行slaveof保存主节点信息
    从节点通过定时任务发现主节点信息,并建立连接
    从节点发送ping命令,主节点则返回pong命令
    成功建立连接

  2. 从节点根据自己是否保存Master runid来判断是否是第一次复制,
  3. 如果是第一次复制,则进行全量复制,从节点向Master发送PSYNC ? -1 命令来进行完整同步;
  4. 如果不是第一次复制,从节点向Master发送PSYNC runid offset;
  5. Master接收到PSYNC 命令后首先判断runid是否和本机的id一致,如果一致则会再次判断offset偏移量和本机的偏移量相差有没有超过 大小,如果没有那么就给Slave发送CONTINUE,此时Slave只需要等待Master传回失去连接期间丢失的命令;如果runid和本机id不一致或者双方offset差距超过了复制积压缓冲区大小,那么就会返回FULLRESYNC runid offset,Slave将runid保存起来,并进行完整同步。

    上述涉及三个信号
    FullReSync -> 全量复制信号
    Continue -> 部分复制信号
    Err -> psync进行部分复制的时候发现,offset偏移量和主节点的偏移量超过了复制积压缓存区,返回错误信号,进行全量复制FullResync

哨兵

原帖地址

基本原理

1. 定时任务
  • 通过向主从节点发送info命令获取最新的主从结构;
  • 通过发布订阅功能获取其他哨兵节点的信息;
  • 通过向其他节点发送ping命令进行心跳检测,判断是否下线
    2. 主观下线

    心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线。顾名思义,主观下线的意思是一个哨兵节点“主观地”判断下线

    3. 客观下线

    哨兵节点在对主节点进行主观下线后,会通过sentinel is-master-down-by-addr命令询问其他哨兵节点该主节点的状态;如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。
    客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作

    4. 选举领导者哨兵节点

    Raft算法 : 哪个节点先发出申请成为主节点,哪个节点就当master

    5. 故障转移
  1. 先过滤不健康的节点
  2. 根据优先级推荐出节点
  3. 选出来的节点成为主节点
  4. 原来的主节点变成新的主节点的从节点

Redis 和 Memcached 有啥区别,为啥选择用Redis作为你们的缓存中间件?

  1. Redis 相比 Memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。
  2. 在 redis3.x 版本中,便能支持 Cluster 模式,而 Memcached 没有原生的集群模式
  3. Redis 只使用单核,而 Memcached 可以使用多核,所以平均每一个核上 Redis 在存储小数据时比 Memcached 性能更高。而在 100k 以上的数据中,Memcached 性能要高于 Redis,虽然 Redis 最近也在存储大数据的性能上进行优化,但是比起 Remcached,还是稍有逊色
  4. memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小

Redis事务

Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis的事务本质上就是一串命令的执行,已经不可被打断