Redis核心数据结构

Redis是属于key-value结构,value可以存储的数据结构非常多,在工作中常用的有以下5种:

Redis核心数据结构
  1. String(字符串)
  2. Hash(哈希字典)
  3. List(列表)
  4. Set(集合)
  5. Zset(有序集合)

其中,list、hash、set、zset这四种数据结构属于 容器数据结构,共享以下两条通用原则:

  • 容器不存在则自动创建
  • 如果容器中没有原则,则自动删除容器并释放内存

String(字符串)

String内部结构

String是Redis最简单也是最广泛使用的数据结构,其内部是一个数组:

String内部结构

内部结构实际大小

Redis中string(字符串)是动态字符串,允许修改;它在结构上的实现类似于Java中的ArrayList(默认构造一个大小为10的初始数组),这是冗余分配内存的思想,也称为预分配;这种思想可以减少扩容带来的性能消耗。

String扩容

当string(字符串)的大小达到扩容阈值时,将会对string(字符串)进行扩容,string(字符串)的扩容主要有以下几个点:

  1. 长度小于1MB,扩容后为原先的两倍; length = length * 2

  2. 长度大于1MB,扩容后增加1MB; length = length + 1MB

  3. 字符串的长度最大值为 512MB

常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 单值操作
SET key value //存入字符串键值对
GET key //获取一个字符串键值
DEL key [key ...] //删除一个键


// 批量操作
MSET key value [key value ...] //批量存储字符串键值对
MGET key [key ...] //批量获取字符串键值


// 单值、批量都可以
SETNX key value //存入一个不存在的字符串键值对
EXPIRE key seconds //设置一个键的过期时间(秒)

// 原子加减
INCR key //将key中储存的数字值加1
DECR key //将key中储存的数字值减1
INCRBY key increment //将key所储存的值加上increment
DECRBY key decrement //将key所储存的值减去decrement

使用场景

单值缓存

1
2
3
4
5
6
7
8
localhost:1>set love 1
"OK"
localhost:1>get love
"1"
localhost:1>del love
"1"
localhost:1>get love
null

对象缓存

在字符串中进行对象缓存有2种方法:

  • 把 entity对象转成JSON字符串后,进行单值存储

  • 通过批量存储字符串键值对的方式(批量缓存)

    1
    2
    3
    4
    5
    localhost:0>mset user:1:usenrame zhangsan user:1:age 20
    "OK"
    localhost:0>mget user:1:usenrame user:1:age
    1) zhangsan
    2) "20"

为什么推荐 批量存储字符串键值对的形式 来缓存entity对象?

  • 当我们需要去修改 Redis存储的对象字段内容的话
    • 第一种:必须先把整个JSON字符串提取出来后,转成对象后再修改字段,再转成JSON字符串存进去
    • 第二种:因为是单独的去存储每个字段,所以可以直接单独修改每个字段内容,相对第一种 效率提高了很多

过期set命令

  • 通过给缓存key设置过期时间,使缓存到期后会自动删除 过期的key

方式一:先创建key,对key设置过期时间

1
2
3
4
5
6
7
8
9
// expire key seconds
localhost:1>expire test:1 10
"1"
// 10秒内查询
localhost:1>get test:1
"40"
// 10秒后查询
localhost:1>get test:1
null

方式二:创建key的同时设置过期时间

1
2
3
4
5
6
7
8
9
// setex key seconds value
localhost:1>setex test:2 5 zhangsan
"OK"
// 5秒内查询
localhost:1>get test:2
"zhangsan"
// 5秒后查询
localhost:1>get test:2
null

计数器

string也可以用来做计数器,但前提value必须是整数,自增范围区间:[-9223372036854775808,9223372036854775808]

image-1

我们可以在浏览一次后,就在redis中进行一次计数,用于做 阅读量、点赞量等计数器。

incr -> 自增1

