Hits

Redis基础知识

Redis 基础 (万丈高楼平地起)

Redis 安装

  • docker安装
docker pull redis // 拉取redis镜像
docker run --name myredis -d -p6379:6379 redis // 运行redis容器
docker exec -it myredis redis-cli // 执行容器中redis-cli命令,可以直接使用命令行操作redis
  • github源码安装
git clone --branch 2.8 --depth 1 git@github.com:antirez/redis.git // 下载源码
cd redis
make // 编译
cd src
./redis-server --daemonize yes //运行redis夫妻,daemonize表示后台执行
./redis-cli // 运行redis命令行 
  • apt-get install (Ubuntu)、yum install (RedHat) 或者 brew install (Mac)
brew install redis  // mac
apt-get install redis  // ubuntu
yum install redis  // redhat
redis-cli // 运行命令行

Redis 基础数据结构

Redis主要有5种基础数据结构,分别为 string(字符串)list(列表)set(集合)hash(哈希)zset(有序集合)

string(字符串)

string是Redis最简单的数据结构。Redis所有的数据结构都是以唯一的key字符串作为名称,然后通过这个唯一key值来获取相应的value数据,不同类型的数据结构的差异就在于value的结构不一样。

string 表示的是一个可变的字节数组,我们初始化字符串的内容、可以拿到字符串的长度,可以获取string的字串,可以覆盖string的字串内容,可以追加子串。

https://img.lg1024.com/blog/img/redis_string1.png

Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图所示,内部为当前字符串实际分配的空间capacity,一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会扩展1M的空间。需要注意的是字符串最大长度为512M。

https://img.lg1024.com/blog/img/redis_string.png

键值对 设置获取字符串,字符串没有提供子串插入方法和子串删除方法。批量对多个字符串的读写,可以节省网络耗时开销。

> set thunder shenzhen.xunlei  // 初始化 [变量名称] 和 [变量内容]
    OK
> get thunder   // 获取字符串内容 [变量名称]
    "shenzhen.xunlei"
> strlen thunder // 获取字符串长度 [变量名称]
    (integer) 15
> getrange thunder 4 7 // 获取子串 [变量名称] [start 0] [end 14]
    "zhen"
> setrange thunder 4 .liu // 覆盖子串 [变量名] [start] [子串]
    (integer) 15  // 返回长度
> get thunder
    "shen.liu.xunlei"
> append thunder .hao
    (integer) 19   // 返回长度
> get thunder
    "shen.liu.xunlei.hao"
> mset name1 boy name2 girl name3 unknown
    OK
> mget name1 name2 name3
    "body"
    "girl"
    "unknown"

计数器 如果字符串的内容是一个整数,那么还可以将字符串当作计数器来使用

> set thunder 42
    OK
> get thunder
    "42"
> incrby thunder 100
    "142"
> decrby thunder 100
    (integer) 42
> get thunder
    "42"
> incr thunder  // 等价于 incrby thunder 1
    (integer) 43
> decr thunder  // 等价于 decrby thunder 1
    (integer) 42

计数器是有范围的,它不能超过 Long.Max,不能低于 Long.Min

> set thunder 9223372036854775807
    OK
> incr thunder
    (error) ERR increment or decrement would overflow
> set thunder -9223372036854775808
    OK
> decr thunder
    (error) ERR increment or decrement would overflow

过期和删除 字符串可以使用 del 指令进行主动删除,可以使用 expire 指令设置过期时间,到点会自动删除,这属于被动删除。可以使用 ttl 指令获取字符串的寿命。

> expire thunder 60 // 60s后过期
    (integer) 1   // 1表示设置成功,0表示变量thunder不存在
> ttl thunder
    (integer) 50  // 还有50秒寿命,返回-2表示变量不存在,返回-1表示没有设置过期时间
> del thunder
    (integer) 1  // 删除成功返回1
> get thunder
    (nil)       // 变量thunder没有了

