Redis 基础


Redis 基础

Redis 定位 - 特性

关系型数据库 特性

# 特点
- 它以表格的形式,基于行存储数据,是一个二维的模式;
- 它存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应表结构;
- 表与表之间存在关联(Relationship);
- 大部分关系型数据库都支持 SQL(结构化查询语言)的操作,支持复杂的关联查询;
- 通过支持事务ACID(酸)来提供严格或者实时的数据一致性。

# 缺点
- 实现扩容技术复杂,分为Scale-up(纵向扩展)和Scale-out(横向扩展);
- 表结构修改困难,因此存储的数据格式也受到限制;
- 在高并发和高数据量的情况下,关系型数据库通常会把数据持久化到磁盘,基于磁盘的读写压力比较大。

非关系型数据库 特性

# 特点
- 存储非结构化的数据,比如文本、图片、音频、视频;
- 表与表之间没有关联,可扩展性强;
- 保证数据的最终一致性。遵循 BASE(碱)理论。Basically Available(基本可用),Soft-state(软状态),Eventually Consistent(最终一致性);
- 支持海量数据的存储和高并发的高效读写;
- 支持分布式,能够对数据进行分片存储,扩缩容简单。

Redis 特性

- Redis 是使⽤ C 语⾔开发的数据库,与传统数据库不同的是 Redis 的数据是存在内存中的,Redis是内存数据库,所以读写速度⾮常快,因此 Redis 被⼴泛应⽤于缓存⽅向;
- 丰富的数据类型;
- 功能丰富,如分布式锁、持久化、过期策略;

Redis 安装 - 启动 - 使用

Redis 安装

Redis 启动

Redis 使用

  • 切换数据库
select 0
  • 清空当前数据库
flushdb
  • 清空所有数据库
flushall
  • 查看key对外类型
type key
  • 查看key内部类型
object encoding key
  • 查看生成快照时间
lastsave

Redis 数据类型

对象 对象type属性值 type命令输出 底层的存储结构 object encoding
字符串对象 (String) OBJ_STRING string OBJ_ENCODING_INT
OBJ_ENCODING_EMBSTR
OBJ_ENCODING_RAW
int
embstr
raw
列表对象 (list) OBJ_LIST list OBJ_ENCODING_QUICKLIST quicklist
哈希对象 (Hash) OBJ_HASH hash OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_HT
ziplist
hashtable
集合对象 (Sets) OBJ_SET set OBJ_ENCODING_INTSET
OBJ_ENCODING_HT
intset
hashtable
有序集合对象 (Sorted Sets) OBJ_ZSET zset OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_SKIPLIST
ziplist
skiplist(包含ht)

字符串 (String)

String - 使用
- 字符串是一种最基本的Redis值类型。Redis字符串是二进制安全的,这意味着一个Redis字符串能包含任意类型的数据,例如:一张JPEG格式的图片或者一个序列化的Ruby对象。

# 可使用命令
- SET: ;
- SETNX: 
- SETEX
- PSETEX
- GET
- GETSET
- STRLEN
- APPEND
- SETRANGE
- GETRANGE
- INCR
- INCRBY
- INCRBYFLOAT
- DECR
- DECRBY
- MSET
- MSETNX
- MGET
  • set - get

将字符串值 value 关联到 key,如果 key 已经持有其他值,SET 就覆写旧值,无视类型;

# 可选参数
- EX seconds:  将键的过期时间设置为 seconds 秒。执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value;
- PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value;
- NX : 只在键不存在时,才对键进行设置操作。执行 SET key value NX 的效果等同于执行 SETNX key value ;
- XX : 只在键已经存在时,才对键进行设置操作。

# 设置key value
> set k1 v2
OK
> get k1
"v1"

# 设置 EX(过期时间 expire time 单位:S)
> SET key-with-expire-time "hello" EX 10086
OK

> GET key-with-expire-time
"hello"

> TTL key-with-expire-time
(integer) 10069

# 使用 NX选项
> SET not-exists-key "value" NX
OK      # 键不存在,设置成功

> GET not-exists-key
"value"

> SET not-exists-key "new-value" NX
(nil)   # 键已经存在,设置失败

> GEt not-exists-key
"value" # 维持原值不变

