Redis基本数据结构和常见问题

一、基本数据结构及实现

Redis提供了丰富的数据结构,包括STRING(字符串)、LIST(列表)、SET(集合)、HASH(散列)和ZSET(有序集合)基本数据类型。

Redis的最是通过hashtable实现的。在Redis里面每个键值对都是一个dictEntry,通过指针指向key的存储结构和value的存储结构,此外还有一个next存储里指向下一个键值对的指针。

typedef struct dictEntry {
    void *key; //key void*表示任意类型指针

    union {                   
       void      *val;//value定义
       uint64_t  u64;
       int64_t   s64;
       double   d;
    } v;
    struct dictEntry *next;   //next指针
} dictEntry;

在dicEntry的外面还有一层redisDB。

/* Redis数据库结构体 */
typedef struct redisDb {
    // 数据库键空间,存放着所有的键值对(键为key,值为相应的类型对象)
    dict *dict;                 
    // 键的过期时间
    dict *expires;              
    // 处于阻塞状态的键和相应的client(主要用于List类型的阻塞操作)
    dict *blocking_keys;       
    // 准备好数据可以解除阻塞状态的键和相应的client
    dict *ready_keys;           
    // 被watch命令监控的key和相应client
    dict *watched_keys;         
    // 数据库ID标识
    int id;
    // 数据库内所有键的平均TTL(生存时间)
    long long avg_ttl;         
} redisDb;

我们知道Redis的key都是字符串类型,value的类型有多种。那Redis如何是适配这多种类型呢?于是就封装了一层redisObject。而我们所说的Redis数据类型的任何一种,都是通过redisObject存储的。

我们来看一下redisObject的结构:

typedef struct redisObject {
    //对象的数据类型,占4bits,共5种类型
    unsigned type:4;        
    //对象的编码类型,占4bits,共10种类型
    unsigned encoding:4;
    //least recently used
    //实用LRU算法计算相对server.lruclock的LRU时间
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    //引用计数
    int refcount;
    //指向底层数据实现的指针
    void *ptr;
} robj;

到这里就能看出一些奇妙的地方了,type是数据类型,encoding是编码类型,且数量不等。

127.0.0.1:6379> set number 123

OK

127.0.0.1:6379> object encoding number

"int"

127.0.0.1:6379> set story "long long ago,and long long ago,other long long ago"

OK

127.0.0.1:6379> object encoding story

"raw"

127.0.0.1:6379> set msg "hello world"

OK

127.0.0.1:6379> object encoding msg

"embstr"

到此我们大概看出来一些端倪,Redis的数据类型背后根据存储的数据不同使用的不同的编码存储。为什么要这样做呢?

节约存储空间!

其实远远不止这些,我们后面会详细说到。接下来就看每种数据结构有哪些编码类型。

三、数据结构详解

1、String

上文我们已经分析出了,String底层有三种。

int:存储8个字节的长整形

embstr:代表embsds格式的SDS,存储小于44字节的字符串

raw:存储大于44字节的SDS

接下来问题来了,什么是SDS呢?SDS全称是Simple Dynamic String,即简单动态字符串。

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 长度 */
    uint8_t alloc; /* 分配的内存大小 */
    unsigned char flags; /* 属性,标志不同种类的sds */
    char buf[];/* 内容 */
};

本质上就是一个char数组!

那么既然是一个char数组,为什么要通过SDS实现呢?

SDS

很简单,因为C语言中没有字符串类型,只有char数组。但是使用char数组会有一些问题,什么问题呢,大家可以想一下。

1、必须分配足够的空间,否则可能会溢出

2、如果要获取长度,需要遍历数组,时间复杂度是O(n)

3、如果长度变更,需要重新内存分配

4、字符串规则是遇到第一个'/0'即为结束,因此不能存放二进制的内容

好了,既然有了这些问题,SDS是如何解决的呢?

1、SDS实现了动态扩容,无需担心溢出的问题

2、定义了len属性,获取长度时间复杂度是O(1)

3、通过空间预分配和惰性空间释放,放置了重新分配内存

4、判断结束标志示len属性,避免了二进制不安全

哇,到这里是不是感觉到Redis的编码格式设计的很巧妙。别着急,慢慢来,还有。

