(一)hash哈希
我们知道redis中的数据都是以键值对的方式存储的,key全部都是string类型,而value可以是不同的数据结构,其中就包括hash,也就是说,key这一层组织完成后到了value仍然是hash
1.Hash的一些命令
1)hset
设置hash中指定的字段(filed)和值value
我们首先要确定一个key,然后在这个key的value中,再添加一个hash结构
同时我们可以使用这个命令,同时添加多个key对应的哈希结构value
时间复杂度是O(1)如果插入多组就是O(n)
2)hget
获取hash中指定字段的值
我们需要给定一个key,然后再给定一个key中保存的filed
时间复杂度是O(1),如果我们查询的字段不存在就会返回nil
3)hexists
判断hash中是否有对应的field
返回值如果是1就表示存在,如果是0就表示不存在
时间复杂度也是O(1)
4) hdel
删除hash中指定的field,我们要注意,del是删除的key,而hdel是删除的field
我们可以通过这个命令,一次性删除多个field
时间复杂度:删除一个元素为O(1),n个元素为O(n)
5)hkeys
获取hash中的所有field
时间复杂度为O(n) ,N为field的个数
6)hvals
获取hash中的所有value
获取hash中的所有值
时间复杂度为O(n),N为field的个数
7)hgetall
获取hash中的field和value
时间复杂度O(n),N为field的个数
8)hmget
一次获取多个field中的value,我们刚刚的hget一次只能获取一个,而且之前说过,为了保证效率,我们要尽量减少网络的开销,所以我们可以使用hmget来一次获取多个value
这里其实还有hmset,可以一次性放入多个field和value,但是hset本身支持这个功能,所以这里我们不做说明
时间复杂度O(n),N为field的个数
9)hlen
获取hash中所有field的个数
时间复杂度O(1)
返回值为field的个数
10)hsetnx
在field不存在情况下设置hash中的field和value,如果存在就会失败,与setnx类似
时间复杂度为O(1),0表示失败,1表示成功
11)hincrby
用来使field对应的value进行+n的操作,我们都知道hash用来存键值对结构,所以当然也可以用来计数
但是我们使用这个命令时要注意,我们改变的值和增加的值,都需要是整数,不然就会报错
时间复杂度为O(1),返回值为变化后的值
12)hincrbyfloat
就是上一条指令的浮点数版本,用法是一样的,这里不多赘述
命令小结
我们上述也说了一些,一次查询多条数据的指令,但是我们在使用的时候要注意,我们redis可能做缓存也可能做服务器,所以如果我们查询的数据很多,就会导致阻塞,可能会导致出现服务器一瞬间压力过大引发一系列其他问题,而且我们上述的h系列的命令,必须要保证key对应的value必须是hash类型的
那如果我们一定要获取到所有的field数据,我们可以使用hscan,这个遍历redis的hash的渐进式遍历的,也就是一次遍历一小部分,分多次遍历所有的field,这样就不会阻塞住redis了
2.hash的内部编码
hash的内部编码有两种
ziplist(压缩列表):当hash类型的元素个数小于一定值时,redis就会使用ziplist来作为hash的内部实现,使用ziplist可以更加进奏的实现多个元素的连续存储,所以可以很好的节省空间,但是如果我们hash类型元素过多,就会导致读写效率会变得很慢
hashtable(哈希表):当元素个数比较多时,redis就会使用hashtable来作为内部实现,因为我们说元素个数多,就会导致ziplist的读写效率下降,但是hashtable的读写时间复杂度为O(1)
也就是说,ziplist是用时间换空间,但是hashtable是用空间换时间,而我们之前也说过,空间的话我们现在的硬件还是比较够用的,但是时间还是需要我们尽可能的去节省
3.hash的一些应用场景
1)作为缓存
我们之前使用的mysql是关系型数据库,用户的属性和信息表现为一个表
但是我们redis是使用一个个键值对,所以就会通过映射的方式来表示用户的信息,我们可以使用字符串的json格式
但是这样仍然不够直观,我们就可以通过今天的hash类型来存储
缓存方式对比:
1).如果我们使用原生的string类型
虽然也可以表示用户信息实现也很简单,但是会导致内存的占用量比较大,而且用户信息比较分散,因为每个key都是不同的,不满足高内聚的特点
2).如果我们序列化字符串使用json格式
优点:针对总是以整体作为操作的信息⽐较合适,编程也简单。同时,如果序列化⽅案选择合适,内存的使⽤效率很⾼
缺点:我们序列化和反序列化也有一定的开销,如果总操作个别属性就不是很灵活
3).hash类型
优点:简单,灵活,直观,同时可以很方便的存储和获取信息
缺点:在内部涉及到内部编码ziplist和hashtable的转换,可能会对内存造成消耗
(二)list列表
list就相当于顺序表,支持头插头删,尾插尾删,所以list内部的编码方式并非是一个简单的数据,更像是双端队列
同时,列表的元素是有序的,这里的有序是指顺序不同会导致结果不同,并不是升序和降序的有序,而且列表中的元素的允许重复的
1.list的一些命令
1)lpush
用来头插到list中(左头右尾),支持同时插入多个元素
时间复杂度O(1),如果插入多个元素就为O(n)n为插入元素的个数
返回值是插入后list的长度
2)lpushx
当key存在时就将元素头插到list中,不存在就直接返回
时间复杂度:只插⼊⼀个元素为O(1),插⼊多个元素为O(N),N为插⼊元素个数.
返回值:插⼊后list的⻓度。
3)rpush
把一个或多个元素尾插到list中
时间复杂度为O(1),插入N个为O(N)
返回值为list插入后的长度
4)rpushx
当key存在时就把一个或多个元素尾插到list中
时间复杂度:只插⼊⼀个元素为O(1),插⼊多个元素为O(N),N为插⼊元素个数.
返回值:插⼊后list的⻓度
5)lrange
我们这里的l是代表list并不是left
获取从start开始到end区间的所有元素,左右都是闭区间,并且因为redis支持负数下标,如果我们要获取到最后一个元素可以使用-1,并且redis下标与数组一样,都是从0开始
同时这里redis有一个很好的点
我们在c++中,如果下标超出了范围,我们一般会认为这是一个“未定义的行为”可能会导致程序崩溃,也可能会出现不合法或者合法的数据,这就会导致我们不一定会立刻发现问题,但是这种不负责的行为,效率确实是比较高
在java中,如果我们下标超出范围,会给我们抛出异常,但是因为要给我们多做一步下标合法性的验证,就会导致速度会比较慢,但是我们可以第一时间发现问题
而redis是尽可能的去获取到给定区间的元素,如果我们给了一个非法区间,比如超出下标,那么redis也会返回可以获取到下标元素的值(拥有鲁棒性,容错能力强)
6)lpop
头删,从list左侧取出元素
时间复杂度:O(1)
返回值:取出的元素或者nil。
7)rpop
尾删,从list右侧取出元素
时间复杂度:O(1)
返回值:取出的元素或者nil。
我们这里要注意,在当前的redis5中并没有count参数,但是redis6.2以后,新增了count参数,描述这一次要删几个元素
同时搭配这几个出队入队操作,可以实现,栈,队列,双端队列等数据结构
8)lindex
获取从左数第index位置的元素
时间复杂度:O(N)
返回值:取出的元素或者nil。
9)linsert
在特定的位置插入元素
我们发现我们在111签名插入1010,但是有两个111,所以我们会在最前面的一个111前面加
时间复杂度为O(N),返回值为插入后的list长度
10)llen
获取到list的长度
时间复杂度O(1),返回值为list的长度
11)lrem
删除某个值的一些元素
12)ltrim
保留start和stop之间的元素,两边外的元素直接被删除
13)lset
根据下标修改元素
我们如果想在不存在的下标上设置元素会直接返回nil
如果我们想删除一个指定位置的元素,我们可以通过lset把这个位置的元素设置为一特定的字符,然后通过lrem来进行删除
2.阻塞版本的命令
blpop和brpop是lpop和rpop的阻塞版本,他们的区别为:
1).如果列表有元素,那么阻塞和非阻塞是一样的,如果没有元素,非阻塞版本会返回nil,阻塞版本会根据阻塞时间,进行阻塞,这是redis可以执行一些别的命令,但是要求执行命令的客户端为阻塞状态。
2).命令中如果设置了多个间,就会从左向右遍历,一旦有一个键对应的列表有元素,就会弹出并且返回
3).如果有多个客户端执行blpop,最先执行命令的客户端会得到弹出的元素
我们redis中的list就相当于一个阻塞队列,我们之前在多线程上也说到过阻塞队列,但是当时我们需要手动保证线程安全,但是redis的线程安全是通过线程安全的情况,而因为我们redis的空间还是比较大的,一般不考虑队列满的情况
blpop
lpop的阻塞版本
返回值为列表和取出的元素或者nil
时间复杂度O(1)
brpop
rpop的阻塞版本
时间复杂度O(1)
返回值为取出的元素或nil
3.lsit的内部编码
1.ziplist(压缩列表)
我们之前在hash也说过,他的内部编码也是ziplist,ziplist把数据按照更紧凑的压缩形式进行标识,能够很好的节省空间,但是如果数据比较多操作数据的效率就会比较低
2.linkedlist(链表)
ziplist如果数据多,操作数据的效率会比较低,所以我们可以通过linkedlist来操作数据,虽然这样会使我们占用的空间变大,但是对数据的操作会比较快
但是这两种方式各有各的好处,有没有一种编码方式可以综合这两种的特点?
我们redis在之后使用了quicklist来内部编码list类型,quicklist相当于使list和ziplist的结合,整体还是一个链表,但是链表的每一个节点,是一个ziplist,我们通过链表来保证每一个ziplist都不是很大,这样就可以在节省空间的前提下,尽可能的快速的操作数据
4.使用场景
消息队列
我们说list给我们提供了阻塞状态的lpop和rpop,所以我们可以用这个阻塞实现生产者消费者模型,我们生产者客户端用lpush从列表左边插入元素,然后消费者用brpop来阻塞式的去拿队首元素
分频道的消息队列
redis使用lpush和brpop命令,但是通过不同键模拟不同频道的概念,不同的消费者可以通过brpop不同的键
作为数组
我们可以把list作为类似数组的一个结构,来存储多个元素,具体要如何组织数据,需要根据实际业务来确定