schema 所对应的db仅以编号区分。同一个db 内,key 作为顶层模型,它的值是扁平化的。也就是说db 就是key的命名空间
key的定义通常以“:” 分隔,如:Article:Count:1
我们常用的Redis数据类型有:string、list、set、map、sorted-set
Redis中的所有value 都是以object 的形式存在的,其通用结构如下
typedef struct redisObject {
unsigned [type] 4;
unsigned [encoding] 4;
unsigned [lru] REDIS_LRU_BITS;
int refcount;
void *ptr;
} robj;
纯内存
非阻塞IO
一次只运行一条命令
拒绝长(慢)命令
keys, flushall, flushdb, slow lua script, mutil/exec, operate big value(collection)
其实不是单线程
fysnc file descriptor
close file descriptor
Redis中的string 可表示很多语义
这三种类型,redis会根据具体的场景完成自动转换,并且根据需要选取底层的承载方式
例如整数可以由32-bit/64-bit、有符号/无符号承载,以适应不同场景对值域的要求
在Redis内部,string的内部以 int、SDS(简单动态字符串 simple dynamic string)作为存储结构
typedef struct sdshdr {
// buf中已经占用的字符长度
unsigned int len;
// buf中剩余可用的字符长度
unsigned int free;
// 数据空间
char buf[];
}
当对SDS 进行操作时,如果超出了容量。SDS会对其进行扩容,触发条件如下:
SDS中存储的内容可以是ASCII 字符串,也可以是字节串
由于SDS通过len 字段来确定业务串的长度,因此业务串可以存储非文本内容
对于字符串的场景,buf[len] 作为业务串结尾的’\0’ 又可以复用C的已有字符串函数
value 在内存中有2个部分:redisObject和ptr 指向的字节串部分。在创建时,通常要分别为2个部分申请内存,但是对于小字节串,可以一次性申请。
public VideoInfo get(Long id) {
String redisKey = redisPrefix + id;
VideoInfo videoInfo e redis.get(redisKey);
if (videoInfo == null) {
videoInfo = mysql.get(id);
if (videoInfo != null) {
// 序列化
redis.set(redisKey serialize(videoInfo)):
}
}
}
除此之外,string 类型的value还有一些CAS的原子操作,如:get、set、set value nx(如果不存在就设置)、set value xx(如果存在就设置)。
String 类型是二进制安全的,也就是说在Redis中String类型可以包含各种数据,比如一张JPEG图片或者是一个序列化的Ruby对象。一个String类型的值最大长度可以是512M。
在Redis中String有很多有趣的用法
可从头部(左侧)加入元素,也可以从尾部(右侧)加入元素。
有序列表。
微博粉丝即可以list结构存放在redis做缓存。
key = 某大v
value = [zhangsan, lisi, wangwu]
还可存储一些list型的数据结构,类似粉丝列表、文章的评论列表之类。
可通过lrange命令,即从某元素开始读取多少元素,可基于list实现分页查询,这就是基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
搞个简单的消息队列,从list头推进去,从list尾拉出来。
List类型中存储一系列String值,这些String按照插入的顺序排序。
List 类型的 value对象,由 linkedlist 或者 ziplist 实现。
当 List 元素个数少并且元素内容长度不大
采用ziplist 实现,否则使用linkedlist
链表的代码结构
typedef struct list {
// 头结点
listNode *head;
// 尾节点
listNode *tail;
// 节点值复制函数
void *(*dup)(void * ptr);
// 节点值释放函数
void *(*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
// 链表长度
unsigned long len;
} list;
// Node节点结构
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
从上面的结构可以看出,对于linkedlist和 ziplist,它们的rpush、rpop、llen的时间复杂度都是O(1)
但是对于ziplist,lpush、lpop都会牵扯到所有数据的移动,时间复杂度为O(N)
但是由于List的元素少,体积小,这种情况还是可控的
对于ziplist 的每个Entry 其结构如下
记录前一个相邻的Entry的长度,作用是方便进行双向遍历,类似于linkedlist 的prev 指针
ziplist是连续存储,指针由偏移量来承载
Redis中实现了2种方式的实现
这里面会有个问题,就是当前一个Entry的长度变化时,这时候有可能会造成后续的所有空间移动。当然这种情况发生的可能性比较小。
Entry内容本身是自描述的,意味着第二部分(Entry内容)包含了几个信息:Entry内容类型、长度和内容本身。而内容本身包含:类型长度部分和内容本身部分。类型和长度同样采用变长编码:
LPUSH 命令是在头部加入一个新元素,RPUSH 命令是在尾部加入一个新元素。当在一个空的键值(key)上执行这些操作时会创建一个新的列表。类似的,当一个操作清空了一个list时,这个list对应的key会被删除。这非常好理解,因为从命令的名字就可以看出这个命令是做什么操作的。如果使用一个不存在的key调用的话就会使用一个空的list。
一些例子:
LPUSH mylist a # 现在list是 "a"
LPUSH mylist b # 现在list是"b","a"
RPUSH mylist c # 现在list是 "b","a","c" (注意这次使用的是 RPUSH)
list的最大长度是2^32 – 1个元素(4294967295,一个list中可以有多达40多亿个元素)
从时间复杂度的角度来看,Redis list类型的最大特性是:即使是在list的头端或者尾端做百万次的插入和删除操作,也能保持稳定的很少的时间消耗。在list的两端访问元素是非常快的,但是如果要访问一个很大的list中的中间部分的元素就会比较慢了,时间复杂度是O(N)
类似List,但无序且其元素不重复。
向集合中添加多次相同的元素,集合中只存在一个该元素。在实际应用中,这意味着在添加一个元素前不需要先检查元素是否存在。
支持多个服务器端命令来从现有集合开始计算集合,所以执行集合的交集,并集,差集都可以很快
set的最大长度是2^32 – 1
个元素(一个set中可多达40多亿个元素)
Set在Redis中以intset 或 hashtable存储。
因为元素有序排列,所以SET的获取操作采用二分查找的方式,复杂度为O(log(N))
进行插入操作时,首先通过二分查找到要插入的位置,再对元素进行扩容
然后将插入位置之后的所有元素向后移动一个位置,最后插入元素。时间复杂度为O(N)。
为了使二分查找的速度足够快,存储在content 中的元素是定长的。
当插入2018 时,所有的元素向后移动,并且不会发生覆盖的情况
并且当Set 中存放的整型元素集中在小整数范围[-128, 127]内时,可以大大的节省内存空间。这里面需要注意的是:IntSet支持升级,但是不支持降级。
无序集合,自动去重,其实数据太多时不太推荐使用。
直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上呢?
得基于redis进行全局的set去重
可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧
把两个大v的粉丝都放在两个set中,对两个set做交集。但是全局这种计算开销也大。
记录唯一的事物
比如,你想知道访问某个博客的IP地址,不要重复的IP,这种情况只需要在每次处理一个请求时简单的使用SADD命令就可以了,可以确信不会插入重复的IP
表示关系
你可以使用Redis创建一个标签系统,每个标签使用一个Set来表示。然后你可以使用SADD命令把具有特定标签的所有对象的所有ID放在表示这个标签的Set中
如果你想要知道同时拥有三个不同标签的对象,那么使用SINTER命令就好了
可以使用SPOP 或者 SRANDMEMBER 命令从集合中随机的提取元素。
一般可将结构化的数据,比如一个对象(前提是这个对象未嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,即可直接操作hash里的某个字段。
key=150
value={
“id”: 150,
“name”: “zhangsan”,
“age”: 20
}
hash类的数据结构,主要存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,可直接仅修改该对象中的某字段的值。
value={
“id”: 150,
“name”: “zhangsan”,
“age”: 21
}
因为Redis本身是一个K.V存储结构,Hash类型的结构可以理解为subkey - subvalue
这里面的subkey - subvalue只能是
因为Map的 value 可表示整型和浮点型,因此Map也可以使用hincrby
对某个field的value值做自增操作。
hash有HashTable 和 ziplist 两种实现。对于数据量较小的hash,使用ziplist 实现
HashTable在Redis 中分为3 层,自底向上分别是:
dict是Hash表存储的顶层结构
// 哈希表(字典)数据结构,Redis 的所有键值对都会存储在这里。其中包含两个哈希表。
typedef struct dict {
// 哈希表的类型,包括哈希函数,比较函数,键值的内存释放函数
dictType *type;
// 存储一些额外的数据
void *privdata;
// 两个哈希表
dictht ht[2];
// 哈希表重置下标,指定的是哈希数组的数组下标
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 绑定到哈希表的迭代器个数
int iterators; /* number of iterators currently running */
} dict;
Hash表的核心结构是dictht,它的table 字段维护着 Hash 桶,桶(bucket)是一个数组,数组的元素指向桶中的第一个元素(dictEntry)。
typedef struct dictht {
//槽位数组
dictEntry **table;
//槽位数组长度
unsigned long size;
//用于计算索引的掩码
unsigned long sizemask;
//真正存储的键值对数量
unsigned long used;
} dictht;
从上图可以看出,Hash表使用的是 链地址法 解决Hash冲突
当一个bucket 中的Entry 很多时,Hash表的插入性能会下降,此时就需要增加bucket的个数来减少Hash 冲突
和大多数Hash表实现一样,Redis引入负载因子判定是否需要增加bucket个数
负载因子 = Hash表中已有元素 / bucket数量
扩容之后bucket的数量是原先的2倍
目前有2 个阀值:
小于1 时一定不扩容
大于5 时一定扩容
在1 ~ 5 之间时,Redis 如果没有进行bgsave/bdrewrite
操作时则会扩容
当key - value 对减少时,低于0.1时会进行缩容。缩容之后,bucket的个数是原先的0.5倍
这里面的ziplist 和List的ziplist实现类似,都是通过Entry 存放element
和List不同的是,Map对应的ziplist 的Entry 个数总是2的整数倍,第奇数个Entry 存放key,下一个相邻的Entry存放value
ziplist承载时,Map的大多数操作不再是O(1)了,而是由Hash表遍历,变成了链表的遍历,复杂度变为O(N)
由于Map相对较小时采用ziplist,采用Hash表时计算hash值的开销较大,因此综合起来ziplist的性能相对好一些
Redis Hashes 保存String域和String值之间的映射,所以它们是用来表示对象的绝佳数据类型(比如一个有着用户名,密码等属性的User对象)
| `1` | `@cli` |
| `2` | `HMSET user:1000 username antirez password P1pp0 age 34` |
| `3` | `HGETALL user:1000` |
| `4` | `HSET user:1000 password 12345` |
| `5` | `HGETALL user:1000` |
一个有着少量数据域(这里的少量大概100上下)的hash,其存储方式占用很小的空间,所以在一个小的Redis实例中就可以存储上百万的这种对象
Hash的最大长度是2^32 – 1个域值对(4294967295,一个Hash中可以有多达40多亿个域值对)
去重但可排序,写进去的时候给一个分数,有了这个分数可以自定义排序规则。
想根据时间对数据排序,则写时可用时间戳作为分数,自动按时间排序。
排行榜:将每个用户以及其对应的什么分数写入进去
zadd board score username
接着
zrevrange board 0 99
就可以获取排名前100的用户
zrank board username
可以看到用户在排行榜里的排名
zadd board 85 zhangsan
zadd board 72 wangwu
zadd board 96 lisi
zadd board 62 zhaoliu
96 lisi
85 zhangsan
72 wangwu
62 zhaoliu
zrevrange board 0 3
获取排名前3的用户
96 lisi
85 zhangsan
72 wangwu
zrank board zhaoliu
4
类似于Map的key-value对,但有序
由于Sorted-Set 本身包含排序信息,在普通Set 的基础上,Sorted-Set 新增了一系列和排序相关的操作:
Sorted-Set类型的valueObject 内部结构有两种:
对于更通用的场景,Sorted-Set使用sliplist来实现。
和通用的跳表不同的是,Redis为每个level 对象增加了span 字段,表示该level 指向的forward节点和当前节点的距离,使得getByRank类的操作效率提升
需要修改的箭头和对skip进行查找操作遍历并废弃过的路径是吻合的。span修改仅是+1或-1。
zskiplist 的查找平均时间复杂度 O(Log(N)),因此add / remove的复杂度也是O(Log(N))。因此Redis中新增的span 提升了获取rank(排序)操作的性能,仅需对遍历路径相加即可(矢量相加)。
还有一点需要注意的是,每个skiplist的节点level 大小都是随机生成的(1-32之间)。
zskiplist 是zset 实现顺序相关操作比较高效的数据结构,但是对于简单的zscore操作效率并不高。Redis在实现时,同时使用了Hashtable和skiplist,代码结构如下:
Hash表的存在使得Sorted-Set中的Map相关操作复杂度由O(N)变为O(1)。
Redis有序集合类型与Redis的集合类型类似,是非重复的String元素的集合。不同之处在于,有序集合中的每个成员都关联一个Score,Score是在排序时候使用的,按照Score的值从小到大进行排序。集合中每个元素是唯一的,但Score有可能重复。
使用有序集合可以很高效的进行,添加,移除,更新元素的操作(时间消耗与元素个数的对数成比例)。由于元素在集合中的位置是有序的,使用get ranges by score或者by rank(位置)来顺序获取或者随机读取效率都很高。(本句不确定,未完全理解原文意思,是根据自己对Redis的浅显理解进行的翻译)访问有序集合中间部分的元素也非常快,所以可以把有序集合当做一个不允许重复元素的智能列表,你可以快速访问需要的一切:获取有序元素,快速存在测试,快速访问中间的元素等等。
简短来说,使用有序集合可以实现很多高性能的工作,这一点在其他数据库是很难实现的。
使用有序集合你可以:
在大型在线游戏中创建一个排行榜,每次有新的成绩提交,使用ZADD命令加入到有序集合中。可以使用ZRANGE命令轻松获得成绩名列前茅的玩家,你也可以使用ZRANK根据一个用户名获得该用户的分数排名。把ZRANK 和 ZRANGE结合使用你可以获得与某个指定用户分数接近的其他用户。这些操作都很高效。
有序集合经常被用来索引存储在Redis中的数据。比如,如果你有很多用户,用Hash来表示,可以使用有序集合来为这些用户创建索引,使用年龄作为Score,使用用户的ID作为Value,这样的话使用ZRANGEBYSCORE 命令可以轻松和快速的获得某一年龄段的用户。
有序集合可能是Redis中最高级的数据类型了,所以请花一些时间查看一下 有序集合命令列表 来获得更多信息,同时你可能也想阅读Redis数据类型介绍
位图类型,String类型上的一组面向bit操作的集合。由于 strings是二进制安全的blob,并且它们的最大长度是512m,所以bitmaps能最大设置 2^32个不同的bit。
pfadd/pfcount/pfmerge。
在redis的实现中,使用标准错误小于1%的估计度量结束。这个算法的神奇在于不再需要与需要统计的项相对应的内存,取而代之,使用的内存一直恒定不变。最坏的情况 下只需要12k,就可以计算接近2^64个不同元素的基数。
geoadd/geohash/geopos/geodist/georadius/georadiusbymember
Redis的GEO特性在 Redis3.2版本中推出,这个功能可以将用户给定的地理位置(经、纬度)信息储存起来,并对这些信息进行操作。