消息的生产者和消费者是不同的客户端,在Redis中通过channel(频道)模型进行关联。订阅者可以订阅多个channel,消息的发布者可以给指定的channel发布消息,只要有消息到达了channnel,所有订阅了这个channel的订阅者都会收到这条消息。
subscribe channel-1 channel-2 channel-3 //一次订阅多个频道 publish channel-1 2673 //发布者可以向指定频道发布消息(并不支持一次向多个频道发送消息,可以在业务代码中添加多条) unsubscribe channel-1 //取消订阅(不能在订阅状态下使用)
按规则(Pattern)订阅频道:
支持 ?和 * 占位符。?代表一个字符,* 代表0个或者多个字符。
psubscribe *sport //适配所有以sport结尾的频道名称
一般来说,考虑到性能和持久化因素,不建议使用Redis的发布订阅功能来实现MQ。Redis的一些内部机制用到了发布订阅功能。
Redis单个命令是原子性的,但是为了确保多个命令作为一个不可分割的处理序列,就需要使用Redis事务。Redis事务具有三个特点:
multi //开启事务 exec //执行事务,在exec没有被调用时,所有队列中的命令都不会被执行 discard //取消事务,清空任务队列,放弃执行 watch //监视
为了防止事务过程中某个key的值被其他客户端请求修改,带来非预期的结果,在Redis中就提供了一个watch命令。用于多个客户端更新变量的时候,跟原值作比较,只有它没有被其他线程修改的情况下,才更新为新的值。它可以为Redis事务提供CAS乐观锁行为。
可以使用watch命令监视一个或者多个key,如果事务开启之后,至少有一个被监视的key在exec执行之前被修改了,那么整个事务都会被取消(key提前过期除外)。可以使用unwatch进行取消。
在multi命令之前先对要watch的key进行监视,然后开启事务,exec命令开始执行,如果返回nil,则代表监视的值已经被其他客户端进行了修改,事务取消。
第一种,在执行exec之前发生错误:
一般是命令存在语法错误,编译器出错。事务会被拒绝运行,也就是队列中所有的命令都不会得到执行。
第二种,在执行exec之后发生错误:
可能是类型出错,但是只有这条命令没有被执行,对于其余命令并没有影响。
为什么没有回滚机制?
无论是哪种错误都不应该发生在生产环境内,回滚也不能解决代码的问题。
一种轻量级脚本语言,使用C来编写,跟数据的存储过程有点类似。
优势:
调用Lua脚本:
在Lua脚本中调用Redis命令:
redis.call(command,key,[param1 , param2 ...]) //command 是命令 //key 是被操作的键 //param1,param2 代表给key的参数
例子:执行 set yang 21 就应该编写命令:eval "return redis.call('set','yang','21') 0"
通常我们会把Lua脚本放在文件里面,然后执行这个文件。
Lua脚本文件:
缓存Lua脚本:
Lua脚本如果比较长的时候,如果每次调用脚本都需要将整个脚本传给Redis服务端,就会产生较大的网络开销,为了解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以直接通过摘要码来执行Lua脚本。
缓存方式:
script load "return 'Hello World'" //使用script load命令在服务端缓存lua脚本并生成一个摘要码 evalsha "437fda89fwe89f8s982323jh1283s382" 0 //通过摘要码执行缓存的脚本
脚本超时:
由于Redis的指令执行本身是单线程的,如果执行Lua脚本超时或者进入了死循环,就没有办法继续提供服务了。
每个脚本默认有一个超时时间为5s,与配置文件中配置项:lua-time-limit 5000
有关,超过5s,其他客户端的命令便不会等待,直接返回"BUSY"错误。当然也不可以一直拒绝其他客户端的命令。Redis还提供了命令可以使用:
script kill //终止脚本的执行,并不是所有脚本都可以kill,那些对Redis的数据进行了修改(SET,DEL等)是不可以通过这种方式来停止脚本运行的。如果使用了这种方式,则会返回UNKILLABLE错误。遇到这种情况,只能通过shutdown nosave命令,该操作不会进行持久化操作,意味着发生在上一次快照后的数据库修改全部都会被丢失。
为什么这么设计?为什么包含修改的脚本不能中断?
因为要保证脚本运行的原子性,如果脚本执行了一部分就被终止,那就违背了脚本原子性的目标。
根据实际测试,Redis的QPS在十万左右,在高性能的服务器上性能还能更强。
Redis这么快的原因总结:
纯内存结构,采用了hashtable实现的KV结构的内存数据库,时间复杂度为O(1)
请求处理单线程:单线程指的是处理客户端请求是单线程的,可以将它称作主线程。4.0版本后还引入了一些线程处理其他的事情,比如清理脏数据,无用连接的释放,大key的删除等。单线程的好处是:1. 没有创建线程,销毁线程带来的性能损耗 2. 避免了上下文切换导致的CPU消耗 3. 避免了线程之间带来的竞争问题。
多路复用机制:使用了多路复用处理并发连接
在Redis中单线程已经够用了,Redis的瓶颈不在CPU上,更有可能是在内存或者网络带宽上。也因为如此,不要在生产环境上运行长命令:比如:keys *,flushall,flushdb等,否则会导致请求被阻塞。
具体的多路复用以及单线程相关的底层知识点就不再这里叙述了,体量太大了,理解即可。
如果所有的key都没有设置过期属性,Redis内存满了怎么办?
在内存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。
#maxmemory<bytes>
如果不设置maxmemory或者设置为0,32位系统最多使用3GB内存,64位系统不限制内存。也可以通过config动态修改:config set maxmemory 2GB
maxmemory-policy
进行决定使用哪种策略:**LRU : **Least Recently Used:最近最少使用,判断最近被使用的时间,目前最远的数据优先被淘汰。
LFU :Least Frequently Used: 最不常用,按照使用频率删除,4.0版本增强。
random : 随机删除。
config set maxmemory-policy xxxxxx-xxx //动态修改淘汰策略
如果没有设置ttl或者没有符合前提条件的key被淘汰,那么volatile-lru
,volatile-random
,volatile-ttl
相当于noeviction
(不做内存回收)
建议使用volatile-lru
,在保证正常服务的情况下,优先删除最近最少使用的key。
LRU是一个很常见的算法,比如InnoDB的Buffer Pool 也用到了LRU。
**传统的LRU : **通过链表+HashMap实现,设置链表长度,如果新增或者被访问,就移动到头节点,超过链表长度,末尾的节点被删除。
**Redis LRU : **对传统的LRU算法进行了改良,通过随机采用来调整算法的精度。如果淘汰策略使用的是LRU,那么就会从配置文件中读取配置的采样个数:maxmemory_samples(默认是5个)
,然后随机从数据库中选择读到个数个key,淘汰其中热度最低的key对应的缓存数据。所以采样数配置的越大,就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计算,执行效率降低。
如何找出热度最低的数据?
Redis中所有对象结构都有一个lru字段,且使用了unsigned 的低24位,这个字段用来记录对象的热度。对象被创建时会记录lru值。在被访问的时候会更新lru的值。但并不是获取系统当前的时间戳,而是设置为全局变量server.lrulock的值。
server.lrulock的值是怎么来的?
Redis中有个定时处理的函数serverCron
,默认每100毫秒调用函数updateCachedTime
更新一次全局变量sever.lrulock
的值,它记录的是当前unix的时间戳。
为什么不获取精确的时间而是放在全局变量中呢?
这样函数查询key调用lookupKey中更新数据的lru热度值时,就不用每次调用系统时间函数Time,可以调高执行效率。
评估热度:
函数评估指定对象的lru热度,方法就是对象的lru的值和全局的server.lrulock的差值越大(越久没有得到更新),该对象热度越低。server.lrulock只有24位,按秒为单位来表示才能存储194天,当超过24bit能表示的最大值时,就会从头开始计算。在这种情况下,可能会出现对象的lru大于server.lrulock情况,那么就将两个相加而不相减来求最久的key。
为什么不使用传统的LRU实现呢?
需要额外的数据结构,消耗资源。而Redis LRU算法在采样数为10的时候,已经能接近传统的LRU算法了。
除了消耗资源之外,传统的LRU还存在什么问题?
因为传统LRU,没有做随机采样,所以有可能访问频率高但是最近一次访问没有访问频率低的最后一次访问时间近。而结果就是将访问频率高的key删除了。
当Redis使用LFU淘汰策略时,原本用于记录LRU热度的字段LRU_BITS这24 bits将被分为两个部分:
counter 是用基于概率的对数计数器实现的,8位可以表示百万次的访问频率。对象被读写的时候,lfu的值会被更新。
这里增长并不是访问一次就加一,增长的速率由一个参数决定,lfu-log-factor
越大,counter的增长就越慢。而这个参数是通过配置文件来决定的:
# lfu-log-factor 10
如果一段时间热度高,就一直保持这个热度也是不行的。体现不了整体频率,所以,没有访问的时候,计数器需要递减。减少的值由衰减因子:lfu-decay-time(分钟)
来控制,如果值为1,N分钟没有访问,计数器就需要减少N。衰减因子越大,衰减就越满。可以通过配置项进行配置衰减因子大小:
# lfu-decay-time 1
RDB是Redis默认的持久化方案(如果开启了AOF,优先使用AOF)。当满足一定的条件的时候,会把当前内存种的数据写入磁盘,生成一个快照文件dump.rdb。Redis重启的时候会加载这个文件来恢复数据。
什么时候写入rdb文件?
自动触发: 配置规则触发,在redis.conf,SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。
save 900 1 #900秒内至少有一个key被修改 save 300 10 #300秒内至少有10个key被修改 save 60 10000 #60秒内至少有10000个key被修改 #注意以上三个规则不冲突,同时生效
如果不需要使用rdb方案,就将save注释或者配置成空字符串""。
使用lastsave命令可以查看最近一次成功生成快照的时间。
除了配置触发生成RDB,还有两种自动的触发方式:
优势:
**劣势: **
如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化!
Redis默认不开启。AOF采用日志的形式来记录每个写操作,并追加到文件中。开启以后,执行更改Redis数据的命令时,就会把命令写入到AOF文件中。Redis重启时会根据日志文件的内容把记录的指令从前到后执行一次以完成对数据的恢复工作。
**AOF配置 : **
# 开关 appendonly no # 文件名 appendfilename "appendonly.aof"
数据都是实时持久化到磁盘吗?
由于操作系统的缓存机制,AOF数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写入到AOF文件?
参数 | 说明 |
---|---|
appendfsync everysec |
AOF 持久化策略(硬盘缓存到磁盘),默认everysec |
no 表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全 | |
always 表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低。 | |
everysec 表示每秒执行一次fsync,可能会丢失这1s的数据,兼顾安全行和效率。(通常选择这个) |
文件越来越大,怎么办?
为了解决这个问题,Redis新增了重写机制。当AOF文件大小超过了所设定的阈值,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。
也可以使用命令bgrewriteaof
来重写,AOF文件重写并不是对原文件进行重新整理,而是直接读取服务现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。
# 重写触发机制 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
重写过程中,AOF文件被更改了怎么办?
另外,在配置文件中有两个与AOF相关的参数:
**优势: **
**劣势: **