# 使用 XX选项
> EXISTS exists-key
(integer) 0

> SET exists-key "value" XX
(nil)   # 因为键不存在,设置失败

> SET exists-key "value"
OK      # 先给键设置一个值

> SET exists-key "new-value" XX
OK      # 设置新值成功

> GET exists-key
"new-value"
  • incr - decr
# 设置数字 incr(为键 key 储存的数字值加上1)
> SET page_view 20
OK

> INCR page_view
(integer) 21

> GET page_view    # 数字值在 Redis 中以字符串的形式保存
"21"

# 设置数字 decr(为键 key 储存的数字值减去1)
redis> SET failure_times 10
OK

redis> DECR failure_times
(integer) 9
  • mset - mget
> mset key1 value1 key2 value2
OK
> mget key1 key2
1) "value1"
2) "value2"
  • 其它
# STRLEN key(返回键 key 储存的字符串值的长度)
> SET mykey "Hello world"
OK

> STRLEN mykey
(integer) 11

# APPEND key value(如果键 key 已经存在并且它的值是一个字符串,APPEND 命令将把 value 追加到键 key 现有值的末尾)
> EXISTS myphone               # 确保 myphone 不存在
(integer) 0

> APPEND myphone "nokia"       # 对不存在的 key 进行 APPEND ,等同于 SET myphone "nokia"
(integer) 5  

> APPEND myphone " - 1110"     # 对已存在的字符串进行 APPEN 长度从 5 个字符增加到 12 个字符
(integer) 12

> GET myphone
"nokia - 1110"
String - 原理

dict.h

typedef struct dictEntry {
    void *key; /* key 关键字定义 */
    union {
        void *val; /* value 定义 */
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; /* 指向下一个键值对节点 */
} dictEntry;

server.h

typedef struct redisObject {
    unsigned type:4; /* 对象的类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
    unsigned encoding:4; /* 具体的数据结构 */
    unsigned lru:LRU_BITS; /* 24 位,对象最后一次被命令程序访问的时间,与内存回收有关 */
    /* 
     * LRU time (relative to global lru_clock) 
     * or LFU data (least significant 8 bits frequency 
     * and most significant 16 bits access time). 
     */
    int refcount;/* 引用计数。当 refcount 为 0 的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了 */
    void *ptr;/* 指向对象实际的数据结构 */
} robj;

sds.h

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
...
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used 当前字符数组使用长度 */
    uint32_t alloc; /* excluding the header and null terminator 当前字符数组总共分配的内存大小*/
    unsigned char flags; /* 3 lsb of type, 5 unused bits 当前字符数组的属性、用来标识到底是 sdshdr8、sdshdr32等*/
    char buf[]; /* 字符串存储的值 */
};
...
  • 字符串类型内部编码
# 字符串类型内部编码
- int: 存储8个字节的长整型;
- embstr: 代表embstr格式的sds(Simple Dynamic String 简单动态字符串),存储小于44个字节的字符串;
- raw: 存储大于44个字节的字符串(3.2版本之前39字节)。#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44

# String 类型内部编码查看
> set number 1
OK
> set str "this strtype is embstr "
OK
> 127.0.0.1:6379> set rawstr "this strtype is embstr aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
OK

> type number
string
> type str
string
> type rawstr
string

> OBJECT encoding number
"int"
> OBJECT encoding str
"embstr"
> OBJECT encoding rawstr
"raw"
String - 问题
  • SDS是什么?
SDS: 全称Simple Dynamic String(简单动态字符串),Redis 中字符串的实现;
  • Redis 为什么使用SD实现字符串
# C语言字符串(使用字符数组char[]实现)
- 字符数组必须先给目标变量分配足够的空间,否则可能会溢出;
- 要获取字符串使用长度,必须遍历字符数组,时间复杂度是 O(n);
- 字符串长度的变更会对字符数组做内存重分配;
- 通过从字符串开始到结尾碰到的第一个'\0'来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。

