# Redis的消息订阅、pipeline、事务、modules、布隆过滤器、缓存LRU

# Redis的进阶使用

# Redis的管道pipeline 减少IO (opens new window)

# nc 就是netcat 使用yum install -y nc
[root@redis-01 ~]# nc localhost 6379   # 与redis建立通讯可以发送指令
keys *
*0
set k1 hello
+OK
^C
[root@redis-01 ~]# echo -e "sdfsdf\nsdfsf"   # echo -e 可以将\n识别为换行
sdfsdf
sdfsf

# echo 结合管道 达到 多个命令一次性给到redis去执行 减少通讯成本
[root@redis-01 ~]# echo -e "set k2 99\nincr k2\n get k2" | nc localhost 6379
+OK
:100   # 返回100
$3     # 宽度
100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

延伸:大量插入大量数据 冷加载 Redis 大量数据插入 (opens new window)

# Redis的发布订阅Pub/Sub

# 场景就是聊天室
127.0.0.1:6379> help @pubsub

  PSUBSCRIBE pattern [pattern ...]
  summary: Listen for messages published to channels matching the given patterns
  # 侦听发布到与给定模式匹配的通道的消息
  since: 2.0.0

  PUBLISH channel message
  summary: Post a message to a channel
  # 发布一条消息到频道
  since: 2.0.0

  PUBSUB subcommand [argument [argument ...]]
  summary: Inspect the state of the Pub/Sub subsystem
  # 检查Pub/Sub的状态 
  since: 2.8.0

  PUNSUBSCRIBE [pattern [pattern ...]]
  summary: Stop listening for messages posted to channels matching the given patterns
  # 停止发布到匹配给定模式的渠道的消息听
  since: 2.0.0

  SUBSCRIBE channel [channel ...]
  summary: Listen for messages published to the given channels
  # 监听不同的频道发布的消息  类似广播的行为
  since: 2.0.0

  UNSUBSCRIBE [channel [channel ...]]
  summary: Stop listening for messages posted to the given channels
  # 停止频道监听
  since: 2.0.0

# 演示  新开两个xshell窗口 连接上redis-cli 以下分为a,b
# a
127.0.0.1:6379> SUBSCRIBE yyc    # 监听yyc频道发布的消息
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "yyc"
3) (integer) 1
# b
127.0.0.1:6379> PUBLISH yyc hello  # 发布一条消息到yyc频道
(integer) 1
# a
127.0.0.1:6379> SUBSCRIBE yyc
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "yyc"
3) (integer) 1
1) "message"     # 收到发布的消息了
2) "yyc"
3) "hello"

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

延伸:想一下微信的功能,它不仅能实时接收信息,往上滑还能看到历史的信息

下面就简单梳理一下聊天室的设计存储架构,只使用Redis及数据库

聊天室的设计存储架构

  1. 一般缓存是为了解决用户读请求的,目的就是提速,写请求会有双写问题,数据一致性等问题

  2. 用户读请求分为实时性与历史性的,查看实时的就是通过Redis的发布/订阅实现就行;查看短期时间的数据,可以通过Redis的有序集合按照时间显示数据;查看历史的聊天记录,就是根据时间段查询数据库

  3. 用户写请求,数据写入频道(发布/订阅)中;数据写入有序集合中;数据写入kafka(消息队列中间件),数据库再与kafka通过一定的速率消费数据

    聊天室的设计细节梳理

# Redis的事务

# Redis的事务 不像MySQL的那么完整 它追求的是速度 其实它并没有所谓回滚的这件事
127.0.0.1:6379> help @transactions

  DISCARD -
  summary: Discard all commands issued after MULTI
  # 丢弃所有 MULTI 之后发的命令
  since: 2.0.0

  EXEC -
  summary: Execute all commands issued after MULTI
  # 执行所有 MULTI 之后发的命令
  since: 1.2.0

  MULTI -
  summary: Mark the start of a transaction block
  # 标记一个事务块开始
  since: 1.2.0

  UNWATCH -
  summary: Forget about all watched keys
  # 取消事务命令
  since: 2.2.0

  WATCH key [key ...]
  summary: Watch the given keys to determine execution of the MULTI/EXEC block
  # 锁定key直到执行了 MULTI/EXEC 命令  通过乐观锁(optimistic lock)实现 CAS (check-and-set)操作
  since: 2.2.0