list

Redis的列表相当于Java语言里的LinkedList,注意列表是链表而不是数组。这意味着list的插入和删除操作非常快,时间复杂度O(1),但是索引定位很慢,时间复杂度是O(n)。

当列表弹出最后一个元素之后,该数据结构自动被删除,内存被回收。

https://img.lg1024.com/blog/img/redis_list2.png

Redis将列表数据结构命令为list而不是array,是因为列表的存储结构用的是链表而不是数组,而且链表是双向链表。因为它是链表,所以随机定位性能较弱,首尾插入删除性能较优,如果list的列表长度很长,使用时,我们一定要关注链表相关操作的时间复杂度。

https://img.lg1024.com/blog/img/redis_list.png

负下标 链表元素的位置使用自然数 0,1,2,...,n-1 表示,还可以使用负数 -1,-2,...,-n 来表示, -1 表示倒数第一, -2 表示倒数第二,那么 -n 就表示第一个元素,对应的下标为 0

队列/堆栈 链表可以从表头和表尾追加和移除元素,结合使用 rpush/rpop/lpush/lpop 四条指令,可以将链表作为队列或者堆栈使用,左向右向进行都可以

// 右进左出 (对列)
> rpush xunleilist go
    (integer) 1
> rpush xunleilist java python
    (integer) 3   // 现在列表存储的是  go--java--python
> lpop xunleilist
    "go"
> lpop xunleilist
    "java"
> lpop xunleilist
    "python"

// 左进右出
> lpush xunleilist go java python
    (integer) 3  // 现在列表存储的是  python--java--go
> rpop xunleilist
    "go"
...

// 右进右出 (栈)
> rpush xunleilist go java python
    (integer) 3  // 现在列表存储的是  go--java-python
> rpop xunleilist
    "python"
...

// 左进右出
> lpush xunleilist go java python
    (integer) 3 // 现在列表存储的是 python--java--go
> lpop xunleilist
    "python"
...

长度 使用 llen 指令获取链表长度

> rpush xunleilist go java python
    (integer) 3
> llen xunleilist
    (integer) 3

慢操作 lindex 它需要对链表进行遍历,性能随着参数index的增大而变差。 ltrim 保留 start_indexend_index 之间的值, ltrim 可以定义一个定长数组。 index可以为负数,-1为倒数第一个值,-2为倒数第二个值

随机读 可以使用index指令访问指定位置元素,使用range指令来获取链表子元素列表,提供 startend 下标参数

> rpush xunleilist go java python
    (integer) 3  // 当前列表存储的是 go java python
> lindex xunleilist 1  // 左边第2个元素,即java,从0开始算第1个 O(n) 慎用
    "java"
> lrange xunleilist 0 2 // 获取从左边起,第1个-第3个 O(n) 慎用
    "go"
    "java"
    "python"
> lrange xunleilist 0 -1 // 获取全部元素 O(n)慎用
    "go"
    "java"
    "python"
> ltrim xunleilist 1 -1 // O(n) 慎用
    OK
> ltrim xunleilist 1 0 // 其实清空列表。区间范围长度为负

使用 range 获取全部元素时,需要提供 end_index ,如果没有负下标,就需要首先通过 llen 指令获取总长度,才可以得出 end_index 的值,有了负下标,使用 -1 代替 end_index 就可以达到相同的效果。

修改元素 使用 lset 指令在指定位置修改元素

> rpush xunleilist go java python
    (integer) 3
> lset xunleilist 1 javascript
    OK
> lrange xunleilist 0 -1
    "go"
    "javascript"
    "python"

插入元素 使用 linsert 指令在列表中间位置插入元素,有经验的程序员都知道在插入元素时,我们经常搞不清楚是在指定位置的前面插入还是后面插入,所以antirez在 linsert 指令里增加了方向参数 beforeafter 来显示指示前置和后置插入。不过让人意想不到的是 linsert 指令并不是通过指定位置来插入的,而是通过指定具体的值。这是因为分布式环境下,列表的元素总是频繁的变化的,意味着上一时刻计算的元素的下标下一时刻可能就不是你所期望的下标了。

