Redis|Redis 基础


目录

  • Redis 基础
    • Redis 定位 - 特性
      • 关系型数据库 特性
      • 非关系型数据库 特性
      • Redis 特性
    • Redis 安装 - 启动 - 使用
      • Redis 安装
      • Redis 启动
      • Redis 使用
    • Redis 数据类型
      • 字符串 (String)
        • String - 使用
        • String - 原理
        • String - 问题
        • String - 应用场景
      • 哈希表 (Hash)
        • Hash - 使用
        • Hash - 原理
        • Hash - 问题
        • Hash - 应用场景
      • 列表 (Lists)
        • Lists - 使用
        • Lists - 原理
        • Lists - 问题
        • Lists - 应用场景
      • 集合 (Sets)
        • Sets - 使用
        • Sets - 原理
        • Sets - 问题
        • Sets - 应用场景
      • 有序集合 (Sorted sets)
        • Sorted Sets - 使用
        • Sorted Sets - 原理
        • Sorted Sets - 问题
        • Sorted Sets - 应用场景

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


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


  • 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

【Redis|Redis 基础】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 - 应用场景
- 排行榜(直播间在线?户列表,各种礼物排?榜,弹幕消息)。

    推荐阅读