# Redis字符串(内部实现为SDS)
- 不用担心内存溢出问题,如果需要会对 SDS 进行扩容;
- 获取字符串使用长度时间复杂度为 O(1),因为结构体sdshdr32定义了 len 属性;
- 通过“空间预分配” (sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存;
- 判断是否结束的标志是 len 属型(同样以'\0'结尾是因为这样就可以使用C语言中函数库操作字符串的函数);
C 字符串 Redis 字符串
获取字符串长度的复杂度为 O(N) 获取字符串长度的复杂度为 O(1)
API 是不安全的,可能会造成缓冲区溢出 API 是安全的 API安全,不会造成个缓冲区溢出
修改字符串长度 N 次必然需要执行 N 次内存重分配 修改字符串长度 N 次最多需要执行 N 次内存重分配
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数
  • embstr和raw区别
- embstr 使用只分配一次内存空间(因为 RedisObject 和 SDS 是连续的),而 raw需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间);
- embstr优点: 创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便;
- embstr缺点: 如果字符串的长度增加需要重新分配内存时,整个RedisObject 和 SDS 都需要重新分配空间,因此 Redis 中的 embstr实现为只读。
  • int和embstr如何转为raw
# 转换条件
int -> raw : 当int数据不再是整数,或大小超过了long的范围(2^63 - 1)时,自动转为raw;
embstr -> raw : 当empstr执行append操作时,自动转为raw。

# 演示过程
> set number 1
OK
> set embstr "this strtype is embstr" 
OK
> object encoding number 
"int"
> object encoding embstr 
"embstr"
> append number a
(integer) 2
> append embstr append
(integer) 28
> object encoding number 
"raw"
> object encoding embstr 
"raw"
  • 存储小于44个字节的字符串,为什么变成raw
# 演示过程
> set k2 a
OK
> OBJECT encoding k2
"embstr"
> append k2 b
(integer) 2
> OBJECT encoding k2
"raw"

# 结论
- 对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。
  • 当字符串长度小于44时,为什么不会恢复成embstr
关于 Redis 内部编码的转换,都符合以下规律:编码转换在 Redis 写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新 set)。
  • 为什么对底层的数据结构进行一层包装
通过封装,可以根据对象的类型动态地选择存储结构和可以使用的命令,实现节省空间和优化查询速度。
String - 应用场景
- 把字符串当作原子计数器使用,如用户访问次数、热点文章点赞、转发等;
- 热点数据缓存;
- 数据共享分布式;
- 分布式锁;
- 全局 ID;
- 限流;
- 位统计;
- 在小空间里编码大量数据;

哈希表 (Hash)

- Redis Hashes是字符串字段和字符串值之间的映射,所以它们是完美的表示对象(eg:一个有名,姓,年龄等属性的用户)的数据类型。

# 可使用命令
- HSET
- HSETNX
- HGET
- HEXISTS
- HDEL
- HLEN
- HSTRLEN
- HINCRBY
- HINCRBYFLOAT
- HMSET
- HMGET
- HKEYS
- HVALS
- HGETALL
- HSCAN
Hash - 使用
  • hset - hget
> HSET website google "www.g.cn"
(integer) 1
> HGET website google
"www.g.cn" 
  • hsetnx - hget
> HSETNX database key-value-store Redis
(integer) 1

> HGET database key-value-store
"Redis"
  • hmset - hmget
> hmset user:1000 username antirez birthyear 1977 verified 1
OK

> hget user:1000 username
"antirez"

> hget user:1000 birthyear
"1977"

> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"
  • 其它
# hkeys 返回哈希表 key 中的所有域
> HMSET website google www.google.com yahoo www.yahoo.com
OK

> HKEYS website
1) "google"
2) "yahoo"

# hexists 返回哈希表 key 中是否存在的Key
> hexists website google
(integer) 1
Hash - 原理

redis.conf

# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-ziplist-entries 512 /* ziplist 中最多能存放的 entry 节点数 */
hash-max-ziplist-value 64 /* ziplist 中最大能存放的值长度 */

t_hash.c

/* Check if the ziplist needs to be converted to a hash table */
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
  hashTypeConvert(o, OBJ_ENCODING_HT);

ziplist.c

/* The ziplist is a specially encoded dually linked list that is designed
 * to be very memory efficient. It stores both strings and integer values,
 * where integers are encoded as actual integers instead of a series of
 * characters. It allows push and pop operations on either side of the list
 * in O(1) time. However, because every operation requires a reallocation of
 * the memory used by the ziplist, the actual complexity is related to the
 * amount of memory used by the ziplist.
 */