1
2
3
4
5
6
7
8
9
10
11
localhost:0>INCR article:1
"1"
localhost:0>INCR article:1
"2"
localhost:0>INCR article:1
"3"

// 获取文章ID为1的计数器
localhost:0>GET article:1
"3"

image-1

incrby -> 自定义累加值

1
2
3
4
5
6
localhost:1>set test:1 20
"OK"
localhost:1>incrby test:1 20
"40"
localhost:1>get test:1
"40"
incrby

测试value为整数的自增区间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 最大值
localhost:1>set max 9223372036854775808
"OK"
// 自增失败,超出了区间范围
localhost:1>incr max
"ERR value is not an integer or out of range"

// 最小值
localhost:1>set min -9223372036854775808
"OK"

// 自减的失败,超出了区间范围
localhost:1>incrby min -1
"ERR increment or decrement would overflow"

Hash(哈希字典)

Redis的hash(字典)相当于Java语言中的HashMap,它是根据散列值分布(哈希计算)的无序字典,内部的元素是通过键值对的方式存储。

它的数据结构也是数组+链表组成的二维结构,节点元素散列在数组(哈希桶)上,如果发生hash碰撞则使用链表串联在数组节点上。

Hash哈希字典

Hash(字典)扩容

  • Redis中的hash存储的value只能是字符串
  • Java中的HashMap 扩容是一次性完成扩容的,而Redis考虑到单线程性能问题,采用的是渐进式扩容,多次完成
    • 渐进式扩容:需要扩容时会拷贝一个新的hash结构,在rehash(扩容)结束后,会把旧的hash数值同步到新的hash,并删除旧的hash,用新的hash来代替旧的hash。

常用操作

1
2
3
4
5
6
7
8
9
10
HSET  key  field  value 			//存储一个哈希表key的键值
HSETNX key field value //存储一个不存在的哈希表key的键值
HMSET key field value [field value ...] //在一个哈希表key中存储多个键值对
HGET key field //获取哈希表key对应的field键值
HMGET key field [field ...] //批量获取哈希表key中多个field键值
HDEL key field [field ...] //删除哈希表key中的field键值
HLEN key //返回哈希表key中field的数量
HGETALL  key //返回哈希表key中所有的键值

HINCRBY key field increment //为哈希表key中field键的值加上增量increment

使用场景

对象缓存

前面提到过,redis是kye-value的形式,而哈希本身也是key-value的形式,前面有提到过用字符串来缓存entity对象,同样哈希也是可以缓存entity对象的。

哈希对象缓存 image-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 存储语句分析 user:redis key 1:.... 为哈希key ,zhangsan:哈希value
HMSET user 1:username zhangsan

// 存储对象 id为1,username为张三的entity对象
localhost:0>HMSET user 1:username zhangsan 1:age 20 1:balance 8888

// 存储对象 id为2,username为李四的entity对象

// 获取数据
localhost:0>HMGET user 1:username 1:age 1:balance
1) "zhangsan"
2) "20"
3) "8888"

localhost:0>HMGET user 2:username 2:age 2:balance
1) "lisi"
2) "99"
3) "100"

电商购物车

在电商系统可以通过redis快速的实现购物车功能,性能、效率比直接操作数据库更快。

redis实现京东购物车

下面我们将通过一个小示例,通过redis实现京东购物车功能:

image-1

字段解释:

image-1

如:card:1 10001 1

card+用户ID 作为redis的key,商品ID为field(字段名,哈希内层key),商品数量为value(哈希内层value)

购物车操作

1)添加商品到购物车

1
2
3
4
5
6
7
8
9
10
11
12
// 使用hset时,如果key已经存在,则会更新数据(返回0),如果不存在则会新建该键值对(返回1)。

// 新建键值对的情况
localhost:1>hset card:1 10001 1
"1"

localhost:1>hset card:1 10002 5
"1" // 创建键值对

// 更新键值对数据
localhost:1>hset card:1 10001 2
"0" // 键值对已存在,强制更新