注意 linsert 只会在指定的第一个值的前/后插入一个值,多个相同的值,只会影响第一个。应用场景很少

> rpush xunleilist go java python
    (integer) 3
> linsert xunleilist before java ruby
    (integer) 4
> lrange xunleilist 0 -1
    "go"
    "ruby"
    "java"
    "python"

删除元素 列表的删除 lrem 操作也不是通过指定下标来确定元素的,你需要指定删除的最大个数以及元素的值。

> rpush xunleilist go java python
    (integer) 3
> lrem xunleilist 1 java
    (integer) 1
> lrange xunleilist 0 -1
    "go"
    "python"

定长列表 在实际应用场景中,我们有时候会遇到定长列表的需求。比如要求走马灯的形式实时显示中奖用户名列表,因为中奖用户太多,能显示的数量一般不超过100表,那么这里就会使用定长列表。维持定长列表的指令是 ltrim, 需要提供两个参数 startend,表示需要保留列表的下标范围,范围之外的所有元素都将被移除。如果 end 对应的真实下标小于 start,则其效果等价于 del 指令。

> rpush xunleilist go java python javascript ruby erlang rust cpp
    (integer) 8
> ltrim xunleilist -3 -1
    OK
> lrange xunleilist 0 -1
    "erlang"
    "rust"
    "cpp"

快速列表

如果再深入一点,你会发现Redis底层存储的还不是一个简单的linkedlist,而是称之为快速链表quicklist的一个结构。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。所以Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

hash

https://img.lg1024.com/blog/img/redis_hash.png

哈希等价于Java语言的HashMap或者是Python语言的dict,在实现结构上它使用二维结构,第一维是数组,第二维是链表,hash的内容key和value存放在链表中,数组里存放的是链表的头指针。通过key查找元素时,先计算key的hashcode,然后用hashcode对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的value值,链表的左右就是用来将产生了 hash碰撞 的元素串起来,哈希的第一维数组的长度也是2^n

https://img.lg1024.com/blog/img/redis_hash2.png

增加元素 可以使用 hset 一次增加一个键值对,也可以使用 hmset 一次增加多个键值对

> hset xunleihash go fast
    (integer) 1
> hset xunleihash java fast python slow
    OK

获取元素 可以通过 hget 定位到具体key对应的value,可以通过 mhget 获取多个key对应的value,可以使用 hgetall 获取所有的键值对,可以使用 hkeyshvals 分别获取所有的key列表和value列表,这些操作和java的Map接口很类似。

> hmset xunleihash go fast java fast python slow
    OK
> hget xunleihash go
    "fast"
> hmget xunleihash go python
    "fast"
    "slow"
> hgetall xunleihash
    "go"
    "fast"
    "java"
    "fast"
    "python"
    "slow"
> hkeys xunleihash
    "go"
    "java"
    "python"
> hvals xunleihash
    "fast"
    "fast"
    "slow"

删除元素 可以使用 hdel 删除指定key,也可以删除多个key

> hmset xunleihash go fast java fast python slow
    OK
> hdel xunleihash go
    (integer) 1
> hdel xunleihash java python
    (integer) 2

判断元素是否存在 通常我们使用 hget 获得key对应的value是否为空就知道对应的元素是否存在了,不过如果value的字符串长度特别大,通过这种方式判断元素存在与否就略显浪费,这是可以使用 hexists 判断key是否存在

> hmset xunleihash go fast java fast python slow
    OK
> hexists xunleihash go
    (integer) 1

计数器 hash结构还可以当计数器使用,对于内部的每一个key都可以作为独立的计数器。如果value值不是整数,调用 hincrby 指令会出错

> hincrby xunleihash go 1
    (integer) 1
