redis
remote dictionary server
- 数据存储形式 key-value
- redis是基于内存存储,性能快
- redis提供了多种持久化机制
- redis提供了主从,哨兵及集群的搭建方式,高可用
数据存储形式key-value
key
- key的话一般是string类型;空字符串也能作为key值
- key不宜过长,一是从存储角度来讲比较耗费内存,而且在查找key时对key进行比较将会增加开销。
- 推荐使用中划线或冒号将描述字段分隔,整体作为key,如:“student:id”、“student:age”。
- key的最大容量为512M。
value的类型
- String
平时存储对象都是转成json串以string的形式存储 - hash
hash的话就相当于map,redis的数据结构本身就类似一个map;常用于一对多的关系存储,如;购物车-商品的关系 - list
list是有序的集合 - set
无序的但是是唯一的,使用场景如:判断一段时间内,该操作是否是第一次进入,第一次进入有特殊处理的场景 - zset
有序且唯一的,使用场景:类似于朋友圈点赞,需要顺序展示,且一人只能显示一个;
String
- 最常见的就是用来存储对象序列化后的字符串
例:用于分布式存储session - 还可以存储数值,进行数值操作(incr\decr key、incrby\decyby key 值)
例:可以用来记录当前系统在线人数 - bitmap,不是一个实际的数据类型,只是对string进行bit运算
从【Redis基本数据类型String】这篇文章中了解到,redis中还可以对字符串进行位运算
setbit/getbit key [offset value]
bitcount key [start end]用来统计value中有多少位为“1”;
bitpos key bit [start end]用来查找指定字节范围中第一个bit位的索引;
bitop operation destkey key [key …] —(bitop and andkey k1 k2 、bitop or orkey k1 k2)
假设有以下需求:电信运营商给用户提供了掌上营业厅服务,现在局方想要统计每个用户在一年内的掌上营业厅登录情况,在哪些天登录了,一共有多少天登录?
方案一:使用数据库,记录每一个用户的登录情况。用户每登录一次,则增加一条记录,记录包含登录当天的日期。然后一年中一共有多少天登录通过SQL语句来查询。
问题:一个用户在一年中有多少天登录,就会产生多少条记录,这里还要注意到电信运营商的用户数基本是千万级的,这样这张表就会变的庞大无比,本方案不可行。
方案二:使用Redis的Bitmap来存储用户登录信息
一位就代表一天,1表示登录了,0表示没有登录,使用count就能获取到一年总的登录天数
后续的工作中可以以这个为例子,做一些功能的优化
list
列表(list)用于存储多个有序的字符串。可以充当栈和队列的角色
数据结构
一般有序会采用数组或者是双向链表,其中双向链表由于有前后指针实际上会很浪费内存。
3.2版本之前:
Redis 列表list使用两种数据结构作为底层实现
- 压缩列表ziplist(插入元素过多或字符串太大,就需要调用 realloc 扩展内存 )
- 双向链表linkedlist(需附加指针prev 和 next,较浪费空间,加重内存的碎片化 )
通过redis.conf配置完成
list-max-ziplist-value 64
#当元素个数 > 512个, 转变为 linkedlist
list-max-ziplist-entries 512
#当某个元素值 > 64字节,转变为 linkedlist
3.2版本之后:
Redis 列表使用quciklist (快速链表) 数据结构作为底层实现
双向链表linkedlist
优缺点:添加、删除元素快,但内存开销比较大!
每个节点上除了保存数据,还额外保存两个指针prev、next(16字节)
双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片
压缩列表 ziplist
由于ziplist是紧凑存储,如果插入的元素过大或过多,就会调用realloc扩展内存,realloc可能会重新分配内存空间,这时候就会导致大量数据的拷贝;
优缺点:存储在一段连续的内存上,查询快,存储效率高。不利于修改,插入和删除操作需要频繁的申请和释放内存。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝。
每一个存储节点(entry)都是一个zlentry (zip list entry)。
// 压缩列表节点
typedef struct zlentry {
// prevrawlen是前一个节点的长度,prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
unsigned int prevrawlensize, prevrawlen;
unsigned int lensize, len; // len为当前节点长度 lensize为编码len所需的字节大小
unsigned int headersize; // 当前节点的header大小
unsigned char encoding; // 节点的编码方式
unsigned char *p; // 指向节点的指针
} zlentry;
可见,每一个存储节点 zlentry,都包含
- prevrawlen 前一个 entry的长度
- lensize 当前entry的长度
▶ 如何通过一个节点向前跳转到另一个节点?
指向当前节点的指针e - 前一个entry的长度 = 指向前一个节点的地址 p
▶ 完整的zlentry由以下3各部分组成:
- prevrawlen:记录前一个 entry的长度,通过该值可计算出前一个节点的地址
- len/encoding:记录当前节点content占有的内存字节数及其存储类型,用来解析content用
- content:保存了当前节点的值。
▶ prevrawlen是变长编码,有两种表示方法:
- 前一节点的长度 < 254字节,则使用1字节来存储 prevrawlen;
- 前一节点的长度 >= 254字节,第 1 个字节的值设为 254 ,剩下4字节保存实际长度(5字节)
这时候就会考虑到ziplist会存在连锁更新问题:
每个entry都存储着前一个节点所占的字节数,这个数值又是变长编码的,由此可知:
假设:一个压缩列表e1、e2、e3、e4……,其中e1节点= 253字节,则e2.prevrawlen = 1字节 如果:此时在e2与e1之间插入一个新节点a,a长度= 254字节,则e2.prevrawlen需扩充到5字节;
以此类推,如果e2的长度变化又引起了e3.prevrawlen的长度变化,则e3也需扩充,直到表尾节点或某一节点的prevrawlen本身长度可容纳前一个节点的变化。每次扩充都需进行空间再分配操作。删除节点也是,一旦引起了节点的prevrawlen的变化,都可能产生连锁更新反应!
快速列表quicklist
hash
- Redis 中每个 hash 可以存储 2^32 - 1 键值对(40多亿)
- hash的话就相当于map,redis的数据结构本身就类似一个map;常用于一对多的关系存储,如;购物车-商品的关系;
- 我们经常把对象序列化成json串,然后以string的形式存储在redis中,如果在一个对象其他值都不发生变化,只有某两个属性需要经常性变动的时候,也可以使用hash的形式,将属性做key,以hash的形式存储
- 其实hash也有string中的数值运算等操作,hincr
数据结构:
底层使用两种数据结构存储:
- ziplist 压缩列表
- hashtable 普通的哈希表(key为set的值,value为null)
当使用ziplist编码必须满足下面两个条件,否则使用hashtable
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对数量小于512个
类似于list数据类型
set
Set 集合用于存储无序且不重复的元素。
Set 重要的特性:即在服务器端完成多个Sets之间的聚合计算操作,如unions、intersections和differences。由于这些操作均在服务端完成,因此效率极高,而且也节省了大量的网络IO开销。
数据结构
底层使用两种数据结构存储:
- inset 可理解为数组
- hashtable 普通的哈希表(key为set的值,value为null)
使用intset存储必须满足下面两个条件,否则使用hashtable,条件如下:
- 对象保存的所有元素都是整数值(int)
- 对象保存的元素数量不超过512个
set-max-inset-entries 512
常用指令
应用范围
- 文章浏览统计;利用set保存唯一性数据;同一个IP地址访问当前页面不断刷新只记录一次有效浏览数,仅需在每次访问该文章时将访问者的IP存入Redis中
- 好友推荐;可以通过Set类型的数据,做交集、并集、差集等方式,得到好友之间的共同关注
除了值的唯一性之外,他可以有获取并、交、差集的指令
zset
Zset 是Set的一个升级版本,他在set的基础上增加了一个 顺序属性,每个member成员都带有一个score分数( redis通过分数进行集合内成员的排序)。
数据结构
底层使用两种数据结构存储:
- ziplist 压缩列表
- skiplist 跳跃表(简称跳表)+ dict 字典
当使用ziplist编码必须满足下面两个条件,否则使用跳表
- 有序集合保存的元素数量 < 128个
- 有序集合保存的所有元素的长度 < 64字节
对应redis中的配置如下
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
跳表
所谓跳表,就是在有序链表的基础上优化查询
优化方式有点类似于二分查找,比如上图要查10应该放在哪里,会先判断19和10的大小,10比19小;这时判断10和7的大小,10比7大;这时判断10和11的大小,这时候,直接把10放到7和11之间;
可见上图是3个层级,那么10放在7和11之间一定就是放在最下层吗?
其实不是的,跳表中的实现,会随机一个层级,比如随机成2,那么就会和7处于一样的层级;
计算随机层数是一个很关键的过程,对skiplist的统计特性有着很重要的影响。这并不是一个普通的服从均匀分布的随机数,而是服从一定规则的:
- 首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。
- 如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p。
- 节点最大的层数不允许超过一个最大值,记为MaxLevel(Redis里是32)。
#define ZSKIPLIST_MAXLEVEL 32
#define ZSKIPLIST_P 0.25
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}