文章目录
Redis提供的5种数据结构已经足够强大,但除此之外,Redis还提供了诸如慢查询分析、功能强大的Redis Shell、Pipeline、事务与Lua脚本、 Bitmaps、HyperLogLog、发布订阅、GEO等附加功能,这些功能可以在某些场景发挥重要的作用,本节将介绍以下内容。
1:慢查询分析
所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间(并不包括命令的网络传输时间,所以没有慢查询不代表客户端没有超时),当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来。
1.1:慢查询的两个配置参数
对于慢查询功能,需要明确两件事:
- 预设阀值怎么设置?
- 慢查询记录存放在哪?
Redis提供了 slowlog-log-slower-than(预设阀值,单位为微秒) 和 slowlog-max-len(慢日志最多存放条数) 配置来解决这两个问题。如果slowlog-log-slower-than=0会记录所有的命令,slowlog-log-slower- than<0对于任何命令都不会进行记录。而当慢查询日志列表已处于其最大长度时,再插入记录时最早插入的 一个命令将从列表中移出。
在Redis中有两种修改配置的方法,一种是修改配置文件,另一种是使用config set命令动态修改。
慢查询日志是存放再内存列表中的,可以通过一组命令来实现对慢查询日志的访问和管理。
1.获取慢查询日志:slowlog get [n]
2.获取慢查询日志列表当前的长度:slowlog len
3.慢查询日志重置:slowlog reset
开发建议
slowlog-max-len配置建议:线上建议调大慢查询列表,记录慢查询时 Redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以 减缓慢查询被剔除的可能,例如线上可设置为1000以上。
slowlog-log-slower-than配置建议:默认值超过10毫秒判定为慢查询, 需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流 量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到 1000。因此对于高OPS场景的Redis建议设置为1毫秒。慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因 此客户端执行命令的时间会大于命令实际执行时间。
由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期 执行slow get令将慢查询日志持久化到其他存储中(例如MySQL),然后可以制作可视化界面进行查询
2:Redis Shell
2.1: redis-cli详解
之前曾介绍过redis-cli,包括-h、-p参数,但是除了这些参数,还有很多有用的参数,要了解redis-cli的全部参数,可以执行redis-cli --help命令来进行查看,下面将对一些重要参数的含义以及使用场景进行说明。
- -r:将命令执行多次
- -i:代表每隔几秒执行一次命令,但是-i选项必须和-r选 项一起使用.注意:此时单位是秒,可以使用小数。
- -x:代表从标准输入(stdin)读取数据作为redis-cli的最后一个参数。
- -a:如果Redis配置了密码,可以用-a(auth)选项,有了这个选项就不需要 手动输入auth命令。
- –slave:把当前客户端模拟成当前Redis节点的从节点,可以用来 获取当前Redis节点的更新操作。
- –rdb:请求Redis实例生成并发送RDB持久化文件,保存在本地。 可使用它做持久化文件的定期备份。
- –pipe:用于将命令封装成Redis通信协议定义的数据格式,批量发送给Redis执行.
- –latency:latency有三个选项,分别是–latency、–latency-history、–latency-dist。 它们都可以检测网络延迟,对于Redis的开发和运维非常有帮助。
2.2:redis-benchmark详解
redis-benchmark可以为Redis做基准性能测试,它提供了很多选项帮助开发和运维人员测试Redis的相关性能,下面分别介绍这些选项。
- .-c
-c(clients)选项代表客户端的并发数量(默认是50)。
- -n
<requests>
-n(num)选项代表客户端请求总量(默认是100000)。
例如redis-benchmark-c100-n20000代表100各个客户端同时请求Redis,一 共执行20000次。redis-benchmark会对各类数据结构的命令进行测试,并给出性能指标:
例如上面一共执行了20000次get操作,在0.27秒完成,每个请求数据量 是3个字节,99.11%的命令执行时间小于1毫秒,Redis每秒可以处理 73529.41次get请求。
- .-q
选项仅仅显示redis-benchmark的requests per second信息,例如:
- -r
在一个空的Redis上执行了redis-benchmark会发现只有3个键:
如果想向Redis插入更多的键,可以执行使用-r(random)选项,可以向 Redis插入更多随机的键。
- -t
-t选项可以对指定命令进行基准测试。
- --csv
–csv选项会将结果按照csv格式输出,便于后续处理,如导出到Excel 等
3:Pipeline
Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。但大部分命令是不支持批量操作的,例如要执行n次hgetall命令,并没有mhgetall命令存在,需要消耗n次RTT。Redis的客户端和服务端可能部署在不同的机器上。例如客户端在北京,Redis服务端在上海,两地直线距离约为 1300公里,那么1次RTT时间=1300×2/(300000×2/3)=13毫秒(光在真空中传输速度为每秒30万公里,这里假设光纤为光速的2/3),那么客户端在1秒 内大约只能执行80次左右的命令,这个和Redis的高并发高吞吐特性背道而驰。Pipeline(流水线)机制能改善上面这类问题,它能将一组Redis命令进 行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端,图为没有使用Pipeline执行了n条命令,整个过程需要n次 RTT。
大部分开发人员更倾向于使用高级语言客户端中的Pipeline,目前大部分Redis客户端都支持Pipeline,之后将介绍如何通过Java的Redis客 户端Jedis使用Pipeline功能。
原生批量命令与Pipeline对比:
- 原生批量命令是原子的,Pipeline是非原子的。
- 原生批量命令是一个命令对应多个key,Pipeline支持多个命令。
- 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户 端的共同实现。
4:事务与Lua
4.1:事务
为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集 成Lua脚本来解决这个问题。
Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和 exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的,例如下面操作实现了上述用户关注问题:
可以看到set命令此时的返回结果是QUEUED,代表命令并没有真正执行,而是暂时保存在Redis中。
如果要停止事务的执行,可以使用discard命令代替exec命令即可。
如果事务中的命令出现错误,Redis的处理机制也不尽相同。
- 命令错误
例如下面操作错将set写成了sett,属于语法错误,会造成整个事务无法 执行,key和counter的值未发生变化:
- 运行时错误
例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就是运行时命令,因为语法是正确的:
可以看到Redis并不支持回滚功能,sadd user:a:follow user:b命令已 经执行成功,开发人员需要自己修复这类问题。
有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来解决这类问题,表展示了两个客户端执行命令的时序。
可以看到“客户端-1”在执行multi之前执行了watch命令,“客户端-2”在“客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果 为nil),整个代码如下所示:
4.2:Lua用法简述
Redis将Lua作为脚本语言可帮助开发者定制自己的Redis命令,在使用Lua之前,必须修改源码来完成。
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
4.2.1:Lua的基本使用
Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简单。
(1)字符串
local strings val = "world"
其中,local代表val是一个局部变量,如果没有local代表是全局变量。 print函数可以打印出变量的值,例如下面代码将打印world,其中"–"是Lua 语言的注释。
-- 结果是"world"
print(hello
(2)数组
local tables myArray = {"redis", "jedis", true, 88.0}
--true
print(myArray[3])
如果想遍历这个数组,可以使用for和while,这些关键字和许多编程语言是一致的。
(a)for
local int sum = 0
--for语句循环
for i = 1, 100
do
sum = sum + i
end
-- 输出结果为5050
print(sum)
要遍历myArray,首先需要知道tables的长度,只需要在变量前加一个# 号即可:
for i = 1, #myArray
do
print(myArray[i])
end
除此之外,Lua还提供了内置函数ipairs,使用for index,value ipairs(tables)可以遍历出所有的索引下标和值:
for index,value in ipairs(myArray)
do
print(index)
print(value)
end
(b)while
local int sum = 0
local int i = 0
while i <= 100
do
sum = sum +i
i = i + 1
end
--输出结果为5050
print(sum)
(c)if else
local tables myArray = {"redis", "jedis", true, 88.0}
for i = 1, #myArray
do if myArray[i] == "jedis"
then
print("true")
break
else
--do
nothing
end
end
(3)哈希
local tables user_1 = {age = 28, name = "tome"}
--user_1 age is 28
print("user_1 age is " .. user_1["age"])
--遍历user_1,使用Lua的内置函数pairs:
for key,value in pairs(user_1)
do
print(key .. value)
end
(4)函数定义
在Lua中,函数以function开头,以end结尾,funcName是函数名,中间部分是函数体。
function funcName()
...
end
--contact函数将两个字符串拼接:
function contact(str1, str2)
return str1 .. str2
end
--"hello world"
print(contact("hello ", "world"))
4.3:Redis与Lua
4.3.1:在Redis中使用Lua
在Redis中执行Lua脚本有两种方法:eval和evalsha。
(1)eval
eval 脚本内容 key个数 key列表 参数列表
下面例子使用了key列表和参数列表来为Lua脚本提供更多的灵活性:
eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world "hello
redisworld"
此时KEYS[1]=“redis”,ARGV[1]=“world”,所以最终的返回结果 是"hello redisworld"。
如果Lua脚本较长,还可以使用redis-cli–eval直接执行文件。
eval命令和–eval参数本质是一样的,客户端如果想执行Lua脚本,首先 在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端。
(2)evalsha
首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和, evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送 Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻 在服务端,脚本功能得到了复用。
加载脚本:script load命令可以将脚本内容加载到Redis内存中,得到SHA1
# redis-cli script load "$(cat lua_get.lua)"
"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
执行脚本:evalsha的使用方法如下,参数使用SHA1值,执行逻辑和 eval一致。
--evalsha 脚本SHA1值 key个数 key列表 参数列表
evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
"hello redisworld"
Lua可以使用redis.call函数实现对Redis的访问,例如下面代码是Lua使用 redis.call调用了Redis的set和get操作:
redis.call("set", "hello", "world")
redis.call("get", "hello")
4.3.2:案例
下面以一个例子说明Lua脚本的使用,当前列表记录着热门用户的id, 假设这个列表有5个元素,如下所示:
user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:
现要求将列表内所有的键对应热度做加1操作,并且保证是原子执行, 此功能可以利用Lua脚本来实现。
1)将列表中所有元素取出,赋值给mylist:
local mylist = redis.call("lrange", KEYS[1], 0, -1)
2)定义局部变量count=0,这个count就是最后incr的总次数:
local count = 0
3)遍历mylist中所有元素,每次做完count自增,最后返回count:
for index,key in ipairs(mylist)
do
redis.call("incr",key)
count = count + 1
end
return count
将上述脚本写入lrange_and_mincr.lua文件中,并执行如下操作,返回结果为5
执行后所有用户的热度自增1:
4.3.3:Redis如何管理Lua脚本
Redis提供了4个命令实现对Lua脚本的管理
(1)script load
此命令用于将Lua脚本加载到Redis内存中,前面已经介绍并使用过了, 这里不再赘述。
(2)script exists
scripts exists sha1 [sha1 …]
此命令用于判断sha1是否已经加载到Redis内存中。
(3)script flush
script flush
此命令用于清除Redis内存已经加载的所有Lua脚本。
(4)script kill
script kill
此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua 脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或 者外部进行干预将其结束。
5: 发布订阅
Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息,如所示。Redis提供了若干命令支持该功能,在实际应用开发时,能够为此类问题提供实现方法。和很多专业的消息队列系统(例如Kafka、RocketMQ)相比,Redis的发布订阅略显粗糙,例如无法实现消息堆积和回溯。但胜在足够简单,如果当前场景可以容忍的这些缺点,也不失为一个不错的选择。
5.1:命令
1.发布消息
publish channel message
2.订阅消息
subscribe channel [channel ...]
订阅者可以订阅一个或多个频道,下面操作为当前客户端订阅了 channel:sports频道:
有关订阅命令有两点需要注意:
一:新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。
二:客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe、 psubscribe、unsubscribe、punsubscribe的四个命令。
3.取消订阅
unsubscribe [channel [channel ...]]
客户端可以通过unsubscribe命令取消对指定频道的订阅,取消成功后, 不会再收到该频道的发布消息
4.按照模式订阅和取消订阅
psubscribe pattern [pattern...]
punsubscribe [pattern [pattern ...]]
5.查询订阅
(1)查看活跃的频道
pubsub channels [pattern]
所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以指定具体的模式
(2)查看频道订阅数
pubsub numsub [channel ...]
当前channel:sports频道的订阅数为2:
5.2:使用场景
聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式,下 面以简单的服务解耦进行说明。如图所示,图中有两套业务,上面为视频管理系统,负责管理视频信息;下面为视频服务面向客户,用户可以通过各种客户端(手机、浏览器、接口)获取到视频信息。