> hincrby xunleihash python 4
    (integer) 4
> hincrby xunleihash java 4
    (integer) 4
> hgetall xunleihash
    "go"
    "1"
    "python"
    "4"
    "java"
    "4"
> hset xunleihash rust good
    (integer) 1
> hincrby xunleihash rust 1
    (error) ERR hash value is not an integer

扩容 当哈希内部的元素比较拥挤时(hash碰撞比较频繁),就需要进行扩容。扩容需要申请新的两倍大小的数组,然后将所有的键值对重新分配到新的数组下标对应的链表中(rehash)。如果hash结构很大,比如有上百万个键值对,那么一次完整rehash的过程就会耗时很长。对于单线程的Redis来说就亚历山大,所以Redis采用了渐进式rehash的方案。它们会同时保留两个新旧的hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中,这样就可以避免因扩容导致的线程卡顿现象。

缩容 Redis的hash结构不断有扩容还有缩容,从这点看,它要比Java的HashMap厉害,Java的HashMap只有扩容,缩容的原理和扩容一样,只不过新的数组大小要比旧数组小一倍。

set

Java程序员都知道HashSet的内部结构实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样的,它的内部也使用hash结构,所有的value都指向同一个内部值。

https://img.lg1024.com/blog/img/redis_zset3.png

增加元素 可以一次增加多个元素

> sadd xunleiset go java python
    (integer) 3

读取元素 使用 smembers 列出所有的元素,使用 scard 获取集合长度,使用 srandmember 获取随机count个元素,如果不提供count参数,默认为1

> sadd xunleiset go java python
    (integer) 3
> smembers xunleiset
    "java"
    "python"
    "go"
> scard xunleiset
    (integer) 3
> srandmember xunleiset
    "java"

删除元素 使用 srem 删除一到多个元素,使用 spop 删除随机一个元素

> sadd xunleiset go java python rust erlang
    (integer) 5
> srem xunleiset go java
    (integer) 2
> spop xunleiset
    "erlang"

判断元素是否存在 使用 sismember 指令,只能接受单个元素

> sadd xunleiset go java python rust erlang
    (integer) 5
> sismember xunleiset rust
    (integer) 1
> sismember xunleiset javascript
    (integer) 0

sortedset

https://img.lg1024.com/blog/img/redis_sortedset2.png

SortedSet(zset) 是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构 Map<String, Double> ,可以给每一个元素value赋予一个权重 score ,另一方面它又类似于 TreeSet ,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

https://img.lg1024.com/blog/img/redis_sortedset.png

zset底层实现使用了两个数据结构,第一个hash,第二个是跳跃列表,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。跳跃列表的目的在于给元素value排序,根据score的范围获取元素列表。

增加元素 通过 zadd 指令可以增加一到多个value/score对,score放在前面

> zadd xunleisortedset 4.0 python
    (integer) 1
> zadd xunleisortedset 4.0 java 1.0 go
    (integer) 2

长度 通过指令 zcard 可以得到zset的元素个数

> zcard xunleisortedset
    (integer) 3

删除元素 通过指令 zrem 可以删除zset元素,可以一次删除多个

> zrem xunleisortedset go python
    (integer) 2

计数器 同hash结构一样,zset也可以作为计数器使用

> zadd xunleisortedset 4.0 python 4.0 java 1.0 go
    (integer) 3
> zincrby xunleisortedset 1.0 python
    "5"

获取排名和分数 通过 zscore 指令获取指定元素的权重,通过 zrank 指令获取指定元素的正向排名,通过 zrevrank 指令获取指定元素的反向排名[倒数第一名]。正向是由小到打,反向是由大到小。

> zscore xunleisortedset python
    "5"
> zrank xunleisortedset go
    (integer) 0
> zrank xunleisortedset python
    (integer) 2
> zrevrank xunleisortedset python
    (integer) 0

