zoukankan      html  css  js  c++  java
  • 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 - 应用场景
    - 排行榜(直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息)。
    
  • 相关阅读:
    常见银行编码收集
    kafka集群在消息消费出现无法找到topic分区的处理解决
    find命令通过排序只保留最新的文件目录
    Git fetch和git pull的区别
    git 常用命令
    wordpress模板修改及函数说明
    webbench进行压力测试
    git存储用户名与密码
    导出putty配置
    一个成功的Git分支模型
  • 原文地址:https://www.cnblogs.com/HOsystem/p/15391569.html
Copyright © 2011-2022 走看看