/*
* The general layout of the ziplist is as follows
* <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
*/

typedef struct zlentry {
    /* 上一个链表节点占用的长度 */
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    
    /* 存储上一个链表节点的长度数值所需要的字节数 */
    unsigned int prevrawlen;     /* Previous entry len. */
    
    /* 存储上一个链表节点的长度数值所需要的字节数 */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    
    /* 当前链表节点占用的长度 */
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    /* 当前链表节点的头部大小(prevrawlensize + lensize),即非数据域的大小 */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    
    /* 编码方式 */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    
    /* 压缩链表以字符串的形式保存,该指针指向当前节点起始位置 */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

/* Different encoding/length possibilities */
#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30
#define ZIP_STR_06B (0 << 6) /* 长度小于等于 63 字节 */
#define ZIP_STR_14B (1 << 6) /* 长度小于等于 16383 字节 */
#define ZIP_STR_32B (2 << 6) /* 长度小于等于 4294967295 字 */
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe

dict.h

typedef struct dictEntry {
    void *key; /* key 关键字定义 */
    union {
        void *val; /* value 定义 */
        uint64_t u64;
        int64_t s64;
        double d;
    } 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. */
typedef struct dictht {
    dictEntry **table; /* 哈希表数组 */
    unsigned long size; /* 哈希表大小 */
    unsigned long sizemask; /* 掩码大小 用于计算索引值;总是等于size-1 */
    unsigned long used; /* 已有节点数 */
} dictht;

typedef struct dict {
    dictType *type; /* 字典类型 */
    void *privdata; /* 私有数据 */
    dictht ht[2];	/* 一个字典有两个哈希表 */
    
    /* rehash索引 */
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    
    /* 当前正在使用的迭代器数量 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

dict.c

/* 扩容判断 _dictExpandIfNeeded */
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) &&
        dictTypeExpandAllowed(d))
    {
        return dictExpand(d, d->ht[0].used + 1);
    }
    return DICT_OK;
}

/* 扩容方法  */
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
    if (malloc_failed) *malloc_failed = 0;

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);

    /* Detect overflows */
    if (realsize < size || realsize * sizeof(dictEntry*) < realsize)
        return DICT_ERR;

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    if (malloc_failed) {
        n.table = ztrycalloc(realsize*sizeof(dictEntry*));
        *malloc_failed = n.table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else
        n.table = zcalloc(realsize*sizeof(dictEntry*));

    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

server.c

/* 缩容 */
int htNeedsResize(dict *dict) {
    long long size, used;

    size = dictSlots(dict);
    used = dictSize(dict);
    return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));
}
  • 哈希表类型内部编码
# 哈希表类型内部编码
- ziplist: 压缩列表 OBJ_ENCODING_ZIPLIST,一个经过特殊编码的双向链表;
- hashtable(dict): 哈希表 OBJ_ENCODING_HT,称为字典(dictionary),它是一个数组+链表的结构。

# hash类型内部编码查看
> hset h1 k1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
> hset h2 k2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 0
> OBJECT encoding h1
"ziplist"
> OBJECT encoding h2
"hashtable"
Hash - 问题
  • 什么时候使用ziplist存储
# 满足以下条件时候使用ziplist存储
- 所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节);
- 哈希对象保存的键值对数量小于 512。

# 满足以下条件时侯使用hashtable存储
- 一个哈希对象超过配置的阈值(键和值的长度有>64byte,键值对个数>512 个)时,会转换成哈希表(hashtable)。

# hashtable数据
- dictEntry -> dictht -> dict -> OBJ_ENCODING_HT

- dictht 后面是 NULL 说明第二个 ht 还没用到;dictEntry后面是 NULL 说明没有 hash 到这个地址;dictEntry 后面是NULL 说明没有发生哈希冲突。

image-20211008192638266

  • 为什么要定义两个哈希表ht[2]