embstr和raw,为什么要设计两个编码格式呢?就是为了长度不同吗?SDS也已经满足了呀?

实际原因是embstr的使用,只分配了一次内存,redisObject和SDS是一起分配的,二raw是分配了两次内存

Redis基础篇

 

那这三种类型之间是怎么转换的呢?

1、int 不在时整形,转成raw

2、int大小超过long的范围,转成embstr

3、embstr超过44字节,转成raw

注意:不可回转!!!

到此,String的数据结构就介绍完毕了。最后大家都思考一下String的使用场景有哪些?

1、缓存:热点数据,提升检索效率;

2、数据共享:session共享多个服务器共用;

3、分布式锁:setNx方法,判断是否添加成功;

4、全局唯一ID:INT类型的incrby;

5、计数器:INT类型

6、限流:INT类型

哈哈,是不是感觉String好强大,不需要其他数据类型了?那么问题来了,如果我要存一个对象怎么办?举个例子,存一个学生信息,包括姓名、年龄、学号等信息。大家可能会说,String存储一个json就行了,没错可以实现。但是我如果只想获取学生的年龄呢?

接下来介绍的Hash类型,就是解决这个问题的。

2、Hash

Hash类型是指Redis键值对中的值本身又是一个键值对结构,形如value=[{field1,value1},...{fieldN,valueN}],hash的value只能是字符串,不能嵌套其他类型。同样是存储字符串,Hash和String有什么区别呢?如下图所示:

Redis基础篇

 

从上图可以明显看出:

1、把所有相关的值聚集到一个key中,节省内存空间

2、只使用一个key,可以有效减少key冲突

3、当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU

那么它的底层编码是如何实现的呢?是不是使用dicEntry实现的呢?我们先来操作一波:

127.0.0.1:6379> hset user1 name aaaaaaaaaaaaaaaaaaa

(integer) 1

127.0.0.1:6379> hset user2 name aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

(integer) 1

127.0.0.1:6379> object encoding user1

"ziplist"

127.0.0.1:6379> object encoding user2

"hashtable"

可以看到,哈希类型的内部编码有两种:ziplist(压缩列表),hashtable(哈希表)。又有两个陌生的结构,不要慌张,我们下文会逐个分析。

ziplist

ziplist是一个经过特殊编码的,由连续的内存块组成的双向链表。它不存储指向上一个节点和下一个节点的指针,而是存储上一个节点的长度和当前节点的长度。这让数据在内存中更为紧凑,所以叫做zip,同时可以轻易地得到前驱后驱数据项的位置。

<zlbytes><zltail><zllen><entry>...<entry><zlend>

Redis基础篇

 

接下来我们具体看下实际元素里,究竟是怎么存储的。

typedef struct zlentry {
    //prevrawlen 前驱节点的长度
    //prevrawlensize 编码前驱节点的长度prevrawlen所需要的字节大小
    unsigned int prevrawlensize, prevrawlen;
    //len 当前节点值长度
    //lensize 编码当前节点长度len所需的字节数
    unsigned int lensize, len;
    //当前节点header的大小 = lensize + prevrawlensize
    unsigned int headersize;
    //当前节点的编码格式
    unsigned char encoding;
    //指向当前节点的指针,以char *类型保存
    unsigned char *p;
} zlentry;                 

Redis基础篇

 

那么什么时候使用ziplist呢?

hash对象保存的键值对数量<512所有键值对字符串长度小于54字节

如果任何一个条件不满足,存储结构就会转成hashtable。接下来介绍hashtable

hashtable

前面我们知道了,Redis的KV结构是通过dictEntry来实现的。在hashtable中,又对dictEntry进行了多层封装。

typedef struct dictht {
    // 两个哈希表
    dictEntry **table;
    // 哈希表的大小
    unsigned long size;
    // 哈希表大小掩码
    unsigned long sizemask;
    // 哈希表中数据项数量
    unsigned long used;
} dictht;

dictEntry放在了dictht(hashtable)里面了

typedef struct dict {
    // 哈希表的类型,包括哈希函数,比较函数,键值的内存释放函数
    dictType *type;
    // 存储一些额外的数据
    void *privdata;
    // 两个哈希表
    dictht ht[2];
    // 哈希表重置下标,指定的是哈希数组的数组下标
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 绑定到哈希表的迭代器个数
    int iterators; /* number of iterators currently running */
} dict;