2)增加购物车商品数量(hincrby)

1
2
localhost:1>hincrby card:1 10001 5
"6" // 增加5后,购物车的商品(ID:10001)的最新数量

3)获取购物车商品总数量

1
2
localhost:1>hlen card:1
"2" // 代表购物车中就只有2个商品

4)从购物车中删除某商品

1
2
3
4
5
6
localhost:1>hdel card:1 10001
"1" // 执行成功

// 删除后 我们在去查询 购物车商品总数量,只剩下1个
localhost:1>hlen card:1
"1"

5)获取购物车所有商品

1
2
3
4
5
6
7
8
9
10
11
localhost:1>hgetall card:1
1) "10002" //商品ID为10002(Hash key)
2) "6" //商品ID为10002 的数量(Hash value)是6
3) "10003"
4) "1"
5) "10004"
6) "1"
7) "10005"
8) "2"
9) "10006"
10) "9"

Hash优缺点

优点

  • 方便同类数据归档整合存储,方便数据管理
    • 如:存储entity实体对象
  • 相较于String 操作消耗内存和CPU更少,且比String 更节省空间

缺点

  • 过期功能不可用用在field(Hash内层Key)上,只能用在Redis 外层Key
    • 要嘛就是整个key过期,哈希内层的field 字段是不可以加倒计时的
  • Redis集群架构下不适合大规模使用
    • 因为哈希结构的数据是一个完整的结构,如果在集群架构下使用Hash结构的Redis,很容易造成 数据倾斜
数据倾斜
redis集群示意图

假设我们现在有个entity 实体类(user),如果通过Redis集群存储,因为数据分片存储,只能挑其中的某个节点,那么其他的节点是没有这个entity数据的,这就是 数据倾斜,如果大量的用户来访问该数据时,请求都会走到该节点中,该节点的压力会非常大。

解决数据倾斜方案:分段存储

把存储的entity通过某些特定的规则,均匀的分段存储在不同的Redis节点。

List(列表)

List 底层结构

Redis的列表相当于Java语言中的LinkedList,它是一个双向链表数据结构,支持前后顺序遍历。链表结构插入和删除操作快,时间复杂度O(1),查询慢,时间复杂度O(n)。

List双向链表结构

Redis底层存储list(列表)不是一个简单的LinkedList,而是quicklist ——“快速列表”,quicklist是多个ziplist(压缩列表)组成的双向链表。

ziplist

  • ziplist是一块连续的内存存储空间,他们之间无需持有prev和next指针,能通过地址顺序寻址访问。
ziplist

redis底层在存储List元素时,当元素个数少的时候,它会使用一块连续的内存空间来存储,这样样可以减少每个元素增加prev和next指针带来的内存消耗,最重要的是可以减少内存碎片化问题。

quicklist

  • quicklist是由多个ziplist组成的双向链表。
quicklist

常用操作

1
2
3
4
5
6
7
8
LPUSH  key  value [value ...] 		//将一个或多个值value插入到key列表的表头(最左边),往key列表最左侧插入数据
RPUSH key value [value ...] //将一个或多个值value插入到key列表的表尾(最右边),往key列表最右侧插入数据(符合习惯)
LPOP key //移除并返回key列表的头元素(获取并移除 Key列表最左侧的头元素)
RPOP key //移除并返回key列表的尾元素(获取并移除 Key列表最右侧的尾部元素)
LRANGE key start stop //返回列表key中指定区间内的元素,区间以偏移量start和stop指定

BLPOP key [key ...] timeout //从key列表表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key ...] timeout //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待

使用方式

使用方式示意图

Stack(栈)

  • 栈在结构上是先进后出(FILO)的数据结构(比如弹夹压入子弹,子弹被射击出去的顺序就是栈),这种数据结构一般用来逆序输出。