# dictht
- redis 的 hash 默认使用的是 ht[0],ht[1]不会初始化和分配空间;
- 哈希表 dictht 是用链地址法来解决碰撞问题的;在这种情况下,哈希表的性能取决于它的大小(size 属性)和它所保存的节点的数量(used 属性)之间的比率:
	- 比率在 1:1 时(一个哈希表 ht 只存储一个节点 entry),哈希表性能最好;
	- 如果节点数量比哈希表的大小要大很多(比例用ratio表示,5表示平均一个ht存储5个entry),哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在,这种情况下就需要扩容,Redis的这种操作叫做rehash。
# rehash步骤:
- 为字符 ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0]当前包含的键值对的数量;ht[1]的大小为第一个大于等于 ht[0].used*2;

- 将所有的 ht[0]上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放入指定的位置;
- 当 ht[0]全部迁移到了 ht[1]之后,释放 ht[0]的空间,将 ht[1]设置为 ht[0]表,并创建新的 ht[1],为下次 rehash 做准备。
  • 什么时候触发扩容
# 负载因子 dict.c
- static int dict_can_resize = 1;
- static unsigned int dict_force_resize_ratio = 5;

# 触发扩容条件
- radio = used/size,已使用节点与字典大小的比例;
- dict_can_resize 为 1 并且 dict_force_resize_ratio 已使用节点数和字典大小之间的比率超过 1:5,触发扩容。

Hash - 应用场景
- String可做的,Hash都可以;
- 存储对象类型的数据;
- 购物车;
- 可以在一个小型的 Redis实例中存储上百万的对象。

列表 (Lists)

Lists - 使用
- Redis列表是简单的字符串列表,按照插入顺序排序。 你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

- LPUSH 命令插入一个新元素到列表头部,而RPUSH命令 插入一个新元素到列表的尾部。当 对一个空key执行其中某个命令时,将会创建一个新表。 类似的,如果一个操作要清空列表,那么key会从对应的key空间删除。这是个非常便利的语义, 因为如果使用一个不存在的key作为参数,所有的列表命令都会像在对一个空表操作一样。

# 可使用命令
- LPUSH
- LPUSHX
- RPUSH
- RPUSHX
- LPOP
- RPOP
- RPOPLPUSH
- LREM
- LLEN
- LINDEX
- LINSERT
- LSET
- LRANGE
- LTRIM
- BLPOP
- BRPOP
- BRPOPLPUSH

image-20211008220129870

  • push - pop
# 将一个或多个值 value 插入到列表 key 的表头
# 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。
# 当 key 存在但不是列表类型时,返回一个错误。
> LPUSH languages python # 加入单个元素
(integer) 1
> LPUSH languages python # 加入重复元素
(integer) 2
> LRANGE languages 0 -1  # 列表允许重复元素
1) "python"
2) "python"
> LPUSH mylist a b c # 加入多个元素
(integer) 3
> LRANGE mylist 0 -1 
1) "c"
2) "b"
3) "a"
Lists - 原理
  • 链表类型内部编码
# 链表类型内部编码
- 3.2 版本前:
	- ziplist:
	- linkedlist:
- 3.2 版本后:
	- quicklist:  存储了一个双向链表,ziplist 和 linkedlist 的结合体;

# 链表类型内部编码查看
> LPUSH hello hello
(integer) 1
> LRANGE hello 0 -1
1) "hello"
> OBJECT encoding hello
"quicklist"

quicklist.h

