这篇文章强烈建议:看的时候 自己准备一个redis,照着敲一遍命令来加深印象,不然看完了记不住几个命令;
redis的基础数据结构:string,list,hash,set和zset。
string
redis的string表示的是一个可变的字节数组,初始化字符串的内容,可以拿到字符串的长度,可以获取string的子串,可以覆盖string的子串内容,可以追加子串。
如果在redis中放入helloworld,内存中的存储方式为:
redis的字符串是动态字符串,内部结构实现上类似于java的ArrayList。它采用预分配冗余空间的方式来减少内存的频繁分配。
当前字符串实际分配的空间为capacity,capacity一般要高于实际字符串长度len。
当字符串长度小于1M时,扩容都是加倍现有的空间。
当字符串超过1M,扩容时一次只会多扩1M的空间。
注意:字符串最大长度为512M。
初始化字符串
获取字符串
获取字符串的长度
获取子串
覆盖子串
setrange 命令返回的时当前字符串的长度
追加子串
append 命令返回的时当前字符串的长度
计数器
如果字符串的内容是一个整数,还可以把字符串当成计数器来使用。
incrby key 整数值 加法
decrby key 整数值 减法
incr key 等价于 incrby key 1 加一
decr key 等价于 decrby key 1 减一
计数器是有范围的,它不能超过Long.Max,不能低于Long.Min
计数器的范围:-9223372036854775808 —— 9223372036854775807
过期和删除
字符串可以使用del指令进行主动删除,也可以用expire指令设置过期时间,到时间会自动删除(被动删除)。还可以使用ttl指令获取字符串的寿命。
del 和 expire 返回1 表示设置成功,0表示hdx不存在
ttl 返回 52表示 还有 52秒的寿命,返回-2表示变量不存在,-1表示没有设置过期时间
(nil)表示 hdx没有了
list
redis的list的存储结构用的是链表而不是数组,而且还是双向链表。因为是双向链表,所以随机定位性能较弱,首尾插入删除性能较优。如果list的列表长度很长,使用时我们一定要关注链表相关操作的时间复杂度。
负下标 链表元素的位置使用自然数0,1,2,.....n-1表示,还可以使用负数-1,-2,...-n来表示,-1表示 倒数第一,-2表示 倒数第二,那么-n就表示第一个元素,对应的下标为0。
队列/堆栈 链表可以从表头和表尾追加和移除元素,结合使用rpush/rpop/lpush/lpop四条指令,可以将链表作为队列或堆栈使用,左向右向进行都可以
注意:rpop/lpop 移除并返回此数据的指令,返回后数据将消失;
在日常应用中,列表常用来作为异步队列使用。
redis的链表最多可以包含 2的32次方- 1 个元素 (4294967295, 每个列表超过40亿个元素)
长度 llen指令获取链表长度。
随机读 lindex指令访问指定位置的元素
注意:lindex 随机访问数据,访问数据不会删除数据,支持负数
修改元素 lset指令在指定位置修改元素
插入元素 linsert指令在列表的中间位置插入元素
有经验的程序员都知道在插入元素时,我们经常搞不清楚是在指定位置的前面插入还是后面插入,所以antirez在linsert指令里增加了方向参数before/after来显示指示前置和后置插入。不过让人意想不到的是linsert指令并不是通过指定位置来插入,而是通过指定具体的值。这是因为在分布式环境下,列表的元素总是频繁变动的,意味着上一时刻计算的元素下标在下一时刻可能就不是你所期望的下标了。
删除元素 lrem指令来删除下标的元素
定长列表 ltrim指令维持定长列表
实际应用场景中,我们有时候会遇到「定长列表」的需求。比如要以走马灯的形式实时显示中奖用户名列表,因为中奖用户实在太多,能显示的数量一般不超过100条,那么这里就会使用到定长列表。维持定长列表的指令是ltrim,需要提供两个参数start和end,表示需要保留列表的下标范围,范围之外的所有元素都将被移除。
如果指定参数的end对应的真实下标小于start,其效果等价于del指令,因为这样的参数表示需要需要保留列表元素的下标范围为空。
快速列表
如果再深入一点,你会发现Redis底层存储的还不是一个简单的linkedlist,而是称之为快速链表quicklist的一个结构。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。所以Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
hash
哈希等价于Java语言的HashMap或者python语言的dict,在实现结构上它使用二位结构,第一维是数组,第二维是链表,hash的内容key和value存放在链表中,数组里存放的是链表的头指针。通过key查找元素时,先计算key的hashcode,然后用hashcode对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取对应的value值,链表的作用就是用来将产生了【hash碰撞】的元素串起来。java语言开发者会感到非常熟悉,因为这样的结构和hashmap是没有区别的。哈希的第一维数组的长度也是2的n次方。
增加元素
hset一次增加一个键值对,hmset一次可以增加多个键值对
获取元素
hget获取单个key的value值,hmget获取多个key的value值,hgetall获取所有key的value值,hkeys获取所有的key的列表,hvals获取所有value值。
删除元素
hdel 删除指定key或者删除多个key
判断元素是否存在
(1) hget 获取key的value,判断value是否为空来判断元素是否存在(如果value的字符串长度特别大,这种方式来判断是否存在就有点浪费资源)
(2)hexists 判断元素是否存在(只试用hash)
计数器
hincrby 做加法运算
hincrby hhh 1 -1 表示hhh这个hash中给key为1的值做减一操作
扩容
当hash内部的元素比较拥挤时(hash碰撞比较频繁),就需要进行扩容。扩容需要申请新的两倍大小的数组,然后将所有的键值对重新分配到新的数组下标对应的链表中(rehash)。如果hash结构很大,比如有上百万个键值对,那么一次完整rehash的过程就会耗时很长。这对于单线程的redis里来说有点压力山大。所有redis采用了渐进式rehash的方案。redis会同时保留新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。
缩容
redis的hash结构不但有扩容还有缩容,这点上redis比java的hashmao要强一点,java的hashmap只有扩容。缩容的原理和扩容是一致的,只不过新的数组大小要比旧数组小一倍。
set
java程序袁都知道hashset的内部实现就是hashmap,只不过所有的value都指向同一个对象。
redis的set结构也是一样,set的内部也使用hash结构,所有的value都指向同一个内部值。
增加元素
sadd 可以加一个元素或者多个元素
读取元素
smembers 列出所有元素
scard 获取集合长度
srandmember 获取count个元素 如果不提供count参数,默认为1
删除元素
srem 删除一个或多个元素
spop 删除随机一个元素
判断元素是否存在
sismember 只能接受单个元素
sortedset
sortedSet(zset)是redis提供的一个非常特别的数据结构,一方面sortedSet等价于java的数据结构Map<String,Double>
,可以给每一个元素value赋予一个权重score,另一方面sortedSet又类似于treeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名词,还可以通过score的范围来获取元素的列表。
zset底层实现使用了两个数据结构,第一个是hash,第二个是跳跃列表,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。跳跃列表的目的在于给元素value排序,根据score的范围获取元素列表。
增加元素
zadd 可以增加一个到多个value/score对,score放在前面
长度
zcard可以得到zset的元素个数
删除元素
zrem 删除zset中的元素,可以一次删除多个;
获取排名和分数
zscore 获取指定元素的权重
zrank 获取指定元素的正向排名
zrevrank 获取指定元素的反向排序【倒数第一】
正向是由小到大,反向是由大到小
计数器
同hash结构一样,zset也可以作为计数器使用。
根据排名范围获取元素列表
zrange 指定排名范围参数获取对应的元素列表,携带withscores参数可以一并获取元素的权重
zrevrange 指定按负向排序获取元素列表【倒数】。
正向是由小到大,反向是由大到小
根据范围移除元素列表
zremrangebyrank 通过排名范围删除单个或多个元素
zremrangebyscore 通过score范围来一次性移除多个元素
跳跃列表
zset内部的排序功能是通过【跳跃列表】数据结构来实现的,它的结构非常特殊,也比较复杂。
因为zset要支持随机的插入和删除,所有它不好使用数组来表示。我们先看一个普通的链表结构。
我们需要这个链表按照score值进行排序。这意味着当有新元素需要插入时,需要定位到特殊位置的插入点,这样才可以继续保证链表时有序的。通常我们会通过二分查找来找到插入点,但是二分查找对象必须时数据,只有数组才可以支持快速定位,链表做不到,那该怎么办?
例如:现在规范一点的公司,都会有组长,部门主管的职位,当进行开会的时候一般都是 部门主管先开会,然后再组长开会,再给其他人员开会。
跳跃列表就是类似于这种层级制,最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级的指针串起来。然后在这些代表里再跳出二级代表,再串起来。最终形成了金字塔结构。
【跳跃列表】之所以【跳跃】,是因为内部元素可能【身兼数职】,比如上图中间的这个元素同时处于L0,L1和L2层,可以快速在不同层次之间进行【跳跃】。
定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新的元素插入进去。你也许会问那新插入的元素如何才有机会【身兼数职】呢?
跳跃列表采取一个随机策略来决定新元素可以兼职到第几层,首先L0层肯定是100%了,L1层只有50%的概率,L2层只有25%的概率,L3层只有12.5%的概率,一直随机到最顶层L31层。绝大多数元素都过不了基层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。
这还挺公平的,能不能进入中央不是靠拼爹,而是看运气。