Redis命令查询
目录
String
List
Set
Hash
Sorted Set
String
String是Redis最基本的类型,可以理解成与Memcached一模一样的类型,一个key对应一个value。
String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。(二进制安全指只关心二进制化的字符串,不关心具体格式,不会对某种字符组合给予特殊含义,比如C语言的 \0 可作为字符串结尾标识,那么C语言就不是二进制安全的)为什么是二进制安全的?虽然SDS也是把'\0'作为字符串结尾,但是SDS通过len属性而不是'\0'来判断字符串的结尾。SDS依然用'\0'作为结尾只是为了兼容部分C字符串函数,通过遵循C字符串以空字符结尾的惯例,SDS可以在有需要时重用<string.h>函数库,从而避免了不必要的代码重复。
Redis存储图片的两种方式:
- 直接用String存,这样可以先把图片用base64编码,将编码结果作为value存储起来,get的时候对得到的value进行相应的base64解码即可。
- 将图片保存在本地(上云也可以),value直接存对应的路径即可。
- 将图片转为对象,通过对象的序列化与反序列化来存。
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
这里说一下Redis的Incr操作,这个操作可以对值+1,并且是原子操作。什么是原子操作?单线程中,单条指令可认为是原子操作,因为中断只发生于指令间;多线程中,不能被其它线程打断的操作就是原子操作。Redis因为是单线程的,所以这种单条指令都是原子操作。
那么Java中的i++是原子操作吗?不是。
i=0,两个线程同时执行i++100次,i的范围是多少?2~200
i++底层分为了三步:获取i值、+1操作、新值赋给i。(也就是说一共100轮,每轮分为3步操作)
现在有A、B两个线程,出现2的极端情况就是:
B线程先获取到了i的值是0,而A随后也获取到了i的值并执行了99次+1操作让i等于了99,此时B执行了1次+1操作让i等于1,此时A对i执行第100次+1操作,首先也是取i的值,结果取值为1,然后在A执行+1前B去对i执行完了100次+1,此时B最后也给i赋值为100,但是A又来最后一次对i+1,由于之前取的i是1,那么+1就是2,这次A对i进行赋值i=2把B的赋值给覆盖了,i最后就等于2.
200就是正常情况,一个线程处理完了另一个再处理。
String底层的数据结构是用的SDS(Simple Dynamic String),这是Redis自己封装的一个结构体,也就是大小可变的字符串,可以把它看做Java中的ArrayList,采用的预分配额外空间,扩容是当现在空间不够了就会新增len长度的空间,但是当大小len已大于1M时,扩容就只是增加1M的容量。
struct sdshdr {
// buf 已占用长度
int len;
// buf 剩余可用长度
int free;
// 实际保存字符串数据的地方
char buf[];
};
通过 len
属性, sdshdr
可以实现复杂度为 O(1)的计算长度操作(c语言不能直接得到元素个数)。
注意只有执行append之后才会创建额外1倍的空间,最开始的set后free为0
List
一个key对应的value可以有多个值,底层由双向链表实现,两端操作效率更高。
底层数据结构在值较少的情况下是zipList,也就是一块连续的内存空间,当值比较多了就会变为quickList,其实就是通过链表的方式将多个zipList链接起来。
使用zipList是为了避免为每个值都分配next、pre指针从而浪费空间,使用quickList则可以提高插入和删除的性能。(综合了链表和数组的优点)
Set
相比于List,Set可以实现自动去重并且还能提供判断一个元素是否在集合内的接口。
Set的底层在满足1. 集合元素全部是整型值;2. 集合元素个数不超过512 的情况下是intset
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
intset是有序数组(查找用的二分查找)
另外就是用的dict字典,也就是哈希表。在Java中HashSet的底层也是HashMap,不过所有value都指向一个空对象(一个空对象占16字节->8字节的mark word+8字节的class pointer),而Redis中的Set中的哈希表的value都指向null。
Hash
value就是键值对集合
相当于Java中的Map<String,Object>
应用场景比如存储用户数据,key是用户id,value可以是<"name","zhangsan">、<"age","14">...
好处是什么呢?我们想想如果不用这种方式要怎么实现,首先key还是用户id,但是value用String类型来存姓名、年龄这些数据,就要先序列化才能存,当我要修改的时候就需要先反序列化、改值、序列化,最后存进去,这样很麻烦并且不是原子操作还会引入并发修改问题。
底层数据结构是当<field,value>长度小,个数少时用zipList(键值对数量小于512,并且所有键值长度小于64字节),否则用HashTable。
Sorted Set
有序集合(Zset又称Sorted Set),和Set很相似都是字符串的去重集合,所不同的是,Zset的每个value都有一个对应的score值,用于按照从低到高的评分来对value排序,value不能重复但score可以重复。
底层数据结构用了两个,一个是hash,建立每对value和score的映射关系。
另一个是跳跃表。
有序集合的底层实现可以有很多,比如用数组,只是插入删除不方便,用红黑树虽然效率高但是太复杂,用链表在查询时需要遍历效率低,而跳跃表的效率和红黑树差不多但实现起来很简单。
如上图,第0层是原始链表,而第1、2层是索引链表(如果每个链表的数据域是很大的对象,那么索引链表只需要存关键值和几个指针,不需要存整个对象),空间复杂度没变的还是O(n),虽然空间复杂度没变但还是会多很多空间,为了节省空间我们可以多几个节点抽取为一个节点,比如隔3个或5个。这样查询的时间复杂度为O(logn)
注意,不同层的相同score节点用zipList存,比如上面3层的3个1号节点存在一个zipList中,查询时从上往下找,然后跳表每个节点是通过存指向score和value的指针找到对应的值,而非直接存值(只存指针的好处是链表节点不会太大)。