dictht放在了dict里面了。

从源码可以看出,它是一个数组+链表的结构。如图:

Redis基础篇

 

我们发现dictht后面是个null,说明第二个hashtable没有数据。那么为什么要定义两个hashtable,其中一个不用呢?

答案是为了扩展哈希表!ht默认使用ht[0],ht[1]不初始化。hash表有一个普遍存在的问题就是hash冲突,如果冲突过多都会放置在上图的dicEntry数组里。这样hash的读性能就退化称数组的遍历,效率降低。解决hash冲突的有效手段就是对hash进行扩容,让数据足够分散。这个过程会进行一次rehash,这个过程中就会使用到ht[1]。首先是初始化ht[1]的大小为ht[0]的最小n次幂。然后将ht[0]的数据写入ht[1],然后再将ht[0]释放。怎么样这个设计过程是不是很巧妙?

使用场景:

1、和String一样!毕竟hash存储的也是String。

2、存储对象类型的数据:比string节省key。

3、购物车:key:用户id,filed:商品id,value:数量。增加、减少、删除等都可以操作。

3. List

List主要用来存储有序数据,数据可重复。

先来操作一波:

127.0.0.1:6379> lpush queue a (integer) 1 127.0.0.1:6379> lpush queue b c (integer) 3 127.0.0.1:6379> object encoding queue "quicklist"

可以看到list的底层结构是quicklist来实现的。接下来就介绍一下quicklist。

quicklist

quicklist 实际上是 ziplist 和 linkedlist 的混合体,它将 linkedList 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。

typedef struct quicklist {
    quicklistNode *head;//指向第一个quicklistNode
    quicklistNode *tail;//指向最后一个quicklistNode
    unsigned long count; //在所有ziplist中entry的个数总和
    unsigned int len;//quicklistNode的个数
    int fill : 16; //ziplist大小限定,由server.list_max_ziplist_size给定
    unsigned int compress : 16; //节点压缩深度设置,由server.list-compress-depth给定,0表示关闭压缩
} quicklist;

看到里面用到了一个quicklistNode存储数据。

typedef struct quicklistNode {
    struct quicklistNode *prev; //上一个node节点
    struct quicklistNode *next; //下一个node
    unsigned char *zl;            //保存的数据 压缩前ziplist 压缩后压缩的数据
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

Redis基础篇

 

ziplist上文已经介绍了,就不多说了。总的来说,quicklist就是ziplist 和 linkedlist 的混合体。

应用场景:

1、列表:文章列表、热门排行榜之类的

2、队列/栈:list有两个阻塞操作:BLPOP/BRPOP

4、set

set存储String类型的无序集合,最大存储2^32-1。

先操作一波:

127.0.0.1:6379> sadd myset a b c

(integer) 3

127.0.0.1:6379> object encoding myset

"hashtable"

127.0.0.1:6379> sadd newset 1 2 3

(integer) 3

127.0.0.1:6379> object encoding newset

"intset"

可以看到set的编码格式有两种intset和hashtable。如果第一个原始是一个整数,就会初始化为intset,如果intset保存的值的数量大于512(set_max_intset_entries默认值)个,会转化称hashtable。

typedef struct intset {
    uint32_t encoding;//数组中的值的编码方式
    uint32_t length;//数组长度
    int8_t contents[];//数组,记录每个值
} intset;

使用场景:

1、抽奖:spop

2、点赞、打卡:数据集无序,sadd添加,srem取消,sismember是否操作,smembers所有,scard总数

3、标签:sadd添加 sinter筛选

5、zset

sorted set存储的是有序的集合元素。它是为每个元素添加了一个score,按照score的大小排序。

操作一波:

127.0.0.1:6379> zadd myzset 1 java 2 redis 3 mysql

(integer) 3

127.0.0.1:6379> object encoding myzset

"ziplist"

127.0.0.1:6379> zadd myzsetnew 4 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

(integer) 1

127.0.0.1:6379> object encoding myzsetnew

"skiplist"

可以看到,有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表)组成的。

当数据比较少时(<128),且所有元素长度都小于64字节,有序集合使用的是 ziplist 存储的,否则使用skiplist结构存储。

ziplist我们已经很熟悉了,那么问题来了,什么是skiplist?

