1.发布订阅模式
1.1列表的局限
前面我们说通过队列的rpush和blpop可以实现消息队列(队尾进队列出),没有任何元素可以弹出的时候,连接会被阻塞。
但是基于list实现的消息队列,不支持一对多的消息分发,相当于只有一个消费者。如果要实现一对多的消息分发,怎么办?
1.2发布订阅模式
除了通过list实现消息队列外,redis还提供了发布订阅的功能。
-
订阅频道
消息的生产者和消费者是不同的客户端,连接到同一个redis的服务。通过什么对象把生产者和消费者关联起来呢?在RabbitMQ里面叫做Queue,在kafka里面叫做topic。Redis的模型里面叫channel(频道)。订阅者可以订阅一个或者多个channel。消息的发布者可以给指定的channel发布消息。只要有消息到达了channel,所有订阅了这个channel的订阅者都会收到这条消息。
订阅者订阅频道:可以一次订阅多个,比如这个客户端就订阅了3个频道,频道不要声明创建
subscribe channel-1 channel-2 channel-3
发布者可以向指定频道发布消息(并不支持一次向多个频道发送消息)
publish channel-1 2673
取消订阅
unsubscribe channel-1
按照规则(pattern)订阅频道:支持?和*占位符(?代表一个字符,*代表0个或者多个字符)。
# 消费端1 关注运动信息
psubscribe *sport
# 消费端2,关注所有新闻
psubscribe news*
# 消费端3,关注天气新闻
psubscribe news-weather
# 生产者向3个频道发送3条消息,对应的订阅者能收到消息
publish news-port kobe
publish news-misic jaychou
publish news-weather sunny
2.Redis事务
2.1 为什么要用事务
Redis的单个命令是原子性的(比如get set mget mset),要么成功要么失败,不存在并发干扰的问题。
如果涉及到多个命令的时候,需要把多个命令作为一个不可分割的处理序列,就必须要依赖redis的功能特性来实现了。
Redis提供了事务的功能,可以把一组命令一起执行。Redis的事务有3个特点:
- 按进入队列的顺序执行。
- 不会受到其它客户端的请求影响。
- 事务不能嵌套,多个multi命令效果一样。
2.2事务的用法
redis的事务涉及到四个命令:multi(开启事务)、exec(执行事务)、discard(取消事务)、watch(监视)
set tom 1000
set mic 1000
multi
decrby tom 100
incrby mic 100
exec
get tom
get mic
通过multi的命令开启事务。multi执行后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即执行,而是被放在一个队列中。当exec命令被调用后,所有队列的命令才会被执行。
如果中途不想执行事务了,可以通过discard取消事务。
2.3事务可能遇到的问题
事务执行遇到的问题分成两种,一种是在执行exec之前发生错误,一种是在执行exec之后发生错误。
2.3.1在执行exec之前发生错误
比如:入队的命令存在语法错误,包括参数数量、参数名等,事务会拒绝执行,也就是队列中的所有命令都不会得到执行。
2.3.2在执行exec之后发生错误
比如对String使用了hash 的命令,参数个数正确,但数据类型错误,这是一种运行时错误。
flushall
multi
set k1 1
hset k1 a b
exec
1)OK
2)(error)WRONGTYPE Operation against a key holding the wrong kind of value.
最后发现set k1 1的命令是成功的,也就是在这种发生了运行时异常的情况下,只有错误的命令没有被执行,但是其它命令没有收到影响。
这个显然不符合我们对原子性的定义。也就是我们没办法用redis的这种事务机制来实现原子性,保证数据一致性。
Redis从2.6版本开始引入了lua脚本,也就是说Redis可以用Lua来执行Redis命令来保证原子性。
3.Lua脚本
lua脚本是一种轻量级脚本语言,它是用C语言编写的,跟数据库的存储过程有点类似。
使用lua脚本来执行redis的好处:
- 一次发送多个命令,减少连接和网络开销
- redis会将整个脚本作为一个整体执行,不会被其它请求打断,保证了原子性。
- 对于复杂的组合命令,我们可以放在文件中,可以实现命令复用
3.1在Redis中调用lua脚本
使用eval方法,语法格式如下:
redis>eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3...]
- eval :代表执行的是lua语言的命令
- lua-script:代表lua语言脚本内容
- key-num:代表参数中有多少个key,需要注意的是redis中的key是从1开始的,如果没有key的参数,那么写0
- [key1 key2 key3…]:是key作为参数传递给lua语言,也可以不填,但需要和key-num的个数对应起来
- [value1 value2 value3…]:这些参数传递给lua语言,它们是可填可不填的
redis> eval "return 'hello world '" 0
3.2在lua脚本中调用redis命令
3.2.1 命令格式
使用redis.call(command,key [param1,param2,param3])进行操作。语法格式:
redis.call(command,key [param1,param2,parma3...])
- command 是命令,包括set、get、del等。
- key是被操作的键
- param1、param2…代表给key的参数
redis>eval "reutrn redis.call('set','qingshan','2673')" 0
redis>eval "return redis.call('set',keys[1],ARGV[1])" 1 qingshan miaomiaomiao
在redis中直接写lua脚本不够方便,也不能实现编辑和复用,通常我们把lua脚本放在文件中,然后执行这个文件。
创建lua脚本文件:
cd /usr/local/soft/redis-6.0.9/src
vi gupao.lua
redis.call('set','qingshan','lua666')
return redis.call('get','qingshan')
调用脚本文件
cd /usr/local/soft/redis-6.0.9/src
redis-cli --eval gupao.lua 0
4.Redis为什么这么快?
使用redis自带的benchmark脚本测试:
cd /usr/local/soft/redis-6.0.9/src
redis-benchmark -t set,lpush -n 1000000 -q
结果:
set:136239.78 request per record ---- 每秒钟处理13W多次set请求
lpush:132626.00 request per record ---- 每秒钟处理13W多次lpush请求
Redis为什么那么快?总结起来主要是3点:
- 存内存数据结构
- 请求处理单线程
- 多路复用机制
4.1内存
KV结构的内存数据库,时间复杂度O(1)
4.2单线程
这里说的单线程其实指的是处理客户端的请求是单线程的,可以把它叫做主线程。从4.0版本之后,还引入了一些线程处理其他的事情,比如清理脏数据、无用连接的释放、过期数据。
把处理请求的主线程设置成单线程有什么好处
- 没有创建线程、销毁线程带来的开销
- 避免了线程上下文切换导致的CPU消耗
- 避免了线程之间带来的竞争问题,例如加锁释放锁死锁等
这里有一个问题,就算单线程确实有你说的好处,但是会不会白白浪费了CPU的资源吗?也就是说只能用到单核。
官方的解释是这样的:在redis中单线程已经够用了,CPU不是redis的瓶颈。Redis的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,又不需要处理线程并发的问题,那就顺理成章地采用了单线程的方案了。
4.3 I/O多路复用(同步非阻塞IO)
I/O指的是网络I/O
多路指的是多个TCP连接(Socket或Channel)
复用指的是复用一个或多个线程
用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。这样用户可以注册多个socket,然后不断地调用select读取被激活的socket,redis服务端将这些socke置于队列中,然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中,提高读取效率。
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作,从而提高效率。
5.内存回收
5.1过期策略
要实现key过期,我们有几种思路
5.1.1立即过期(主动淘汰)
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即删除。该策略可以立即清楚过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
5.1.2惰性过期(被动淘汰)
只有访问一个key时,才会判断该key是否过期,过期则清楚。该策略可以最大化地节省CPU资源,缺对内存非常不友好。极端情况下可能会出现大量的过期key没有再次被访问,从而不会被消除,占用大量内存。
5.1.3定期过期
每隔一段时间,会扫描一定数量的数据库expires字典中一定数量的key,并清除其中已过期的key。该策略是前2者的折中方案
总结:redis中同时采用了惰性过期和定期过期两种策略,并不是实时地清除过期的key。如果所有的key都没有设置过期属性,redis满了怎么办?
5.2淘汰策略
Redis的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的写入。
5.2.1最大内存设置
redis.conf参数设置
# maxmemory <bytes>
如果不设置 maxmemory 或者设置唯0,32位系统最多使用3G内存,64位系统不限制内存。
动态修改(先get一下):
redis> config set maxmemory 2GB
到达最大内存以后怎么办?
5.2.2淘汰策略
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# maxmemory-policy noeviction
volatile是针对这是了ttl的key,allkeys是针对所有key。
LRU,Least Recently Used:最近最少使用。判断最近被使用的时间,目前最远的数据优先淘汰。
LFU,Least Frequently Used:最不常用。按照使用频率删除,4.0版本新增。
random:随机删除
volatile-ttl:根据键值的ttl属性,删除最近要过期的数据。如果没有,回退到noeviction策略。
noeviction:默认策略,不会删除任何数据,拒绝所有写入操作,并返回客户端错误信息,此时redis只响应读操作。
动态修改淘汰策略(先get一下):
redis> config set maxmemory-policy volatile-lru
6.持久化机制
Redis速度快,很大一部分原因是因为它所有数据都存储在内存中。如果断电或者宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis提供了两种持久化方案,一种是RDB快照(Redis DataBase),一种是AOF(Append Only File)。持久化是redis跟memcache的主要区别之一。
6.1RDB(Redis DataBase)
RDB是Redis默认的持久化方案(注意如果开启了AOF,优先用AOF)。当满足一定条件的时候,会将当前内存中的数据写入磁盘,生成一个快照dump.rdb。Redis重启会通过dump.rdb文件恢复数据。
什么时候写入rdb文件呢?
6.1.1RDB触发
- 自动触发
- 配置规则触发
redis.conf ,SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。
如果不需要rdb方案,注释save或者配置成空字符串""。
save 900 1 # 900秒内至少有一个key被修改(包括添加)
save 300 10 # 300秒内至少有10个key被修改
save 60 10000 # 60秒内至少有10000个key被修改
注意上面的配置是不冲突的,只要满足任意一个都会触发。
rdb文件位置和目录(默认在安装根目录下)
# 文件位置
dir ./
#文件名称
dbfilename dump.rdb
# 是否以LZF压缩rdb文件
rdbcompression yes
- shutdown触发,保证服务器正常关闭
- flushall,清空rdb文件
- 手动触发
如果我们需要重启服务或者迁移数据,这个时候就需要手动触发RDB快照保存。redis提供了两条命令
- save
save在生成快照的时候会阻塞当前redis服务器,redis不能处理其他命令。如果内存中的数据比较多,会造成redis长时间阻塞。生产环境不要使用这个命令。 - bgsave
执行bgsave时,redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
6.1.2 RDB数据恢复(演示)
添加兼职
set k1 1
set k2 2
set k3 3
set k4 4
set k5 5
停服务器,触发save
redis>shutdown
备份dump.rdb
cp dump.rdb dump.rdb.bak
启动服务器,发现数据还在
keys *
模拟数据丢失,触发save,此时dump.rdb清空了
redis>flushall
停服务器
redis>shutdown
重命名备份文件
mv dump.rdb.bak dump.rdb
启动服务器,发现数据恢复。
6.1.3 RDB文件的优势和劣势
- 优势
- RDB是一个非常紧凑(compact)的文件,它保存了redis在某个时间点上的数据集。这种文件非常适合用于备份和灾难恢复。
- 生成RDB文件的时候,redis主线程会fork()一个子线程来处理所有保存工作,主线程不需要进行任何磁盘IO操作
- RDB在恢复大数据集时的速度比AOF的恢复速度要快。
- 劣势
- RDB方式数据没办法做到实时持久化/秒级持久化。
- 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照之后修改的数据。
如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化。
6.2 AOF(Append Only File)
AOF默认不开启。AOF采用日志的形式来记录每个写操作,并追加到文件中。执行更改redis的命令时,就会将命令写入AOF文件中(类比mysql的bin-log)。
redis重启时,会根据日志文件的内容把指令从前到后执行一次已完成数据的恢复工作。
6.2.1AOF配置
配置redis.conf
# 开关
appendonly no
# 文件名
appendfilename "appendonly.aof"
问题:数据都是实时持久化到磁盘吗?
由于操作系统的缓存机制,AOF数据并没有真正地写入磁盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写到AOF文件?
appendfsync everysec #AOF持久化策略(硬盘缓存到磁盘)
appendfsync 策略:
- no:表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全。
- always:表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低;
- everysec:表示每秒执行一次fsync,可能会丢失这1秒的数据。通常选用everysec,兼顾效率和安全性。
问题:文件越来越大,怎么办?
由于AOF持久化是Redis不断将写命令记录到AOF文件中,随着Redis不断的进行,AOF文件会越来越大,占用服务器内存越大以及AOF恢复要求时间越长。
例如,计数器增加100W次,100W个命令都记录进去了,但是结果只有1个。
为了解决这个问题,redis增加了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容进行压缩,只保留可以恢复数据的最小指令集。
可以使用bgrewriteof来重写。
AOF文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对。然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。
# 重写触发机制
auto-aof-rewrite-percentage 100 #默认值100,当前aof超过上一次重写的aof文件大小的百分之多少进行重写
auto-aof-rewrite-size 64mb #设置允许重写的最小aof文件大小
问题:重写过程中,AOF文件被更改了怎么办?
6.2.2 AOF数据恢复
重启redis之后就会进行AOF文件的恢复
6.2.3 AOF优势与劣势
- 优势
- AOF持久化的方法提供了很多种同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也就丢失1秒的数据而已。
- 劣势
- 对于具有相同数据的redis。AOF文件通常比RDF文件体积更大(RDB存的是数据快照)。
- 虽然AOF提供了很多同步的频率,默认情况下,每秒同步一次的频率也有较高的性能。在高并发的情况下,RDB比AOF具有更好的性能保证。
6.3 两种方案比较
对于AOF和RDB两种持久化方式,我们应该如何选择呢?
如果可以忍受一小段时间内数据的丢失,毫无疑问使用RDB是最好的,定时生成RDB快照(snapshot)非常便于进行数据备份,并且RDB恢复数据集的速度比AOF恢复的速度快。
否则就使用AOF重写。