LPUSH 左侧入栈小实验
LPUSH左侧入栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 从左侧入栈  
localhost:1>LPUSH user a1
"1" // a1
localhost:1>LPUSH user a2
"2" // a2 a1
localhost:1>LPUSH user a3
"3" // a3 a2 a1
localhost:1>LPUSH user a4
"4" // a4 a3 a2 a1
localhost:1>LPUSH user a5
"5" // a5 a4 a3 a2 a1

// 从左侧依次往右读取,按顺序 先进先出,张三先进所以张三先出
localhost:1>LPOP user
"a5"
localhost:1>LPOP user
"a4"
localhost:1>LPOP user
"a3"
localhost:1>LPOP user
"a2"
localhost:1>LPOP user
"a1"

Queue 队列

  • 先进先出原则,类似于MQ(LPUSH + RPOP)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
localhost:1>LPUSH admin 1
"1"
localhost:1>LPUSH admin 2
"2"
localhost:1>LPUSH admin 3
"3"
localhost:1>LPUSH admin 4
"4"
localhost:1>LPUSH admin 5
"5"
localhost:1>RPOP admin
"1"
localhost:1>RPOP admin
"2"
localhost:1>RPOP admin
"3"
localhost:1>RPOP admin
"4"
localhost:1>RPOP admin
"5"
CleanShot 2023-02-20 at 15.16.18

Blocking MQ(阻塞队列)

  • 当你的队列中没有数据时,我们可通过 阻塞命令获取,那他就会一直阻塞住队列,直到 时间s超市(阻塞设置了超时时间),或有数据进入队列为止。
1
2
BLPOP  key  [key ...]  timeout	//从key列表表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key ...] timeout //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待

使用场景

List的使用场景很多,需要自行去思考,去尝试和实践,下面介绍一种比较常见的玩法。

订阅号消息

这里我们订阅了《中国联通》和《人在厦门》的公众号,他们会对我们进行推送消息。

image-1

在我们程序员来看,实现该功能有多种方法,有两种是比较常见的。

  1. 《中国联通》发表文章后,系统主动的去push 通知给关注他的人
    • 如果粉丝较少,主动push到redis中还可以接受,如果粉丝较多,达到数十万 数百万,主动去push的话,对redis的压力太大,为此我们可以考虑做出一定的优化,例如 只push在线的粉丝,没在线的粉丝可以通过 第二种方式主动去pull 拉去通知
  2. 粉丝客户登录APP后,主动去拉去 他关注的 公众号的文章

实现过程:

示意图
  1. 《中国联通发表文章》–> redis操作: LPUSH msg:{关注该公众号的粉丝-ID} 文章ID – > LPUSH msg:2200919 991029193
  2. 客户上线后,需要查看该列表 –> redis操作:LRANGE msg:{粉丝-ID} 0 4 ;「0 - 4 读3个,0 、 1 、 3 ,index是从0开始」
    1. LRANGE msg:{粉丝-ID} 0 4

Set(无序集合)

redis的set 类似于Java的HashSet,set内部的键值对是无序且唯一的,当set集合中最后一个元素被删除时,集合会自动删除,回收内存。

常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Set常用操作
SADD key member [member ...] //往集合key中存入元素,元素存在则忽略,
若key不存在则新建
SREM key member [member ...] //从集合key中删除元素
SMEMBERS key //获取集合key中所有元素
SCARD key //获取集合key的元素个数
SISMEMBER key member //判断member元素是否存在于集合key中
SRANDMEMBER key [count] //从集合key中选出count个元素,元素不从key中删除
SPOP key [count] //从集合key中选出count个元素,元素从key中删除

Set运算操作
SINTER key [key ...] //交集运算
SINTERSTORE destination key [key ..] //将交集结果存入新集合destination中
SUNION key [key ..] //并集运算
SUNIONSTORE destination key [key ...] //将并集结果存入新集合destination中
SDIFF key [key ...] //差集运算
SDIFFSTORE destination key [key ...] //将差集结果存入新集合destination中

应用场景