skiplist

skiplist是一个特殊的链表,相比一般的链表,有更高的查找效率,其效率可比拟于二叉查找树。

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头尾指针 
    unsigned long length;   // skiplist的长度  
    int level;  // 最高多少级链表 
} zskiplist;

zskiplist的定义,没啥内容,就头尾指针、长度和级数,重点还是在zskiplistNode中。

typedef struct zskiplistNode {
    sds ele;   // 节点存储的具体值 
    double score;   // 节点对应的分值 
    struct zskiplistNode *backward; // 前向指针
    struct zskiplistLevel {
    	struct zskiplistNode *forward; // 每一层的后向指针 
    	unsigned long span;  // 到下一个节点的跨度 
    } level[];
} zskiplistNode;

通过上面的源码,我们可以简单画一下大概的结构图。

Redis基础篇

 

很明显查询我们需要遍历整个链表,效率低。我们想要提高其查找效率,可以考虑在链表上建索引的方式。每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引。这就是skiplist的实现方式。

Redis基础篇

 

在图中,需要寻找 68,在给出的查找过程中,利用跳表数据结构优势,只比较了 3次。由此可见,跳表预先间隔地保存了有序链表中的节点,从而在查找过程中能达到类似于二分搜索的效果,而二分搜索思想就是通过比较中点数据放弃另一半的查找,从而节省一半的查找时间。

应用场景:顺序会动态变化的场景。

1、热榜、热搜:incrby进行加1,zrevrange获取排序

到这里常用的数据类型就介绍完毕了,简单总结一下。

Redis基础篇

Redis基础篇

其他数据结构

bitmap

Redis基础篇

 

通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。bitmap本身会极大的节省储存空间。

Redis从2.2.0版本开始新增了setbit,getbit,bitcount等几个bitmap相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit等命令只不过是在set上的扩展。

使用场景:

1、在线用户统计

2、每日用户访问统计

hyperloglogs

HyperLogLog是用来做基数统计的算法,它的优点是在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。什么是基数呢?举个例子:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

使用场景:统计网站的UV,日活。

geo

顾名思义,是用来地理位置计算的。

使用场景:附近的人

Stream

5.0之后新出的数据类型,支持广播的可持久化消息队列,类似于kafka。

使用场景:消息队列

Redis客户端

客户端和服务端通信,必须要有一种通信协议,比如我们经常使用的Rest协议、Http协议,TCP协议等都是一种通信协议。

Redis 的客户端和服务端之间采取了一种独立的名为 RESP(REdis Serialization Protocol) 的协议,作者主要考虑了以下几个点:

  • 容易实现
  • 解析快
  • 人类可读

RESP 主要可以序列化以下几种类型:整数,单行回复(简单字符串),数组,错误信息,多行字符串。Redis 客户端向服务端发送的是一组由执行的命令组成的字符串数组,服务端根据不同的命令回复不同类型的数据,但协议的每部分都是以 “\r\n” (CRLF) 结尾的。另外 RESP 是二进制安全的,不需要处理从一个进程到另一个进程的传输,因为它使用了前缀长度进行传输。

在 RESP 中, 一些数据的类型通过它的第一个字节进行判断:

  • 单行回复:回复的第一个字节是 “+”
  • 错误信息:回复的第一个字节是 “-“
  • 整形数字:回复的第一个字节是 “:”
  • 多行字符串:回复的第一个字节是 “$“
  • 数组:回复的第一个字节是 “*”

以 “+” 开头,以 “\r\n” 结尾的字符串形式。

具体协议在此就不多介绍了,感兴趣的同学可以私下自己学习研究。

既然有固定的协议,那我们能不能写段代码执行和解析下这个协议呢?当然可以!接下来给大家演示一下:

/**
 * @author liwei
 * @description redis客户端实现
 * @date 2021/03/15
 */
