Redis提供的5种数据结构已经足够强大,但除此之外,Redis还提供了
诸如慢查询分析、功能强大的Redis Shell、Pipeline、事务与Lua脚本、Bitmaps、HyperLogLog、发布订阅、GEO等附加功能,这些功能可以在某些场景发挥重要的作用。
慢查询分析
许多存储系统(例如MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis也提供了类似的功能。需要注意,慢查询只统计命令执行的时间,所以没有慢查询并不代表客户端没有超时问题。
配置参数
对于慢查询功能,需要明确两件事:
- 预设阀值怎么设置?
- 慢查询记录存放在哪?
Redis提供了slowlog-log-slower-than和slowlog-max-len配置来解决这两个问题。从字面意思就可以看出,slowlog-log-slower-than就是那个预设阀值,它的单位是微秒,默认值是10000,假如执行了一条“很慢”的命令(例如keys *),如果它的执行时间超过了10000微秒,那么它将被记录在慢查询日志中。如果slowlog-log-slower-than = 0会记录所有的命令,slowlog-log-slower-than < 0对于任何命令都不会进行记录。
从字面意思看,slowlog-max-len只是说明了慢查询日志最多存储多少条,并没有说明存放在哪里。实际上Redis使用了一个列表来存储慢查询日志,slowlog-max-len就是列表的最大长度。一个新的命令满足慢查询条件时被插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出,例如slowlog-max-len设置为5,当有第6条慢查询插入的话,那么队头的第一条数据就出列,第6条慢查询就会入列。
在Redis中有两种修改配置的方法,一种是修改配置文件,另一种是使用config set命令动态修改。如果要Redis将配置持久化到本地配置文件,需要执行config rewrite命令。虽然慢查询日志是存放在Redis内存列表中的,但是Redis并没有暴露这个列表的键,而是通过一组命令来实现对慢查询日志的访问和管理。
命令
slowlog get [n]
slowlog len
slowlog reset
每个慢查询日志有4个属性组成,分别是慢查询日志的标识id、发生时间戳、命令耗时、执行命令和参数。
慢查询功能可以有效地帮助我们找到Redis可能存在的瓶颈,但在实际使用过程中要注意以下几点:
- slowlog-max-len配置建议:线上建议调大慢查询列表,记录慢查询时Redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为1000以上。
- slowlog-log-slower-than配置建议:默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到1000。因此对于高OPS场景的Redis建议设置为1毫秒。
- 慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因此客户端执行命令的时间会大于命令实际执行时间。因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此当客户端出现请求超时,需要检查该时间点是否有对应的慢查询,从而分析出是否为慢查询导致的命令级联阻塞。
- 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slowlog get命令将慢查询日志持久化到其他存储中(例如MySQL),然后可以制作可视化界面进行查询。
Redis Shell
Redis提供了redis-cli、redis-server、redis-benchmark等Shell工具。它们虽然比较简单,但是麻雀虽小五脏俱全,有时可以很巧妙地解决一些问题。
redis-cli
redis-cli登录时需要-h、-p参数,但是除了这些参数,还有很
多有用的参数,要了解redis-cli的全部参数,可以执行redis-cli-help命令来进行查看,下面将对一些重要参数的含义以及使用场景进行说明。
- -r(repeat)选项代表将命令执行多次
- -i(interval)选项代表每隔几秒执行一次命令,必须和-r选项一起使用
- -x选项代表从标准输入读取数据作为redis-cli的最后一个参数
- -c(cluster)选项是连接Redis Cluster节点时需要使用的
- --rdb选项会请求Redis实例生成并发送RDB持久化文件,保存在本地
- --bigkeys选项使用scan命令对Redis的键进行采样,从中找到内存占用比较大的键值
- --eval选项用于执行指定Lua脚本
- latency有三个选项,分别是--latency、--latency-history、--latency-dist,它们都可以检测网络延迟
- --stat选项可以实时获取Redis的重要统计信息
redis-benchmark
redis-benchmark可以为Redis做基准性能测试,它提供了很多选项帮助开发和运维人员测试Redis的相关性能,下面分别介绍这些选项。
- -c(clients)选项代表客户端的并发数量(默认是50)
- -n(num)选项代表客户端请求总量(默认是100000)
- -r(random)选项可以向Redis插入更多随机的键
- -t选项可以对指定命令进行基准测试
- --csv选项会将结果按照csv格式输出
Pipeline
Redis客户端执行一条命令分为如下四个过程:
- 发送命令
- 命令排队
- 命令执行
- 返回结果
其中1+ 4称为Round Trip Time(RTT,往返时间)。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命令的执行结果按顺序返回给客户端。虽然Redis服务器并不会限制客户端在流水线中包含的命令数量,但是却会为客户端的输入缓冲区设置默认值为1GB的体积上限:当客户端发送的数据量超过这一限制时,Redis服务器将强制关闭该客户端。
因此用户在使用流水线特性时,最好不要一下把大量命令或者一些体积非常庞大的命令放到同一个流水线中执行,以免触碰到Redis的这一限制。除此之外,很多客户端本身也带有隐含的缓冲区大小限制,如果你在使用流水线特性的过程中,发现某些流水线命令没有被执行,或者流水线返回的结果不完整,那么很可能就是你的程序触碰到了客户端内置的缓冲区大小限制。在遇到这种情况时,请缩减流水线命令的数量及其体积,然后再进行尝试。
即使没有触碰客户端的输入缓冲区限制,一次组装Pipeline数据量过大,也会增加客户端的等待时间,另一方面也会造成一定的网络阻塞。Pipeline只能操作一个Redis实例,但是即使在分布式Redis场景中,也可以作为批量操作的重要优化手段。
事务
熟悉关系型数据库的读者应该对事务比较了解,简单地说,事务表示一组动作,要么全部执行,要么全部不执行。例如在社交网站上用户A关注了用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。
命令
multi
exec
discard
watch
如果事务中的命令出现错误,Redis的处理机制也不尽相同。
1.命令错误
例如错将set写成了sett,属于语法错误,会造成整个事务无法执行。
2.运行时错误
例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就是运行时命令,因为语法是正确的。Redis并不支持回滚功能,所有命令(除去误写的命令)已经执行成功,开发人员需要自己修复这类问题。
Lua
Lua语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为
嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是
功能强大,所以许多应用都选用它作为脚本语言。
数据类型及其逻辑处理
Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数
值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简单。
字符串
下面定义一个字符串类型的数据:
local strings val = "world"
其中,local代表val是一个局部变量,如果没有local代表是全局变量,
print函数可以打印出变量的值。
数组
在Lua中,如果要使用类似数组的功能,可以用tables类型,下面代码使
用定义了一个tables类型的变量myArray,但和大多数编程语言不同的是,Lua的数组下标从1开始计算:
local tables myArray = {"redis", "jedis", true, 88.0}
如果想遍历这个数组,可以使用for和while,这些关键字和许多编程语
言是一致的。关键字for和while都以end作为结束符:
local int sum = 0
for i = 1, 100
do
sum = sum + i
end
-- 输出结果为 5050
print(sum)
要遍历myArray,首先需要知道tables的长度,只需要在变量前加一个#
号即可。除此之外,Lua还提供了内置函数ipairs,使用for index,value ipairs(tables)可以遍历出所有的索引下标和值:
for index, value in ipairs(myArray)
do
print(index)
print(value)
end
要确定数组中是否包含了jedis,有则打印true,注意if以end结尾,if后
紧跟then:
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
哈希
如果要使用类似哈希的功能,同样可以使用tables类型,例如下面代码
定义了一个tables,每个元素包含了key和value,其中strings1..string2是将两个字符串进行连接:
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
函数
在Lua中,函数以function开头,以end结尾:
function contact(str1, str2)
return str1 .. str2
end
在Redis中使用Lua
在Redis中执行Lua脚本有两种方法:eval和evalsha。
eval
eval 脚本内容 key个数 key列表 参数列表
evalsha
将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。
script load命令可以将脚本内容加载到Redis内存中,然后执行evalsha,参数使用SHA1值。
evalsha 脚本SHA1值 key个数 key列表 参数列表
Lua的Redis API
Lua可以使用redis.call函数实现对Redis的访问,还可以使用redis.pcall函数实现对Redis的调用,redis.call和redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,redis.pcall会忽略错误继续执行脚本。
redis.call("set", "hello", "world")
Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中,但是一定要控制日志级别。
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
管理Lua脚本
Redis提供了4个命令实现对Lua脚本的管理:
script load script
script exists sha1 [sha1 … ]
script flush
script kill
Redis提供了一个lua-time-limit参数,默认是5秒,它是Lua脚本的“超时时间”,但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执行,所以当达到lua-time-limit值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个busy的脚本。
但是有一点需要注意,如果当前Lua脚本正在执行写操作,那么script kill将不会生效。
延伸阅读:redis中的lua
Bitmaps
许多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使用率和开发效率。Redis提供了Bitmaps这个“数据结构”可以实现对位的操作。把数据结构加上引号主要因为:
- Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作。
- Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。
命令
setbit key offset value
getbit key offset
bitcount key [start] [end]
bitop op destkey key [key....]
bitpos key targetBit [start] [end]
假设网站有1亿用户,每天独立访问的用户有5千万,如果每天用集合类型和Bitmaps分别存储活跃用户的话。很明显,这种情况下使用Bitmaps能节省很多的内存空间,尤其是随着时间的推移节省的内存还是非常可观的。
不过Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如只有10万(大量的僵尸用户),很显然这时候使用Bitmaps就不太合适了,因为基本上大部分位都是0。所以Bitmaps适用于存储那些不需要记录单个元素的详细信息并且比较紧凑的数据集合。
HyperLogLog
HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等。
例如,2016-03-06的访问用户是uuid-1、uuid-2、uuid-3、uuid-4,2016-03-05的访问用户是uuid-4、uuid-5、uuid-6、uuid-7,如图所示。
命令
pfadd key element [element … ]
pfcount key [key … ]
pfmerge destkey sourcekey [sourcekey ...]
HyperLogLog的优点在于它计算近似基数所需的内存并不会因为集合的大小而改变,无论集合包含的元素有多少个,HyperLogLog进行计算所需的内存总是固定的,并且是非常少的。具体到实现上,Redis的每个HyperLogLog只需要使用12KB内存空间,就可以对接近2^64个元素进行计数,而算法的标准误差仅为0.81%,因此它计算出的近似基数是相当可信的。
HyperLogLog内存占用量非常小,但是存在错误率,开发者在进行数据结构选型时只需要确认如下两条即可:
- 只为了计算独立总数,不需要获取单条数据。
- 可以容忍一定误差率,毕竟HyperLogLog在内存的占用量上有很大的优势。
发布与订阅
Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布
者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息,如图所示。
命令
publish channel message
subscribe channel [channel ...]
unsubscribe [channel [channel ...]]
psubscribe pattern [pattern...]
punsubscribe [pattern [pattern ...]]
pubsub channels [pattern]
pubsub numsub [channel ...]
pubsub numpat
有关订阅命令有两点需要注意:
- 客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四个命令。
- 新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。
聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式。
GEO
Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需要实现这些功能的开发者来说是一大福音。
命令
geoadd key longitude latitude member [longitude latitude member ...]
geopos key member [member ...]
geodist key member1 member2 [unit]
georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
geohash key member [member ...]
zrem key member(删除)
Redis使用geohash将二维经纬度转换为一维字符串,geohash有如下特点:
- GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
- 字符串越长,表示的位置更精确,下表给出了字符串长度对应的精度,例如geohash长度为9时,精度在2米左右。
- 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
- geohash编码和经纬度是可以相互转换的。
Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除(zset的其他命令也适用)。