由于set具备去重(唯一性)功能,在生产环境中的应用场景非常多,这里只举两个个例子:微信抽奖小程序和朋友圈点赞。

微信抽奖小程序

抽奖小程序

因为set是不会重复的,那我们在使用set集合来做抽奖活动时候,key可以存储活动项目ID,value可以存储参与抽奖用户的ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 参与抽奖命令   存储 活动ID 和用户ID 
localhost:1>SADD Lottery:29819289 19201
"1" //返回1添加成功,返回0代表出现了重复情况,添加失败
localhost:1>SADD Lottery:29819289 19202
"1"
localhost:1>SADD Lottery:29819289 19203
"1"
localhost:1>SADD Lottery:29819289 19204
"1"
localhost:1>SADD Lottery:29819289 19205
"1"

// 查看参与抽奖的用户数量
localhost:1>SCARD Lottery:29819289
"5"

// 查看所有参与抽奖的用户列表
localhost:1>SMEMBERS Lottery:29819289
1) "19201"
2) "19202"
3) "19203"
4) "19204"
5) "19205"


// 开始抽奖
// 抽中后不删除该元素,适用于 只抽奖一个人
localhost:1>SRANDMEMBER Lottery:29819289
"19202"
// 查看是否删除了该元素
localhost:1>SMEMBERS Lottery:29819289
1) "19201"
2) "19202"
3) "19203"
4) "19204"
5) "19205"

// 抽中后从Set中删除该元素,适用于 抽出一等奖、二等奖、三等奖等
localhost:1>SPOP Lottery:29819289
"19204"
// 查看是否删除了该元素
localhost:1>SMEMBERS Lottery:29819289
1) "19201"
2) "19202"
3) "19203"
4) "19205"

参与抽奖命令

CleanShot09.39.54

查看参与抽奖的用户数量和查看所有参与抽奖的用户列表

CleanShot09.40.54

两种抽奖模式

CleanShot09.45.20

朋友圈点赞

类似于点赞、收藏等都可以使用redis的set集合来实现。

朋友圈点赞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 点赞 SADD like:{朋友圈消息ID} {用户ID}
localhost:1>SADD like:2989189 88491
"1"
localhost:1>SADD like:2989189 88492
"1"
localhost:1>SADD like:2989189 88493
"1"

// 取消点赞 SREM like:{朋友圈消息ID} {用户ID}
localhost:1>SREM like:2989189 88492
"1"

// 检查用户是否点过赞 SISMEMBER like:{朋友圈消息ID} {用户ID}
localhost:1>SISMEMBER like:2989189 88491
"1"

// 获取点赞用户列表 SADDSMEMBERSlike:{朋友圈消息ID}
// 注意:查看微信朋友圈时,如果你是发布者,你可以看到所有点赞人,如果你不是发布者,则只能看到你的好友的点赞信息,这个Set的高级应用 下面章节会说
localhost:1>SMEMBERS like:2989189
1) "88491"
2) "88492"
3) "88493"

// 获取点赞用户数 SCARD like:{朋友圈消息ID}
localhost:1>SCARD like:2989189
"3"
CleanShot110.13.45

集合操作

集合操作示意图

前置条件:创建set1、set2、set3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
localhost:1>SADD set1 a
"1"
localhost:1>SADD set1 b
"1"
localhost:1>SADD set1 c
"1"
localhost:1>SADD set2 b
"1"
localhost:1>SADD set2 c
"1"
localhost:1>SADD set2 d
"1"
localhost:1>SADD set3 c
"1"
localhost:1>SADD set3 d
"1"
localhost:1>SADD set3 e
"1"
交集
1
2
localhost:1>SINTER set1 set2 set3
1) "c"

如上图所示,取set1、set2、set3 三个集合中 共同的元素,这里共同的元素 是C。

并集(合集)

把set1、set2、set3 三个集合 合并后去重的结果就是并集。

1
2
3
4
5
6
7