typedef struct quicklist {
    quicklistNode *head; /* 指向双向列表的表头 */
    quicklistNode *tail; /* 指向双向列表的表尾 */
    
    /* 所有的 ziplist 中一共存了多少个元素 */
    unsigned long count;        /* total count of all entries in all ziplists */
    
    /* 双向链表的长度,node 的数量 */
    unsigned long len;          /* number of quicklistNodes */
    
    /* 填充因子 */
    int fill : QL_FILL_BITS;              /* fill factor for individual nodes */
    
    /*  压缩深度,0: 不压缩; */
    unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

typedef struct quicklistNode {
    struct quicklistNode *prev; /* 前一个节点 */
    struct quicklistNode *next; /* 后一个节点 */
    
    /* 指向实际的 ziplis */
    unsigned char *zl;
    
    /* 当前 ziplist 占用多少字节 */
    unsigned int sz;             /* ziplist size in bytes */
    
    /* 当前 ziplist 中存储了多少个元素,占 16bit(下同),最大65536个 */
    unsigned int count : 16;     /* count of items in ziplist */
    
    /* 是否采用了 LZF 压缩算法压缩节点;1:RAW 2:LZF */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    
    /* 2:ziplist,未来可能支持其他结构存储 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    
    /* 当前 ziplist 是不是已经被解压出来作临时使用 */
    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.conf

# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -5: max size: 64 Kb  <-- not recommended for normal workloads
# -4: max size: 32 Kb  <-- not recommended
# -3: max size: 16 Kb  <-- probably not recommended
# -2: max size: 8 Kb   <-- good
# -1: max size: 4 Kb   <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2

# Lists may also be compressed.
# Compress depth is the number of quicklist ziplist nodes from *each* side of
# the list to *exclude* from compression.  The head and tail of the list
# are always uncompressed for fast push/pop operations.  Settings are:
# 0: disable all list compression
# 1: depth 1 means "don't start compressing until after 1 node into the list,
#    going from either the head or tail"
#    So: [head]->node->node->...->node->[tail]
#    [head], [tail] will always be uncompressed; inner nodes will compress.
# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
#    2 here means: don't compress head or head->next or tail->prev or tail,
#    but compress all nodes between them.
# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
# etc.
list-compress-depth 0
Lists - 问题
Lists - 应用场景
- 发布订阅;
- 消息队列;
- 慢查询。

集合 (Sets)

Sets - 使用
- Redis集合是一个无序的字符串合集;你可以以O(1) 的时间复杂度(无论集合中有多少元素时间复杂度都为常量)完成 添加,删除以及测试元素是否存在的操作;

# 可使用命令
- SADD
- SISMEMBER
- SPOP
- SRANDMEMBER
- SREM
- SMOVE
- SCARD
- SMEMBERS
- SSCAN
- SINTER
- SINTERSTORE
- SUNION
- SUNIONSTORE
- SDIFF
- SDIFFSTORE
  • SADD - SMEMBERS
> SADD bbs "discuz.net" # 添加单个元素
(integer) 1
> SADD bbs "discuz.net" # 添加重复元素
(integer) 0
> SADD bbs "tianya.cn" "groups.google.com" # 添加多个元素
(integer) 2
> SMEMBERS bbs  # 获取所有元素
1) "discuz.net"
2) "groups.google.com"
3) "tianya.cn"
  • scard - sismember
> sadd myset a b c d e f g
(integer) 7
> smembers myset # 获取所有元素
1) "e"
2) "a"
3) "d"
4) "f"
5) "c"
6) "g"
7) "b"
> SISMEMBER myset a # 统计元素是否存在
(integer) 1
> SCARD myset # 统计元素个数
(integer) 7
  • spop - srem
> sadd myset a b c d e f g
(integer) 7
> spop myset # 随机弹出一个元素
"g"
> srem myset d e f # 移除一个或者多个元素
(integer) 3
> SMEMBERS myset # 查看元素是否存在
1) "a"
2) "c"
3) "b"
  • sdiff - sinter - sunion
# SDIFF
> sadd sa1 1 2 3 4 789 
(integer) 5
> sadd sa2 3 4 5 789
(integer) 3
> SDIFF sa1 sa2
1) "1"
2) "2"

# SINTER
> SINTER sa1 sa2
1) "3"
2) "4"
3) "789"

# SUNION
> sunion sa1 sa2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "789"
Sets - 原理
  • 集合类型内部编码
 # 集合类型内部编码
- intset: 若元素都是整数类型,就用 inset 存储;
- hashtable: 如果不是整数类型,就用 hashtable(数组+链表的存来储结构)。


# 集合类型内部编码查看
> sadd iset 1 2 3 4 5 6
(integer) 6
> object encoding iset
"intset"
> sadd myset a b c d e f 
(integer) 3
> OBJECT encoding myset
"hashtable"

redis.conf

# Sets have a special encoding in just one case: when a set is composed
# of just strings that happen to be integers in radix 10 in the range
# of 64 bit signed integers.
# The following configuration setting sets the limit in the size of the
# set in order to use this special memory saving encoding.
set-max-intset-entries 512
Sets - 问题
Sets - 应用场景
- 用集合跟踪一个独特的事;想要知道所有访问某个博客文章的独立IP?只要每次都用SADD来处理一个页面访问;那么你可以肯定重复的IP是不会插入的;
- 抽奖;
- 点赞、签到、打卡;
- 商品标签;
- 商品筛选;

