登录redis客户端
[root@tspvggdb01-test1-rgtj1-tj1 bin]# ./redis-cli
127.0.0.1:6379> auth 123456
查看redis数据类型
127.0.0.1:6379> type name
查看redis底层数据类型:
127.0.0.1:6379> object encoding name
产看list数据结构提供的API
127.0.0.1:6379> help @list
一、string类型
数据结构:
在 Redis 3.2 版本以前,SDS 的结构如下:
struct sdshdr {
unsigned int len; //len 4字节,表示 buf 中已占用的字节数
unsigned int free;//free 4字节,表示 buf 中剩余可用字节数。
char buf[]; //buf 表示数据空间,用于存储字符串
};
4 字节的 len,可表示的字符串长度为 2^32,而在实际应用中,存放于 Redis 中的字符串往往没有这么长;在 Redis 3.2 版本之后,Redis 将 SDS 划分为 5 种类型: vim sds.h
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已经使用长度8位 */
uint8_t alloc; /*总长度8位 excluding the header and null terminator */
unsigned char flags; /*前3位类型,后五位预留位 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_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 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
编码格式:
如果value是一个字符类型的数据,当长度小于45的时候编码格式是embstr,大于等45的时候编码格式是raw
如果value是数值类型,当机器是64为操作系统,他的默认大小是一个64位的有符号数,如果数据长度小于20时编码格式是int,长度大于20时使用编码格式embstr
总结:对于string类型
- 如果设置的值是一个可以转为long(整型)的数据,且长度小于20,redis会采用int类型的编码格式
- 如果设置的值就是一个普通的字符串,当长度小于45的时候编码格式是embstr,长度大于等45的时候编码格式是raw
embstr编码:(长度小于44),embstr的创建只需分配一次内存,embstr的objet和sds放在一起,更好地利用缓存带来的优势
raw编码:(长度大于44),raw创建为两次一次为sds分配对象,另一次为objet分配对象redisObject内存不在连续,采用指针的形式,实现连接
字符串的扩容:当前字符串实际分配的空间,一般是要高于实际字符串长度的len。当字符串长度小于1M的时候,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩容1M的空间,需要注意的是字符串的最大长度为512M。
字符串的应用:
- string串操作数值类型:可以利用reids的 incr、incrby、decr、decrby 等api做计数、统计功能,如秒杀存库存,统计点赞量
- string可以操作字符串:用来做session共享,key-velue的缓存等
- string可以操作bitmap:可以来用很小的空间,处理较大的数据;例如:统计上亿用户某天的登录情况,等等
二、List类型
list的特点:
- 双向链表,便于头尾操作数据时间复杂度0(1),对于中间位置数据的操作时间复杂度0(n)
- 按照数据的加入顺序
list的底层结构:quickList
- quickList是一个以zipList类型为节点的双向链表,即每个节点是一个zipList类型;
- ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。实际上,ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。另外,ziplist为了在细节上节省内存,对于值的存储采用了变长的编码方式,大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。
- quickList与zipList的关系,比如,一个包含3个节点的quicklist,如果每个节点的ziplist又包含4个数据项,那么对外表现上,这个list就总共包含12个数据项。
redis为什么要设计quicklist的结构:
- 双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
- ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
- redis就是综合了这两种结构的特点,设计了quickList
如何合理的调整ziplist的大小?
- 每个quicklist节点上的ziplist越短,则内存碎片越多。内存碎片多了,有可能在内存中产生很多无法被利用的小碎片,从而降低存储效率。这种情况的极端是每个quicklist节点上的ziplist只包含一个数据项,这就蜕化成一个普通的双向链表了。
- 每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大。有可能出现内存里有很多小块的空闲空间(它们加起来很多),但却找不到一块足够大的空闲空间分配给ziplist的情况。这同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实蜕化成一个ziplist了。通过调整list-max-ziplist-size可以控制每个ziplist的大小
优化列表:
- list-max-ziplist-size 表示每个ziplist的字节大小。默认为-2 表示8KB
- list-compress-depth 表示一个quicklist两端不被压缩的节点个数。默认是0 表示不压缩;2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
list的使用:
redis 做消息队列的缺点:
- 消息已经消费,就会在服务端删除,如果消费端失败,就会丢失
- 做消费者确认ACK麻烦
- 队列不去重(不能严格意义说是缺点)
三、hash结构
redis的hash数据类型的底层存储使用ziplist(压缩列表)和hashtable。当hash对象可以同时满足以下两个条件时,使用的是ziplist
- hash-max-ziplist-entries 512 当hash中的数据项的数目超过512的时候,也就是ziplist数据项超过1024的时候。
- hash-max-ziplist-value 64 当hash中插入的任意一个value的长度超过了64的时候
先修改配置文件 hash-max-ziplist-entries=5
当 key的数据条目大于5时数据类型转为hashtable
ziplist转hashtable:当hash转为hashtable时会占用较大的内存,并且在hashtable扩容时也会影响一定的性能
redis中的set与hash:
set
以普通的key-value
键值对的方式存储,可以设置过期时间,时间复杂度为O(1)
,每执行一个set
就会在Redis中多出一个key
。hset
是以哈希散列表的形式存储,超时时间只能设置在键key
上,单个域field
不能设置过期时间。时间复杂度为O(n)
,n
是单个哈希上的field
域个数。所以,单个哈希并不适合存储大量的字段field
,过多的字段field
会比较消耗CPU。但优点在于散列表存储会比较节省内存。通过 hash-max-ziplist-entries 可以控制ziplist上每个field于的个数- 由此我们知道为什么对于同一个对象结构的数据建议使用hash而不是set 多个属性
四、set结构
set的特点:无序的、自动去重的集合数据类型,并且可以处理多个集合的交集、并集、差集,Set底层用两种数据结构存储,一个是hashtable,一个是inset。
set使用inset作为存储结构的条件:
- 结合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512个(默认值)(set-max-intset-entries 512)
总结:Redis中set数据类型当存储整数集合并且数据量较小的情况下Redis会使用intset作为set的底层实现;当数据量较大或者集合元素为字符串时则会使用dict(hashtable)实现set。
intset:intset底层是一个整数类型的数组,intset将整数元素按顺序存储在数组里,并通过二分法降低查找元素的时间复杂度。数据量大时,依赖于“查找”的命令(如SISMEMBER)就会由于O(logn)的时间复杂度而遇到一定的瓶颈,所以数据量大时会用dict(hashtable)来代替intset。但是intset的优势就在于比dict更省内存,而且数据量小的时候O(logn)未必会慢于O(1)的hash function。
set中的hashtable:就是key作为set的值,value=null,类似java中的hashset
set的应用场景:
1. 好友/关注/粉丝/感兴趣的人集合
- sinter命令可以获得A和B两个用户的共同好友
- sismember命令可以判断A是否是B的好友
- scard命令可以获取好友数量
- 关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合
- 在实战中使用Redis Cluster时,sinter,smove这两个个命令其实是不适合作用于两个不同用户对应的集合的,因为在sinter,smove 操作的集合需要都在同一个slot(槽位)中
2、随机抽取(抽奖):
点击参与抽奖加入集合
SADD key {userID}
查看参与抽奖所有用户
SMEMBERS key
抽取count名中奖者
SRANGEMEMBER key [count] #不会从原集合中删除元素,适合一次性抽取所有中奖用户
STOP key [count] #会从原集合中删除元素,适合分批抽取中奖用户
3、黑名单/白名单:经常有业务出于安全性方面的考虑,需要设置用户黑名单、ip黑名单、设备黑名单等,set类型适合存储这些黑名单数据,sismember命令可用于判断用户、ip、设备是否处于黑名单之中
五、zset类型
zset 有序(默认字典顺序也可以设置评分,按照评分排序) 、无重复元素;zset底层的存储结构包括ziplist或skiplist,在同时满足以下两个条件的时候使用ziplist,其他时候使用skiplist:
- zset-max-ziplist-entries 5 有序集合保存的元素数量小于128个(默认值)
- zset-max-ziplist-value 64 有序集合保存的所有元素的长度小于64字节
ziplist:ziplist
编码的有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。