localhost:1>SUNION set1 set2 set3
1) "c"
2) "e"
3) "a"
4) "d"
5) "b"
差集
1
2
localhost:1>SDIFF set1 set2 set3
1) "a"
差集示意图

以 set1为基准,求出 set1 与 (set2 和set3) 的差集,简单理解:

公式:set1 - { set2 + set3 并集 } –> {a,b,c} - {b,c,d,e} = 基于set1 (set2 和set3) 的差集是a

集合实现微博、微信关注模型

image-1
  • 通过集合也可以实现微信、微博的关注模型,但切记 这只是更多的是提供一种思路,不论什么实现方式只要涉及到了大数据量量和高并发,肯定有其他更优解,更优秀的优化方案。
微博关注模型

名词理解

共同关注的人:现在你查看的那个人也有关注你关注的人

  • 我关注了支付宝官微,支付宝官微也关注了我 关注的人

我关注的人也关注她:是我关注的人也关注了你当前查看的人

  • 我关注了其他人,其他人也关注了 支付宝官微
创建关注关系

关注名人:张柏芝(zbz)、王宝强(wbq)、周杰伦(zjl)、王杰(wj)、陈凯歌(ckg)

张柏芝关注:王百强、王杰

1
2
3
4
localhost:1>SADD zbzSet wbq
"1"
localhost:1>SADD zbzSet wj
"1"

陈冠希关注:张柏芝、王百强

1
2
3
4
localhost:1>SADD chenguanxiSet zbz
"1"
localhost:1>SADD chenguanxiSet wbq
"1"

陈奕迅关注:周杰伦、王杰

1
2
3
4
localhost:1>SADD chenyixun zjl
"1"
localhost:1>SADD chenyixun wj
"1"

陈小春关注:陈凯歌、张柏芝

1
2
3
4
localhost:1>SADD chenxiaochun ckg
"1"
localhost:1>SADD chenxiaochun zbz
"1"

查看 陈冠希与陈小春共同关注:

1
2
localhost:1>SINTER chenguanxiSet chenxiaochun
1) "zbz"

我(陈冠希)关注的人也关注他

  • 陈冠希此时在看(关注)的人是王宝强,查询 陈冠希关注的人里面,有没有也关注了王宝强的
1
2
localhost:1>SISMEMBER zbzSet wbq
"1"

可能认识的人(查询差集)

1
2
3
// 以 陈冠希为基准,查询陈冠希与 陈小春的差集
localhost:1>SDIFF chenguanxiSet chenxiaochun
1) "wbq"

差集实验动图

ZSet(有序集合)

  • zset是redis中比较常使用到的数据结构,zset既保证了数据唯一性(不可重复),也可以通过 value的score 权重来实现排序。该排序功能通过Skip List(跳跃列表)来实现的。

  • 当zset集合中最后一个元素被删除时,集合会自动删除,回收内存。

  • 利用ZSet的去重和有序的特性,一般用于做排名、排行榜,如 微博排行榜、学生成绩排行榜等。

常用操作

1
2
3
4
5
6
7
8
9
10
11
ZADD key score member [[score member]]	//往有序集合key中加入带分值元素
ZREM key member [member …] //从有序集合key中删除元素
ZSCORE key member //返回有序集合key中元素member的分值
ZINCRBY key increment member //为有序集合key中元素member的分值加上increment
ZCARD key //返回有序集合key中元素个数
ZRANGE key start stop [WITHSCORES] //正序(升序 从小到大)获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES] //倒序(降序 从大到小)获取有序集合key从start下标到stop下标的元素

Zset集合操作
ZUNIONSTORE destkey numkeys key [key ...] //并集计算
ZINTERSTORE destkey numkeys key [key …] //交集计算

ZSet实现微博排行榜

微博排行榜

我们可以使用ZSet实现微博排行榜(或其他排行榜),用户在每次搜索一次新闻,都会把该新闻作为key添加一次 记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 添加搜索新闻
ZINCRBY {key} score分值(排行次数) {value}