# 演示 简单事务
# 准备两个客端端(就是xshell开两个窗口)client1,client2
# client1
127.0.0.1:6379> MULTI    # 标记事务开始
OK
127.0.0.1:6379> set k1 aaaa  # 命令放入缓冲队列,并不执行
QUEUED
127.0.0.1:6379> set k2 bbbb  # 命令放入缓冲队列,并不执行
QUEUED
127.0.0.1:6379> exec    # 执行exec时,将队列中的命令一次执行
1) OK
2) OK


127.0.0.1:6379> MULTI    # client1事务开始
OK
127.0.0.1:6379> get k1
QUEUED
# 如果是clint2先执行exec,clint1后执行,那么就查不到k1,因为再执行之前k1已经被删
127.0.0.1:6379> exec
1) (nil)

# client2
127.0.0.1:6379> MULTI    # client2事务开始
OK
127.0.0.1:6379> del k1
QUEUED
# 如果是clint2先执行exec
127.0.0.1:6379> exec
1) (integer) 1
# client1、client2谁先执行exec,就先执行队列中的命令

# 演示 WATCH
# client1
127.0.0.1:6379> WATCH k1  # 服务端会将这个连接中监控这个k1的变化
OK
127.0.0.1:6379> MULTI    # client1事务开始
OK
127.0.0.1:6379> get k1 
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec     # 比client2晚执行 这时k1发生变化,所以不执行此事务
(nil)

# client2
127.0.0.1:6379> MULTI    # client2事务开始
OK
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> set k1 yyc 
QUEUED
127.0.0.1:6379> exec   # client2更改k1的值
1) 1) "k1"
2) OK
127.0.0.1:6379> get k1
"yyc"

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

讲解图

Redis事务

# Redis的布隆过滤器

Redis模块 (opens new window)

布隆过滤器模块GitHub (opens new window)

安装布隆过滤器模块

# 访问redis.io
# modules
# 访问RedisBloom的github https://github.com/RedisBloom/RedisBloom/releases/tag/v2.2.4
wget  RedisBloom-2.2.4.zip        # 下载zip包
yum install unzip  # 安装unzip 解压zip包工具
unzip RedisBloom-2.2.4.zip        # 解压
cd RedisBloom-2.2.4               # 进入目录
make                               # 编译
cp redisbloom.so /opt/yyc/redis5/   # 复制扩展库文件到指定目录
cd /etc/redis/
# 启动redis服务加载布隆过滤器模块
redis-server --loadmodule /opt/yyc/redis5/redisbloom.so --port 6381  
# 再新建个窗口
redis-cli -p 6381  # 连接redis
127.0.0.1:6381> help BF.ADD

  BF.ADD key ...options...
  summary: Help not available
  since: not known
  group: generic

127.0.0.1:6381> bf.add ooxx abc        # 加入
(integer) 1     
127.0.0.1:6381> bf.exists ooxx  ksdf   # 是否存在
(integer) 0
127.0.0.1:6381> bf.exists ooxx abc     # 是否存在
(integer) 1
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

Redis布隆过滤器模块,防止穿透

缓存穿透:访问一些不存在的缓存数据,让缓存失效,请求压力到达数据库这边

​ 如何解决?将网站的所有的数据放入集合,让请求数据与集合进行匹配,若匹配上了,则访问数据库,没有匹配上就返回没有,让数据库每次访问是有效的。那么布隆过滤器就是利用小的空间来做数据与集合匹配的功能,类黑名单功能。

1,发生穿透,而且该数据不存在数据库中

2,客户端,增加redis中的key,value标记这个key是不存在的

3,若是别的线程,调用数据库增加了元素

4,那么必须要完成元素对bloom的添加

防止穿透流程图:

防止穿透流程图

布隆过滤器概率解决问题,不可能百分百阻挡,一定有失误率

1,将网站数据准备好

2,向bitmap中标记

3,请求数据可能被误标记

4,但是,一定概率会大量减少放行:穿透

5,而且,空间成本低

关于布隆过滤器更多原理,可以看算法体系班中哈希函数结构那一节。