根据排名范围获取元素列表 通过 zrange 指令指定排名范围参数获取对应的元素列表,携带 withscores 参数可以一并获取元素的权重,通过 zrevrange 指令按负向排名获取元素列表[倒数]。正向是由小到大,负向是由大到小。

> zrange xunleisortedset 0 -1
    "go"
    "java"
    "python"
> zrange xunleisortedset 0 -1 withscores
    "go"
    "1"
    "java"
    "4"
    "python"
    "4"
> zrevrange xunleisortedset 0 -1 withscores

根据score范围获取列表 通过 zrangebyscore 指令获取指定score范围对应的元素列表。通过 zrevrangebyscore 指令获取倒排元素列表。正向是由小到大,负向是由大到小。参数 -inf 表示负无穷,+inf 表示正无穷。

> zrangebyscore xunleisortedset 0 5
    "go"
    "java"
    "python"
> zrangebyscore xunleisortedset -inf +inf withscores
    "go"
    "1"
    "java"
    "4"
    "python"
    "4"
> zrevrangebyscore xunleisortedset +inf -inf withscores  # 注意反过来了
    "python"
    "4"
    "java"
    "4"
    "go"
    "1"

根据范围移除元素列表 可以通过排名范围,也可以通过score范围来一次性移除多个元素

> zremrangebyrank xunleisortedset 0 1
    (integer) 2  # 删除了2个元素
> zadd xunleisortedset 4.0 java 1.0 go
    (integer) 2
> zremrangebyscore xunleisortedset 4 +inf
    (integer) 2
> zrange xunleisortedset 0 -1
    go

跳跃列表 zset 内部的排序功能是通过[跳跃列表]数据结构来实现的,它的结构非常特殊,也比较复杂。

因为zset要支持随机的插入和删除,所以它不好使用数组来表示,先看一个普通的链表结构。

https://img.lg1024.com/blog/img/redis_zset.png

我们需要这个链表按照score值进行排序。这意味着当有新元素需要插入时,需要定位到特定位置的插入点,这样才可以继续保证链表是有序的。通常我们会通过二分查找来找到插入点,但是二分查找的对象必须是数组,只有数组才可以支持快速位置定位,链表做不到,那该怎么办?

想想一个创业公司,刚开始只有几个人,团队成员之间人人平等,都是联合创始人。随着公司的成长,人数渐渐变多,团队沟通成本随之增加。这时候就会引入组长制,对团队进行划分。每个团队会有一个组长。开会的时候分团队进行,多个组长之间还会有自己的会议安排。公司规模进一步扩展,需要再增加一个层级——部门,每个部门会从组长列表中推选出一个代表来作为部长。部长们之间还会有自己的高层会议安排。

跳跃列表就是类似于这种层级制,最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。然后在这些代表里再挑出二级代表,再串起来。最终就形成了金字塔结构。

想想你老家在世界地图中的位置:亚洲–>中国->安徽省->安庆市->枞阳县->汤沟镇->田间村->xxxx号,也是这样一个类似的结构。

https://img.lg1024.com/blog/img/redis_zset2.png

「跳跃列表」之所以「跳跃」,是因为内部的元素可能「身兼数职」,比如上图中间的这个元素,同时处于L0、L1和L2层,可以快速在不同层次之间进行「跳跃」。

定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。你也许会问那新插入的元素如何才有机会「身兼数职」呢?

跳跃列表采取一个随机策略来决定新元素可以兼职到第几层,首先L0层肯定是100%了,L1层只有50%的概率,L2层只有25%的概率,L3层只有12.5%的概率,一直随机到最顶层L31层。绝大多数元素都过不了几层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。

这还挺公平的,能不能进入中央不是靠拼爹,而是看运气。

本文链接:参与评论 »

--EOF--

提醒:本文最后更新于 129 天前,文中所描述的信息可能已发生改变,请谨慎使用。

专题「数据库相关知识」的其它文章 »

Comments