localhost:1>ZINCRBY hotNews:20211001 1 kaoyanguojiaxian
"5" // 考研国家线 5次访问
localhost:1>ZINCRBY hotNews:20211001 1 quanwangzuiyinghebanlongtaitou
"2" // 全网最硬核版龙抬头 2次访问
localhost:1>ZINCRBY hotNews:20211001 1 gongzuoqunfabuyaxinxi
"3" // 工作群发不雅信息 3次访问
localhost:1>ZINCRBY hotNews:20211001 1 kaoyanchengji
"1" // 考研成绩 1次访问

// 降序 展示当日排行前十
ZREVRANGE {key} {开始下标 - 结束下标} WITHSCORES (取出score分值)

localhost:1>ZREVRANGE hotNews:20211001 0 9 WITHSCORES
1) "kaoyanguojiaxian"
2) "5"
3) "gongzuoqunfabuyaxinxi"
4) "3"
5) "quanwangzuiyinghebanlongtaitou"
6) "2"
7) "kaoyanchengji"
8) "1"


// 展示三日搜索榜单计算
// 新建的集合名称,一般以日期范围为名 3 代表后面有3个set集合要合并交集 ,要合并的set集合
ZUNIONSTORE hotNews:20211001-hotNews:20211003 7 hotNews:20211001 hotNews:20211002 hotNews:20211003

// 通过zset 把我们需要查询的3日搜索记录进行并集
localhost:1>ZUNIONSTORE hotNews:20211001-hotNews:20211003 3 hotNews:20211001 hotNews:20211002 hotNews:20211003
"12"

// 展示三日排行前十
ZREVRANGE hotNews:20211001-hotNews:20211003 0 9 WITHSCORES

localhost:1>ZREVRANGE hotNews:20211001-hotNews:20211003 0 9 WITHSCORES
1) "kaoyanguojiaxian"
2) "5"
3) "gongzuoqunfabuyaxinxi"
4) "3"
5) "quanwangzuiyinghebanlongtaitou"
6) "2"
7) "d"
8) "2"
9) "kaoyanchengji"
10) "1"
11) "h"
12) "1"
13) "g"
14) "1"
15) "f"
16) "1"
17) "e"
18) "1"
19) "c"
20) "1"

image-1

Redis单线程

redis真的是单线程吗

redis并不是完全意义上的单线程,具体的看是客户端读写请求还是其他功能,如果是客户端读写则是单线程,其他功能有额外线程。

redis的单线程主要指的是 客户端发起的请求命令(键值对读写)是由一个线程完成的,但redis的其他功能,如 持久化、异步删除、集群数据同步都是有其他额外线程执行。

redis单线程执行效率快的原因

  1. redis 数据都存储在内存中且数据运算都是在内存中进行,相比较MySQL 在磁盘中速度快得多。
    • redis持久化肯定不是实时存储到硬盘,而是有一个时间周期,否则redis的性能会急剧下降
  2. 多线程切换存在 线程损耗的问题, 单线程就完美的避免了线程损耗,但对一些比较耗时的命令,需要慎重使用,避免线程卡顿

redis单线程如何处理并发客户端

redis单线程的情况下还可以处理那么多的客户端并发请求,只要靠的是 IO多路复用机制。

redis执行逻辑

IO多路复用机制

  • redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。

用人类的语言(说人话)说就是,redis对客户端的处理毋庸置疑是单线程模型,只是他会把多个请求同时放在一个队列中,然后放到事件分派器中,让分排期进行任务调度。

  1. S0 S1 S2 S3客户端同时发起读写请求,redis把他们都存入队列中发送给分派器
  2. 根据队列先进先出原则,S0会先执行,在执行的过程中肯定有一定的IO等待时间(哪怕很短),那么分派器在等待的过程中会让队列中下一个客户端请求 如 S1 先去执行他的命令。