有序集合 (Sorted sets)

Sorted Sets - 使用
- Redis有序集合和Redis集合类似,是不包含 相同字符串的合集。它们的差别是,每个有序集合 的成员都关联着一个评分,这个评分用于把有序集 合中的成员按最低分到最高分排列。
	
# 可使用命令
- ZADD
- ZSCORE
- ZINCRBY
- ZCARD
- ZCOUNT
- ZRANGE
- ZREVRANGE
- ZRANGEBYSCORE
- ZREVRANGEBYSCORE
- ZRANK
- ZREVRANK
- ZREM
- ZREMRANGEBYRANK
- ZREMRANGEBYSCORE
- ZRANGEBYLEX
- ZLEXCOUNT
- ZREMRANGEBYLEX
- ZSCAN
- ZUNIONSTORE
- ZINTERSTORE
  • zadd - zrange - zrevrange - zrangebyscore
> zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
(integer) 5
> ZRANGE myzset 0 -1 withscores	# 升序排序
 1) "java"
 2) "10"
 3) "php"
 4) "20"
 5) "ruby"
 6) "30"
 7) "cpp"
 8) "40"
 9) "python"
10) "50"

> ZREVRANGE myzset 0 -1 withscores # 降序排序
 1) "python"
 2) "50"
 3) "cpp"
 4) "40"
 5) "ruby"
 6) "30"
 7) "php"
 8) "20"
 9) "java"
10) "10"

> zrangebyscore myzset 20 30 # 根据分值区间获取元素
1) "php"
2) "ruby"
  • zrem
> zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
(integer) 5

> ZRANGE myzset 0 -1 withscores 
 1) "java"
 2) "10"
 3) "php"
 4) "20"
 5) "ruby"
 6) "30"
 7) "cpp"
 8) "40"
 9) "python"
10) "50"

> ZREM myzset php cpp # 删除php cpp
(integer) 2

> ZRANGE myzset 0 -1 withscores
1) "java"
2) "10"
3) "ruby"
4) "30"
5) "python"
6) "50"
  • 其它
# zcard
> zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
(integer) 2
> zcard myzset # 统计元素个数
(integer) 5

# 分值递增
> ZINCRBY myzset 5 python
"55"

# 根据分值统计个数
> ZCOUNT myzset 20 60
(integer) 4

# 获取元素 zrank 位置
> zrank myzset java
(integer) 0
> zrank myzset python
(integer) 4

# 获取元素 zscore
> ZSCORE myzset java
"10"
Sorted Sets - 原理
 # 有序集合类型内部编码
- ziplist: 元素数量小于128且所有member的长度都小于64字节时使用ziplist;在 ziplist 的内部,按照 score 排序递增来存储;插入的时候要移动之后的数据;
- skiplist+dict: 若不满足ziplist条件,则使用skiplist+dict存储。
- 跳跃表: https://baike.baidu.com/item/%E8%B7%B3%E8%A1%A8/22819833?fr=aladdin

redis.conf

# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

t_zset.c

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

server.h

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele; /*zset 的元素*/
    double score; /* 分值 */
    struct zskiplistNode *backward; /* 后退指针 */
    struct zskiplistLevel {
        struct zskiplistNode *forward; /* 前进指针 对应level的下一个节点 */
        unsigned long span; /* 从当前节点到下一个节点的跨度(跨越的节点数) */
    } level[]; /* 层 */
} zskiplistNode;

typedef struct zskiplist {
	struct zskiplistNode *header, *tail; /* 指向跳跃表的头结点和尾节点 */
	unsigned long length; /* 跳跃表的节点数 */
	int level; /* 最大的层数 */
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;
Sorted Sets - 问题
  • 为什么不使用AVL树或红黑树?
skiplist更加简洁
Sorted Sets - 应用场景
- 排行榜(直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息)。
posted @ 2021-11-30 23:49  HOsystem  阅读(156)  评论(0编辑  收藏  举报