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 次内存重分配 只能保存文本数据 可以保存文本或者二进制数据 可以使用所有

  • 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
*      ...

*/

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<

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 (

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 说明没有发生哈希冲突。

Redis 基础
  • 为什么要定义两个哈希表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

Redis 基础
  • 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  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

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 – 应用场景
- 排行榜(直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息)。

Original: https://www.cnblogs.com/HOsystem/p/15391569.html
Author: HOsystem
Title: Redis 基础

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/606972/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Linux环境下,原根分区大小27G,新加入20G硬盘,想要合并到根分区

    ================①、查看磁盘结构 [root@localhost ~]# lsblkNAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTs…

    Linux 2023年6月7日
    0107
  • LeetCode-678. 有效的括号字符串

    题目来源 题目详情 给定一个只包含三种字符的字符串: &#xFF08;&#xA0;, &#xFF09; 和 *,写一个函数来检验这个字符串是否为有效字符串。…

    Linux 2023年6月7日
    093
  • CentOS——Redis远程连接可视化工具Rdis Desktop Manage

    前排提示 Centos没有安装Redis的可参考 https://www.cnblogs.com/tianhengblogs/p/15265028.html 一。 修改redis….

    Linux 2023年5月28日
    0153
  • 【Linux】指令学习

    Linux学习记录 😄生命不息,写作不止🏆 一个有梦有戏的人 @怒放吧德德🌝分享学习心得,欢迎指正,大家一起学习成长! 1、虚拟机网卡配置 服务器重启完成之后,我们可以通过linu…

    Linux 2023年6月6日
    0114
  • SSM 集成 Freemarker 模板引擎

    在前后端分离的大趋势下,项目开发过程中,应尽量减少前端和后台的依赖和耦合,前端和后台尽可能采用 ajax 进行交互;但是全站 ajax,不利于网站 SEO,所以引入模板引擎,尽量减…

    Linux 2023年6月14日
    085
  • FusionAccess桌面云安装(windows AD方法)

    创建FusionAccess虚拟机 选择自定义 默认兼容 选择稍后安装操作系统 选择Linux SUSE Linux 名字位置自己选择 选择最少4个处理器 选择最少8G内存 选择仅…

    Linux 2023年6月8日
    0101
  • 网络通信知识地图

    知识地图是一种知识导航系统,并显示不同的知识存储之间重要的动态联系。本篇主要就是从更高的视角将之前的文章的结构思路展现出来。文章结构的思路实际上也是达到架构师程度要掌握的网络通信知…

    Linux 2023年6月14日
    093
  • 项目开发流程与开发模式

    企业项目开发流程 商城 1.1 B2C 直销商城 商家与会员直接交易 ( Business To Customer ) 1.2 B2B 批发商城 商家与商家直接交易 1.3 B2B…

    Linux 2023年6月14日
    0111
  • 【原创】Linux虚拟化KVM-Qemu分析(一)

    背景 Read the fucking source code! –By 鲁迅 A picture is worth a thousand words. –…

    Linux 2023年6月8日
    087
  • 前端开发:如何正确地跨端

    导读:面对多种多样的跨端诉求,有哪些跨端方案?跨端的本质是什么?作为业务技术开发者,应该怎么做?本文分享阿里巴巴ICBU技术部在跨端开发上的一些思考,介绍了当前主流的跨端方案,以及…

    Linux 2023年6月8日
    069
  • Linux 搭建Apollo

    简介 Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用…

    Linux 2023年6月14日
    0105
  • Linux系统Oracle常见操作

    1.1 登录默认数据库 首先切换到oracle用户,用数据库默认管理员登录。 [root@tsm-zh01 ~]# su – oracle [oracle@redhat ~]$ l…

    Linux 2023年6月6日
    072
  • Java动态脚本Groovy,高级啊!

    前言:请各大网友尊重本人原创知识分享,谨记本人博客: 南国以南i 简介: Groovy是用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编…

    Linux 2023年6月14日
    0128
  • 【转】redis 消息队列发布订阅模式spring boot实现

    /*redis 消息处理器/ @Component public class MessageReceiver { /*接收消息的方法/ public void receiveMes…

    Linux 2023年5月28日
    091
  • 【建议收藏】你知道数据库是怎么运行的吗?

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Linux 2023年6月11日
    066
  • js打印前几天或后几天的日期

    创作对你我有价值的,喜欢交朋友,失忆王子,期待与你共同探讨,技术qq群153039807 Original: https://www.cnblogs.com/hshanghai/p…

    Linux 2023年6月13日
    094
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球