# Redis作为数据库/缓存的区别

Redis事务(1)

Redis作为缓存:

缓存:

1、不重要:既然作为缓存,那么相较于数据库中的数据,缓存显得不重要,可以删除,反正还有数据库最后一层拖着底。

2、不是全量数据,缓存应该随着访问而变化,即热数据

要求:

Redis里的数据怎么能随着业务变化,只保留热数据,因为内存大小是有限的,容量是瓶颈

缓存淘汰的解决方案:

1、业务逻辑解决:利用key的有效期。人来决定

1,会随着访问延长?不对!!
2,发生写,会剔除过期时间
3,倒计时,且,redis不能延长
4,定时
5,业务逻辑自己补全
# 演示 设置有效期
127.0.0.1:6379> set k1 aaaa ex 20   # 设置有效期20秒
OK
127.0.0.1:6379> ttl k1              # 查看k1的有效期剩余
(integer) 17
127.0.0.1:6379> ttl k1
(integer) 16
127.0.0.1:6379> ttl k1
(integer) 15
127.0.0.1:6379> ttl k1
(integer) 14
127.0.0.1:6379> ttl k1
(integer) 13
127.0.0.1:6379> ttl k1
(integer) 12
127.0.0.1:6379> get k1             # 获取k1的值
"aaaa"
127.0.0.1:6379> ttl k1             # 查看k1的有效期剩余,不会延长有效期
(integer) 6
127.0.0.1:6379> ttl k1
(integer) 5
127.0.0.1:6379> get k1             # 过了20秒,再次获取,则获取不到
(nil)
127.0.0.1:6379> ttl k1             # 查看k1的有效期为-2,过期了
(integer) -2
127.0.0.1:6379> set k1 aaa         # 设置
OK
127.0.0.1:6379> ttl k1             # 查看k1的有效期为-1,永久
(integer) -1
127.0.0.1:6379> get k1
"aaa"
127.0.0.1:6379> EXPIRE k1 50       # 设置有效期为50秒
(integer) 1
127.0.0.1:6379> ttl k1
(integer) 47
127.0.0.1:6379> ttl k1
(integer) 46
127.0.0.1:6379> ttl k1
(integer) 45
127.0.0.1:6379> set k1 bbb         # 再次设置k1的值为bbb
OK
127.0.0.1:6379> ttl k1             # 发生写,剔除过期时间
(integer) -1
127.0.0.1:6379> EXPIREAT k1 1629687600  # 定时删除key,Unix timestamp转时间https://tool.lu/timestamp/
(integer) 1
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

2、业务运转解决:因为内存是有限的,随着访问的变化,应该淘汰掉冷数据。算法决定,机器决定

# 修改redis配置文件 
vim /etc/redis/6379.conf
# 设置内存大小  最好控制在1~10G
maxmemory <bytes>
# 设置内存回收策略  LFU - 碰了多少次    LRU - 多久没碰他
maxmemory-policy noeviction
1
2
3
4
5
6

# Redis如何淘汰过期的keys

Redis keys过期有两种方式:被动和主动方式。

当一些客户端尝试访问它时,key会被发现并主动的过期。

当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。

具体就是Redis每秒10次做的事情:

  1. 测试随机的20个keys进行相关过期检测。
  2. 删除所有已经过期的keys。
  3. 如果有多于25%的keys过期,重复步奏1.

这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。

过期判定原理:

1、被动访问时判定

2、周期轮询判定(增量)

目的,稍微牺牲下内存,但是保住了redis性能为王!!!!

# Redis的缓存LRU

# 将redis当做使用LRU算法的缓存来使用

当Redis被当做缓存来使用,当你新增数据时,让它自动地回收旧数据是件很方便的事情。这个行为在开发者社区非常有名,因为它是流行的memcached系统的默认行为。

LRU是Redis唯一支持的回收方法。本页面包括一些常规话题,Redis的maxmemory指令用于将可用内存限制成一个固定大小,还包括了Redis使用的LRU算法,这个实际上只是近似的LRU。

# Maxmemory配置指令

maxmemory配置指令用于配置Redis存储数据时指定限制的内存大小。通过redis.conf可以设置该指令,或者之后使用CONFIG SET命令来进行运行时配置。