public class RedisClient {
    private Socket socket;
    private OutputStream writer;
    private InputStream reader;
    public RedisClient(String host, int port) throws IOException {
        //与Redis服务端的Socket连接
        socket = new Socket(host, port);
        writer = socket.getOutputStream();
        reader = socket.getInputStream();
    }
    //set命令
    public String set(String key, String value) throws IOException {
        StringBuffer conmmand = new StringBuffer();
        conmmand.append("*3").append("\r\n");
        conmmand.append("$3").append("\r\n");
        conmmand.append("SET").append("\r\n");
        conmmand.append("$").append(key.getBytes().length).append("\r\n");
        conmmand.append(key).append("\r\n");
        conmmand.append("$").append(value.getBytes().length).append("\r\n");
        conmmand.append(value).append("\r\n");
        return exceConmmand(conmmand);
    }
    //get key
    public String get(String key) throws IOException {
        StringBuffer conmmand = new StringBuffer();
        conmmand.append("*2").append("\r\n");
        conmmand.append("$3").append("\r\n");
        conmmand.append("get").append("\r\n");
        conmmand.append("$").append(key.getBytes().length).append("\r\n");
        conmmand.append(key).append("\r\n");
        return exceConmmand(conmmand);

    }
    //执行命令
    public String exceConmmand(StringBuffer conmmand) throws IOException {
        writer.write(conmmand.toString().getBytes());
        byte[] result = new byte[1024];
        reader.read(result);
        return new String(result);
    }
}

客户端执行程序:

public class ClientTest {
    public static void main(String[] args) {
        try {
            RedisClient client = new RedisClient("49.233.195.72", 6379);
            String set = client.set("redis", "test");
            System.out.println("set:"+set);
            String get = client.get("redis");
            System.out.println("get:"+get);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

set:OK
get:test

Redis 常见问题

1、什么是Redis?

Redis(Remote Dictionary Server)本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。 Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能,比方说用他的List来做FIFO双向链表,实现一个轻量级的高性 能消息队列服务,用他的Set可以做高性能的tag系统等等。另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一 个功能加强版的memcached来用。 Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

Redis是单线程的,即一个线程处理所有网络请求,其他模块仍用了多个线程。Redis是直接操作内存的,避免了频繁的切换上下文。

2、Redis相比memcached有哪些优势?

  • memcached所有的值均是简单的字符串,redis支持更为丰富的数据类型:String、List、Set、Sorted Set、Hash
  • redis的速度比memcached快很多
  • redis可以持久化其数据

3、Redis有哪几种数据淘汰策略?

  • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
  • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

4、一个字符串类型的Key能存储最大容量512M, Value能存储最大容量512M.

5、Redis集群方案应该怎么做?都有哪些方案?

  • twemproxy,大概概念是,它类似于一个代理方式,使用方法和普通redis无任何区别,设置好它下属的多个redis实例后,使用时在本需要连接redis的地方改为连接twemproxy,它会以一个代理的身份接收请求并使用一致性hash算法,将请求转接到具体redis,将结果再返回twemproxy。使用方式简便(相对redis只需修改连接端口),对旧项目扩展的首选。 问题:twemproxy自身单端口实例的压力,使用一致性hash后,对redis节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。
  • codis,目前用的最多的集群方案,基本和twemproxy一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新hash节点。
  • corvus,饿了么开发,加在redis cluster前面的一个客户端代理,主要作用是在不侵入代码的情况下使用redis cluster,业务代理里面对redis的使用与原来单点的实例没有区别
  • redis cluster3.0自带的集群,特点在于他的分布式算法不是一致性hash,而是hash槽的概念,以及自身支持节点设置从节点。具体看官方文档介绍。

6、Redis集群方案什么情况下会导致整个集群不可用?

有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。

7、MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

8、Redis有哪些适合的场景?

(1)会话缓存(Session Cache):最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。

(2)全页缓存(FPC):除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

(3)队列:Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。

(4)排行榜/计数器:Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORES 。Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。

(5)发布/订阅:Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!

9、Redis如何设置密码及验证密码?

设置密码:config set requirepass 123456 授权密码:auth 123456

10、Redis哈希槽

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。11.

11、为什么有16384个槽:https://github.com/antirez/redis/issues/2576

如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。

在消息头中,最占空间的是 myslots[CLUSTER_SLOTS/8]。当槽位为65536时,这块的大小是: 65536÷8=8kb因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。 

redis的集群主节点数量基本不可能超过1000个。

如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。 

槽位越小,节点少的情况下,压缩率高。

Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。而16384÷8=2kb

12、Redis集群的主从复制模型是怎样的?

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品.

13、Redis集群会有写操作丢失吗?为什么?

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

14、Redis集群之间是如何复制的?

异步复制

15、Redis集群最大节点个数是多少?

16384个。

16、Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

17、Redis中的管道有什么用?

一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多POP3协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。

18、怎么理解Redis事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

19、Redis事务相关的命令有哪几个?

MULTI、EXEC、DISCARD、WATCH ##28、Redis key的过期时间和永久有效分别怎么设置? EXPIRE和PERSIST命令。

20、Redis如何做内存优化?

尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面.

21、Redis回收进程如何工作的?

一个客户端运行了新的命令,添加了新的数据。Redi检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。一个新的命令被执行,等等。所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。**

22、Redis回收使用的是什么算法?

**LRU算法

23、Redis如何做大量数据插入?

Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。

24、为什么要做Redis分区?

分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

25、你知道有哪些Redis分区实现方案?

客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

26、Redis分区有什么缺点?

涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。同时操作多个key,则不能使用Redis事务.分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set).当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

27、Redis持久化数据和缓存怎么做扩容?

如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。

28、分布式Redis是前期做还是后期规模上来了再做好?为什么?

既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

29、支持一致性哈希的客户端有哪些?

Redis-rb、Predis等。

30、Redis与其他key-value存储有什么不同?

Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,应为数据量不能大于硬件内存。在内存数据库方面的另一个优点是, 相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。 同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

31、Redis的内存占用情况怎么样?

给你举个例子: 100万个键值对(键是0到999999值是字符串“hello world”)在我的32位的Mac笔记本上 用了100MB。同样的数据放到一个key里只需要16MB, 这是因为键值有一个很大的开销。 在Memcached上执行也是类似的结果,但是相对Redis的开销要小一点点,因为Redis会记录类型信息引用计数等等。当然,大键值对时两者的比例要好很多。64位的系统比32位的需要更多的内存开销,尤其是键值对都较小时,这是因为64位的系统里指针占用了8个字节。 但是,当然,64位系统支持更大的内存,所以为了运行大型的Redis服务器或多或少的需要使用64位的系统。

32、都有哪些办法可以降低Redis的内存使用情况呢?

如果你使用的是32位的Redis实例,可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。

##43、查看Redis使用情况及状态信息用什么命令?info44、Redis的内存用完了会发生什么? 如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以将Redis当缓存来使用配置淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。## 45、Redis是单线程的,如何提高多核CPU的利用率? 可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。

33、一个Redis实例最多能存放多少的keys?

List、Set、Sorted Set他们最多能存放多少元素?理论上Redis可以处理多达232的keys,并且在实际中进行了测试,每个实例至少存放了2亿5千万的keys。我们正在测试一些较大的值。任何list、set、和sorted set都可以放232个元素。换句话说,Redis的存储极限是系统中的可用内存值。

34、Redis常见性能问题和解决方案?

(1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件 (2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次 (3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内 (4) 尽量避免在压力很大的主库上增加从库 (5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。

35、Redis提供了哪几种持久化方式?

RDB持久化方式将某个时间点上Redis中的数据保存到一个RDB文件中,该文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时Redis中的数据。SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。

AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大.如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.你也可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.最重要的事情是了解RDB和AOF持久化方式的不同,让我们以RDB持久化方式开始。

一般我们在生产上采用的持久化策略为:

  • master关闭持久化:为了保证读写最佳性能,将master的持久化关闭!主线程需要调用系统函数fork(),构建出一个子进程进行持久化!很不幸的是,在构建子进程的过程中,父进程就会阻塞,无法响应客户端的请求!
  • slave开RDB即可,必要的时候AOF(Append only file)和RDB都开启

该策略只能够适应绝大部分场景:因为这套策略存在部分的数据丢失可能性。redis的主从复制是异步的,master执行完客户端请求的命令后会立即返回结果给客户端,然后异步的方式把命令同步给slave。因此master可能还未来得及将命令传输给slave,就宕机了,此时slave变为master,数据就丢了。

对采用redis读写分离架构不合适,因为必须要考虑主从同步的延迟性问题

36、如何选择合适的持久化方式?

一般来说, 如果想达到足以媲美PostgreSQL的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。有很多用户都只使用AOF持久化,但并不推荐这种方式:因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外, 使用RDB还可以避免之前提到的AOF程序的bug。

37、修改配置不重启Redis会实时生效吗?

针对运行实例,有许多配置选项可以通过 CONFIG SET 命令进行修改,而无需执行任何形式的重启。 从 Redis 2.2 开始,可以从 AOF 切换到 RDB 的快照持久性或其他方式而不需要重启 Redis。检索 ‘CONFIG GET *’ 命令获取更多信息。但偶尔重新启动是必须的,如为升级 Redis 程序到新的版本,或者当你需要修改某些目前 CONFIG 命令还不支持的配置参数的时候。

38、redis的hash怎么实现的,rehash过程和JavaHashMap的rehash有什么区别

39、redis cluster怎么做到高可用的?redis的持久化机制,为啥不能用redis做专门的持久化数据库存储?

40、redis 缓存

缓存雪崩

  • 发生场景 : 当Redis服务器重启或者大量缓存在同一时期失效时,此时大量的流量会全部冲击到数据库上面,数据库有可能会因为承受不住而宕机
  • 解决方案 :
    • 均匀分布 : 我们应该在设置失效时间时应该尽量均匀的分布,比如失效时间是当前时间加上一个时间段的随机值
    • 熔断机制 : 类似于SpringCloud的熔断器,我们可以设定阈值或监控服务,如果达到熔断阈值(QPS,服务无法响应,服务超时)时,则直接返回,不再调用目标服务,并且还需要一个检测机制,如果目标服务已经可以正常使用,则重置阈值,恢复使用
    • 隔离机制 : 类似于Docker一样,当一个服务器上某一个tomcat出了问题后不会影响到其它的tomcat,这里我们可以使用线程池来达到隔离的目的,当线程池执行拒绝策略后则直接返回,不再向线程池中增加任务
    • 限流机制 : 其实限流就是熔断机制的一个版本,设置阈值(QPS),达到阈值之后直接返回
    • 双缓存机制 : 将数据存储到缓存中时存储俩份,一份的有效期是正常的,一份的有效期长一点.不建议用这个方案,因为比较消耗内存资源,毕竟Redis是直接存储到内存中的

缓存穿透

  • 发生场景 : 此时要查询的数据不存在,缓存无法命中所以需要查询完数据库,但是数据是不存在的,此时数据库肯定会返回空,也就无法将该数据写入到缓存中,那么每次对该数据的查询都会去查询一次数据库
  • 解决方案 :
    • 布隆过滤 : 我们可以预先将数据库里面所有的key全部存到一个大的map里面,然后在过滤器中过滤掉那些不存在的key.但是需要考虑数据库的key是会更新的,此时需要考虑数据库 --> map的更新频率问题
    • 缓存空值 : 哪怕这条数据不存在但是我们任然将其存储到缓存中去,设置一个较短的过期时间即可,并且可以做日志记录,寻找问题原因

缓存预热

  • 其实这个不是一个问题,是一种机制,在上线前先将需要缓存的数据放到缓存中去,这个的实现很简单,可以在启动的时候放(数据比较小),做一个开关(一个隐秘的接口),定时刷新缓存

缓存更新

  • 怎么样保证缓存中的key是实时有效的,以及及时的更新数据资源
    • 监测机制 : 定时去监测Redis,查看过期的缓存,
      问题 :
      在看到这里的时候我有一个问题,如果key过期了那么我要不要再将key重新放入缓存呢,如果放入的话我设置这个有效期就完全没有必要了,完全可以设置为永久有效
      我想了一个解决方案,我们可以对命中率做一个记录,如果这个key在最近一段时间内被频繁命中的话,我们就在失效时进行更新,否则就直接清除掉
    • 被动更新 : 每次请求过来时我们判断一下当前key是否失效,失效就重新查询存放到缓存中,这个问题不会涉及到监测机制那个问题

服务降级

  • 服务降级是不得已而为之的,在关键的时候丢卒保帅,保证核心功能正常运行
    • 服务拒绝 : 直接拒绝掉非核心功能的所有请求,其实基本就是直接废弃掉某些模块
    • 服务延迟 : 将请求加入到线程池中或队列中,延迟执行这些请求
  • 注意 : 服务降级一定要有对应的恢复策略,不能降下去就不回来了,我们可以监测服务的状态,当状态适当时恢复服务的正常使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值