redis 简易单机压测

我们可通过redis自带的一个小工具,进行简易的 get、set 压测,看看当前单机的一个QPS吞吐量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cd /www/server/redis/src

// 测试当前机器的get 读取吞吐量
./redis-benchmark -t get

Summary:
// 1秒钟可读取 123304条数据
throughput summary: 123304.56 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.226 0.056 0.215 0.287 0.439 14.471

// 测试当前机器的set 写入吞吐量
./redis-benchmark -t set
Summary:
// 1秒钟可写入 129870条数据
throughput summary: 129870.13 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.215 0.040 0.215 0.295 0.375 0.919

redis支持的最大连接数

在redis.conf文件中可修改(不建议修改),# maxclients 10000

1
2
3
localhost:1>CONFIG GET maxclients
1) "maxclients"
2) "10000"

redis另类高级指令

keys 全量遍历

类似于mysql的findAll,一次性遍历出redis所有的key(不含value),当redis数据量较大时,性能会很差,在生产环境谨慎使用。

1
2
3
4
5
localhost:1>keys *
1) "hotNews:20211103"
2) "hotNews:20211101-hotNews:20211103"
3) "hotNews:20211102"
4) "hotNews:20211101"

scan 渐进式遍历

在大数据量的情况下,一下子遍历出所有的key会非常消耗CPU和内存资源,这明显是非常不理智的行为,为此 redis准备了另外一个命令 scan 渐进式遍历,他可以慢慢的 遍历出 整个redis的key。

scan缺点

  • 在使用scan遍历的过程中有数据变化(增加、删除、修改),很可能出现数据不精确(或不完整)的情况。
    • 在scan遍历过程中,新增的键如果被分配到了 遍历过的桶上面,redis不会在从头遍历,结果集就没有新增的键

redis存储结构

redis内部结构类似于哈希结构,存储一个数据时,会根据key 进行哈希算法计算,计算出cursor 浮标(哈希桶的位置),找到cursor浮标位置,存储在浮标的哈希桶中,同一个哈希桶的多个数据则由链表结构进行维护。

redis存储结构

上面提到的scan缺点,如果我们在使用scan开始遍历时有数据变化,如 新增数据(分配到了第一个桶),可是此时 如果已经遍历到了第四个哈希桶,则不会再回头去遍历,这样就会丢失掉部分数据。

image-2

scan命令

scan 0 match test* count 3 –> scan {cursor} match {param} count 3
cursor(浮标、光标)是哈希桶的索引值,param是要查询的值,count后面是遍历参考值(只是大概,这里为3,大概获取3条数据),类似于分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
localhost:1>SADD test1 1
"1"
localhost:1>SADD test2 1
"1"
localhost:1>SADD test3 1
"1"
localhost:1>SADD test4 1
"1"
localhost:1>SADD test5 1
"1"
localhost:1>SADD test6 1
"1"
localhost:1>SADD test7 1
"1"
localhost:1>SADD test8 1
"1"

// scan命令:scan 0 match test* count 3 // scan {cursor} match {param} count 3
// cursor(浮标、光标)是哈希桶的索引值,param是要查询的值,count后面是遍历参考值(只是大概 这里为3,大概获取3条数据),类似于分页
localhost:1>scan 0 match test* count 3
1) "1" //下次查询的浮标
2) 1) "test6"
2) "test2"
3) "test3"


// 下次查询的话,cursor 就要使用 上次查询返回的浮标,所以这里cursor就是1
localhost:1>scan 1 match test* count 3
1) "5" //下次查询的浮标
2) 1) "test7"
2) "test1"
3) "test5"

// 下次查询的话,cursor 就要使用 上次查询返回的浮标,所以这里cursor就是5
localhost:1>scan 5 match test* count 3
1) "0" // 浮标为0,代表redis中已经没有其他数据了,可以结束遍历了
2) 1) "test8"
2) "test4"