例如为了配置内存限制为100mb,以下的指令可以放在redis.conf文件中。

# 设置内存大小  最好控制在1~10G
maxmemory 100mb
1
2

设置maxmemory为0代表没有内存限制。对于64位的系统这是个默认值,对于32位的系统默认内存限制为3GB。

当指定的内存限制大小达到时,需要选择不同的行为,也就是策略。 Redis可以仅仅对命令返回错误,这将使得内存被使用得更多,或者回收一些旧的数据来使得添加数据时可以避免内存限制。

# 回收策略

当maxmemory限制达到的时候Redis会使用的行为由 Redis的maxmemory-policy配置指令来进行配置。

以下的策略是可用的:

  • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
  • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
  • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。

选择正确的回收策略是非常重要的,这取决于你的应用的访问模式,不过你可以在运行时进行相关的策略调整,并且监控缓存命中率和没命中的次数,通过RedisINFO命令输出以便调优。

一般的经验规则:

  • 使用allkeys-lru策略:当你希望你的请求符合一个幂定律分布,也就是说,你希望部分的子集元素将比其它其它元素被访问的更多。如果你不确定选择什么,这是个很好的选择。.
  • 使用allkeys-random:如果你是循环访问,所有的键被连续的扫描,或者你希望请求分布正常(所有元素被访问的概率都差不多)。
  • 使用volatile-ttl:如果你想要通过创建缓存对象时设置TTL值,来决定哪些对象应该被过期。

allkeys-lruvolatile-random策略对于当你想要单一的实例实现缓存及持久化一些键时很有用。不过一般运行两个实例是解决这个问题的更好方法。

为了键设置过期时间也是需要消耗内存的,所以使用allkeys-lru这种策略更加高效,因为没有必要为键取设置过期时间当内存有压力时。

# 回收进程如何工作

理解回收进程如何工作是非常重要的:

  • 一个客户端运行了新的命令,添加了新的数据。
  • Redi检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
  • 一个新的命令被执行,等等。
  • 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

# 近似LRU算法

Redis的LRU算法并非完整的实现。这意味着Redis并没办法选择最佳候选来进行回收,也就是最久未被访问的键。相反它会尝试运行一个近似LRU的算法,通过对少量keys进行取样,然后回收其中一个最好的key(被访问时间较早的)。

不过从Redis 3.0算法已经改进为回收键的候选池子。这改善了算法的性能,使得更加近似真是的LRU算法的行为。

Redis LRU有个很重要的点,你通过调整每次回收时检查的采样数量,以实现调整算法的精度。这个参数可以通过以下的配置指令调整:

maxmemory-samples 5
1

Redis为什么不使用真实的LRU实现是因为这需要太多的内存。不过近似的LRU算法对于应用而言应该是等价的。使用真实的LRU算法与近似的算法可以通过下面的图像对比。

LRU comparison

用于生成图像的Redis服务被填充了指定数量的键。这些键将被从头到尾访问,所以当使用LRU算法时第一个键是最佳的回收候选键。接着添加超过50%的键,用于强制旧键被回收。

你可以看到三种点在图片中, 形成了三种带.

  • 浅灰色带是已经被回收的对象。
  • 灰色带是没有被回收的对象。
  • 绿色带是被添加的对象。
  • 在LRU实现的理论中,我们希望的是,在旧键中的第一半将会过期。Redis的LRU算法则是概率的过期旧的键。

你可以看到,在都是五个采样的时候Redis 3.0比Redis 2.8要好,Redis2.8中在最后一次访问之间的大多数的对象依然保留着。使用10个采样大小的Redis 3.0的近似值已经非常接近理论的性能。

注意LRU只是个预测键将如何被访问的模型。另外,如果你的数据访问模式非常接近幂定律,大部分的访问将集中在一个键的集合中,LRU的近似算法将处理得很好。

在模拟实验的过程中,我们发现如果使用幂定律的访问模式,则真实的LRU算法和近似的Redis算法几乎没有差别。

当然你可以提升采样大小到10,消耗更多的CPU时间以实现更真实的LRU算法,同时查看下是否让你的缓存命中率有差别。

通过CONFIG SET maxmemory-samples 命令在生产环境上设置不同的采样大小是非常简单的。