Redis04-小功能大用处
- Redis提供的5种数据结构已经足够强大,但除此之外,Redis还提供了诸如慢查询分析、功能强大的Redis Shell、Pipeline、事务与Lua脚本、Bitmaps、HyperLogLog、发布订阅、GEO等附加功能。
- 慢查询分析:通过慢查询分析,找到有问题的命令进行优化。
- Redis Shell:功能强大的Redis Shell会有意想不到的实用功能。
- Pipeline:通过Pipeline(管道或者流水线)机制有效提高客户端性能。
- 事务与Lua:制作自己的专属原子命令。
- Bitmaps:通过在字符串数据结构上使用位操作,有效节省内存,为开发提供新的思路。
- HyperLogLog:一种基于概率的新算法,难以想象地节省内存空间。
- 发布订阅:基于发布订阅模式的消息通信机制。
- GEO:Redis3.2提供了基于地理位置信息的功能。
1、慢查询分析
- 慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来。
- 如图3-1所示,Redis客户端执行一条命令分为如下4个部分:
- 注意,慢查询只统计步骤(3)的时间,所以没有慢查询并不代表客户端没有超时问题。
1.1、慢查询的两个配置参数
- 对于慢查询功能,需要明确两件事:
- 预设阀值怎么设置?
- 慢查询记录存放在哪?
#设置预设阀值 slowlog-log-slower-than 10000 #最多记录多少条慢查询日志 slowlog-max-len 128
- slowlog-log-slower-than设置预设阀值,单位是微秒(1秒=1000毫秒=1000000微秒),默认值是10000。
- 注意,零值强制记录每个命令,负数禁用慢日志
- 假如执行了一条“很慢”的命令(例如keys *),如果它的执行时间超过了10000微秒,那么它将被记录在慢查询日志中。
- slowlog-max-len只是说明了最多记录多少条慢查询日志,并没有说明存放在哪里。
- 实际上Redis使用了一个列表来存储慢查询日志,slowlog-max-len就是列表的最大长度。
- 一个新的命令满足慢查询条件时被插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出。
- 注意它会消耗内存。可以使用SLOWLOG RESET回收慢日志使用的内存。
- 在Redis中有两种修改配置的方法,一种是修改配置文件,另一种是使用config set命令动态修改。
- 使用config set命令将slowlog-log-slowerthan设置为20000微秒,slowlog-max-len设置为1000。
#设置值 10.1.1.11:6379> config set slowlog-log-slower-than 2000 OK 10.1.1.11:6379> config set slowlog-max-len 100 OK #查看值 10.1.1.11:6379> config get slowlog-log-slower-than 1) "slowlog-log-slower-than" 2) "2000" 10.1.1.11:6379> config get slowlog-max-len 1) "slowlog-max-len" 2) "100"
-
- 执行config rewrite命令,可以将配置持久化到本地配置文件。
//将配置持久化到本地配置文件 10.1.1.11:6379> config rewrite OK //查看本地配置文件 ]# cat /apps/redis/6379.conf | grep slowlog slowlog-log-slower-than 2000 slowlog-max-len 100
1.2、查看和管理慢查询日志
SLOWLOG GET [count] #从slowlog返回顶级条目(默认为10)。条目包括:标识id、发生时间戳、命令耗时(以微秒为单位)、执行命令和参数、客户端IP和端口、客户端名称 SLOWLOG LEN #返回慢日志列表的长度 SLOWLOG RESET #重置slowlog(清空慢日志存储列表)。
示例:
10.1.1.11:6379> SLOWLOG GET 1) 1) (integer) 6 2) (integer) 1660400164 3) (integer) 13 4) 1) "keys" 2) "*" 5) "10.1.1.12:47556" 6) "" ... 10.1.1.11:6379> SLOWLOG LEN (integer) 8 10.1.1.11:6379> SLOWLOG RESET OK 10.1.1.11:6379> SLOWLOG LEN (integer) 0
1.3、最佳实践
- 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 [OPTIONS] [cmd [arg [arg ...]]] -h <hostname> Redis服务器的IP或主机名(default: 127.0.0.1). -p <port> Redis服务器端口(default: 6379). -a <password> 连接到Redis服务器时使用的密码。 --raw 要求命令的返回结果必须是格式化的 --no-raw 要求命令的返回结果必须是原始的格式 -r <repeat> 将命令执行多次。 -i <interval> 每隔几秒执行一次命令,但是-i选项必须和-r选项一起使用 -x 从标准输入(stdin)读取数据作为redis-cli的最后一个参数 --scan 使用SCAN命令列出所有--pattern匹配到的键。 --pattern <pat> 使用--scan,--bigkeys或--hotkeys选项时的键模式(default: *). --pipe 将命令封装成Redis通信协议定义的数据格式,批量发送给Redis执行 --bigkeys 使用scan命令对Redis的键进行采样,从中找到内存占用比较大的键值,这些键可能是系统的瓶颈。 --stat 可以实时获取Redis的重要统计信息,虽然info命令中的统计信息更全,但是能实时看到一些增量的数据(例如requests)对于Redis的运维还是有一定帮助的 --latency 可以测试客户端到目标Redis的网络延迟 --latency-history --latency的执行结果只有一条。--latency-history默认每15秒输出一次信息 --latency-dist 使用统计图表的形式从控制台输出延迟统计信息。默认时间间隔为1秒。 --rdb <filename> 请求远程Redis服务器生成RDB,并传输RDB转储到本地文件。 --eval <file> 用于执行指定Lua脚本 -c 启用集群模式(遵循-ASK和-MOVED重定向)。
示例:
//每隔1秒输出内存的使用量,一共输出100次 ]# redis-cli -h 10.1.1.11 -p 6379 -a hengha123 -r 100 -i 1 info | grep used_memory_human //从标准输入(stdin)读取数据作为redis-cli的最后一个参数 ]# echo "world" | redis-cli -h 10.1.1.11 -p 6379 -a hengha123 -x set hello //实时获取Redis的重要统计信息 ]# redis-cli -h 10.1.1.11 -p 6379 -a hengha123 --stat //测试网络延迟 ]# redis-cli -h 10.1.1.11 -p 6379 -a hengha123 --latency ]# redis-cli -h 10.1.1.11 -p 6379 -a hengha123 --latency-history ]# redis-cli -h 10.1.1.11 -p 6379 -a hengha123 --latency-dist
2.2、redis-server详解
redis-server redis-server [/path/to/redis.conf] [options] redis-server --test-memory <megabytes> redis-server -v or --version redis-server -h or --help
- redis-server --test-memory <megabytes>命令可以用来检测当前操作系统能否稳定地分配指定容量的内存给Redis,通过这种检测可以有效避免因为内存问题造成Redis崩溃
示例:
redis-server redis-server /etc/redis/6379.conf redis-server --port 7777 redis-server --port 7777 --replicaof 127.0.0.1 8888 redis-server /etc/myredis.conf --loglevel verbose
2.3、redis-benchmark详解
- redis-benchmark可以为Redis做基准性能测试,它提供了很多选项帮助开发和运维人员测试Redis的相关性能。
redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests>] [-k <boolean>] -h <hostname> Redis服务器的IP或主机名(default 127.0.0.1) -p <port> Redis服务器端口(default 6379) -a <password> 连接到Redis服务器时使用的密码。 -c <clients> 并行连接数(default 50) -n <requests> 请求总数(default 100000) -q 安静。只显示查询/秒值 -r <keyspacelen> 向Redis插入随机键。keyspacelen键名最后几位是随机值,例如10000表示只对键名的后四位做随机处理 -P <numreq> 每个请求pipeline的数据量(默认为1)。 -k <boolean> 客户端是否使用keepalive,1为使用,0为不使用(default 1) -t <tests> 可以对指定命令进行基准测试。 --csv 以CSV格式输出
示例:
//100个客户端同时请求Redis,一共执行20000次 ]# redis-benchmark -h 10.1.1.11 -p 6379 -a hengha123 -c 100 -n 20000 20000 requests completed in 1.18 seconds 100 parallel clients 3 bytes payload keep alive: 1 99.78% <= 9 milliseconds 16877.64 requests per second //仅显示redis-benchmark的requests per second信息 ]# redis-benchmark -h 10.1.1.11 -p 6379 -a hengha123 -c 100 -n 20000 -q PING_INLINE: 18115.94 requests per second PING_BULK: 18198.36 requests per second SET: 17064.85 requests per second LRANGE_500 (first 450 elements): 6053.27 requests per second LRANGE_600 (first 600 elements): 5142.71 requests per second MSET (10 keys): 15564.20 requests per second //-r选项会在key、counter键上加一个12位的后缀,-r10000代表只对后四位做随机处理(-r不是随机数的个数) ]# redis-benchmark -h 10.1.1.11 -p 6379 -a hengha123 -c 100 -n 20000 -r 10000 10.1.1.11:6379> scan 0 1) "384" 2) 1) "counter:000000001749" 2) "key:000000001252"
3、Pipeline
3.1、Pipeline概念
- Redis客户端执行一条命令分为如下四个过程,其中(1)和(4)称为Round Trip Time(RTT,往返时间):
- (1)发送命令
- (2)命令排队
- (3)命令执行
- (4)返回结果
- 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命令的执行结果按顺序返回给客户端。
- 图3-5为不使用Pipeline执行了n条命令,整个过程需要n次RTT。
- 图3-6为使用Pipeline执行了n次命令,整个过程需要1次RTT。
- Redis命令真正执行的时间通常在微秒级别,所以才会有Redis性能瓶颈是网络这样的说法。
- redis-cli的--pipe选项实际上就是使用Pipeline机制,例如下面操作将set hello world和incr counter两条命令组装:
echo -en '*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n*2\r\n$4\r\nincr\r\n$7\r\ncounter\r\n' | redis-cli --pipe
- 但大部分开发人员更倾向于使用高级语言客户端中的Pipeline,目前大部分Redis客户端都支持Pipeline。
3.2、性能测试
- 表3-1给出了在不同网络环境下非Pipeline和Pipeline执行10000次set操作的效果,可以得到如下两个结论:
- Pipeline执行速度一般比逐条执行要快。
- 客户端和服务端的网络延时越大,Pipeline的效果越明显。
3.3、原生批量命令与Pipeline对比
- 可以使用Pipeline模拟出批量操作的效果,但是在使用时要注意它与原生批量命令的区别,具体包含以下几点:
- 原生批量命令是原子的,Pipeline是非原子的。
- 原生批量命令是一个命令对应多个key,Pipeline支持多个命令。
- 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现。
3.4、最佳实践
- Pipeline虽然好用,但是每次Pipeline组装的命令个数不能没有节制,否则一次组装Pipeline数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成。
- Pipeline只能操作一个Redis实例,但是即使在分布式Redis场景中,也可以作为批量操作的重要优化手段。
4、事务与Lua
- Redis提供了简单的事务功能以及集成Lua脚本,保证多条命令组合的原子性。
4.1、事务
- 事务表示一组动作,要么全部执行,要么全部不执行。
- 例如在社交网站上用户A关注了用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。
4.1.1、事务的基本使用
- Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。
- multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的。
- Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,当然也体现了Redis的“keep it simple”的特性。
MULTI #事务开始 EXEC #事务结束(开始执行事务中的命令) DISCARD #停止事务的执行(放弃执行事务中的命令)
示例:
//开始事务 10.1.1.11:6379> multi OK //此时的返回结果是QUEUED,代表命令并没有真正执行,而是暂时保存在Redis中。 10.1.1.11:6379> sadd user:a:follow user:b QUEUED 10.1.1.11:6379> sadd user:b:fans user:a QUEUED //如果此时另一个客户端执行,返回结果是0 10.1.1.11:6379> sismember user:a:follow user:b (integer) 0 //只有当exec执行后,事务才算完成 10.1.1.11:6379> exec 1) (integer) 1 2) (integer) 1 10.1.1.11:6379> sismember user:a:follow user:b (integer) 1
4.1.2、事务中出现错误
- 如果事务中的命令出现错误,Redis的处理机制也不尽相同。
1、命令错误
示例:
- 错将set写成了sett,属于语法错误,会造成整个事务无法执行,key和counter的值未发生变化。
10.1.1.11:6379> multi OK 10.1.1.11:6379> sett key world (error) ERR unknown command `sett`, with args beginning with: `key`, `world`, 10.1.1.11:6379> incr counter QUEUED 10.1.1.11:6379> exec (error) EXECABORT Transaction discarded because of previous errors.
2、运行时错误
示例:
- 用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就是运行时命令,因为语法是正确的。
10.1.1.11:6379> multi OK 10.1.1.11:6379> sadd user:a2:follow user:b QUEUED 10.1.1.11:6379> zadd user:b:fans 1 user:a2 QUEUED 10.1.1.11:6379> exec 1) (integer) 1 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 10.1.1.11:6379> sismember user:a2:follow user:b (integer) 1
- 可以看到Redis并不支持回滚功能,sismember user:a2:follow user:b命令已经执行成功,开发人员需要自己修复这类问题。
3、事务中watch命令
- 有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。
- Redis提供了watch命令来解决这类问题,表3-2展示了两个客户端执行命令的时序。
示例:
- 可以看到“客户端-1”在执行multi之前执行了watch命令,“客户端-2”在“客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果为nil)。
//T1:客户端1 10.1.1.11:6379> watch key OK //T2:客户端1 127.0.0.1:6379> watch key OK //T3:客户端1 10.1.1.11:6379> multi OK //T4:客户端2 10.1.1.11:6379> append key python (integer) 10 //T5:客户端1 10.1.1.11:6379> append key python QUEUED //T6:客户端1 10.1.1.11:6379> exec (nil) //T7:客户端1 10.1.1.11:6379> get key "javapython"
4.2、Lua用法简述
- Lua语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是功能强大,所以许多应用都选用它作为脚本语言。
- 尤其是在游戏领域,例如大名鼎鼎的暴雪公司将Lua语言引入到“魔兽世界”这款游戏中,Rovio公司将Lua语言作为“愤怒的小鸟”这款火爆游戏的关卡升级引擎。
- Web服务器Nginx将Lua语言作为扩展,增强自身功能。
- Redis将Lua作为脚本语言可帮助开发者定制自己的Redis命令,在这之前,必须修改源码。
- Lua官方网站:http://www.lua.org/
- Lua脚本功能为Redis开发和运维人员带来如下三个好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
4.2.1、数据类型
- Lua语言有4种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格)。
1、字符串
- local代表val是一个局部变量,默认是全局变量。
- print函数可以打印出变量的值。
- Lua语言的注释符是"--"。
示例:
- 下面定义一个字符串类型的数据。
local strings val = "world" -- 结果是"world" print(hello)
2、数组
- 在Lua中,如果要使用类似数组的功能,可以用tables类型。
- Lua的数组下标从1开始计算。
- 要获取Lua数组的长度,只需要在变量前加一个#号即可。
示例:
- 定义了一个tables类型的变量myArray。
local tables myArray = {"redis", "jedis", true, 88.0} --结果是"true" print(myArray[3])
3、哈希
- 如果要使用类似哈希的功能,同样可以使用tables类型。
示例1:
- 定义了一个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"])
示例2:
- 遍历user_1,可以使用Lua的内置函数pairs。
for key,value in pairs(user_1) do print(key .. value) end
4.2.2、控制语句
1、for
- 关键字for以end作为结束符
示例1:
- 会计算1到100的和。
local int sum = 0 for i = 1, 100 do sum = sum + i end -- 输出结果为5050 print(sum)
示例2:
- 遍历数组myArray。
for i = 1, #myArray do print(myArray[i]) end
示例3:
- Lua的内置函数ipairs,使用for index,value ipairs(tables)可以遍历出所有的索引下标和值。
for index,value in ipairs(myArray) do print(index) print(value) end
2、while
- while循环同样以end作为结束符。
示例1:
- 计算1到100的和。
local int sum = 0 local int i = 0 while i <= 100 do sum = sum +i i = i + 1 end --输出结果为5050 print(sum)
3、if else
- if以end结尾,if后紧跟then。
示例:
- 数组中是否包含了jedis,有则打印true。
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
4.2.3、函数定义
- 在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的语法格式:
eval 脚本内容 key个数 key列表 参数列表
示例:
- 使用了key列表和参数列表来为Lua脚本提供更多的灵活性。
//KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果是"hello redisworld"。 127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world "hello redisworld"
- 如果Lua脚本较长,还可以使用redis-cli --eval直接执行文件。
- eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端,整个过程如图3-7所示。
2、evalsha
- Redis还可以evalsha命令来执行Lua脚本。
- 如图3-8所示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。
- 加载脚本:script load命令可以将脚本内容加载到Redis内存中。
//将lua_get.lua加载到Redis中,得到SHA1为:"7413dc2440db1fea7c0a0bde841fa68eefaf149c" ]# redis-cli script load "$(cat lua_get.lua)" "7413dc2440db1fea7c0a0bde841fa68eefaf149c"
- 执行脚本:evalsha使用SHA1值作为脚本名,执行逻辑和eval一致。
evalsha 脚本SHA1值 key个数 key列表 参数列表
- 执行lua_get.lua脚本,只需要执行如下操作。
10.1.1.11:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world "hello redisworld"
4.3.2、Lua的Redis API
- Lua可以使用redis.call函数实现对Redis的访问。redis.call可以调用Redis的set和get操作。
redis.call("set", "hello", "world") redis.call("get", "hello")
- Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和redis.pcall的不同之处在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本,所以在实际开发中要根据具体的应用场景进行函数的选择。
- Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中,但是一定要控制日志级别。
- Redis3.2提供了Lua Script Debugger功能用来调试复杂的Lua脚本,具体可以参考:http://redis.io/topics/ldb。
示例:
10.1.1.11:6379> get hello "world" 10.1.1.11:6379> eval 'return redis.call("get", KEYS[1])' 1 hello hello1 hello2 "world"
4.4、Redis如何管理Lua脚本
- Redis提供了4个命令实现对Lua脚本的管理。
SCRIPT LOAD <script> #将Lua脚本加载到Redis内存中,但不执行它。 SCRIPT EXISTS <sha1> [<sha1> ...] #用于判断sha1是否已经加载到Redis内存中。返回结果是被加载到Redis内存的个数。 SCRIPT FLUSH #用于清除Redis内存已经加载的所有Lua脚本。 SCRIPT KILL #用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或者外部进行干预将其结束。
- 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将不会生效。
- 如果Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么使用shutdown save停掉Redis服务。可见Lua脚本虽然好用,但是使用不当破坏性也是难以想象的。
5、Bitmaps
5.1、数据结构模型
- 现代计算机用二进制(位)作为信息的基础单位,1个字节等于8位。
- 例如“big”字符串是由3个字节组成,但实际在计算机存储时将其用二进制表示,“big”分别对应的ASCII码分别是98、105、103,对应的二进制分别是01100010、01101001和01100111,如图3-9所示。
- Redis提供Bitmaps这个“数据结构”可以实现对位的操作。把数据结构加上引号主要因为:
- Bitmaps本身不是一种数据结构,实际上它就是字符串(如图3-10所示),但是它可以对字符串的位进行操作。
- Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。
5.2、命令
1、设置值
SETBIT key offset value #设置键的第offset个位的值(从0算起)
示例:
- 假设现在有20个用户,userid=0,5,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图3-11所示。
10.1.1.11:6379> setbit unique:users:2022-08-15 0 1 (integer) 0 10.1.1.11:6379> setbit unique:users:2022-08-15 5 1 (integer) 0 10.1.1.11:6379> setbit unique:users:2022-08-15 11 1 (integer) 0 10.1.1.11:6379> setbit unique:users:2022-08-15 15 1 (integer) 0 10.1.1.11:6379> setbit unique:users:2022-08-15 19 1 (integer) 0
2、获取值
GETBIT key offset #获取键的第offset位的值(从0开始算)
示例:
10.1.1.11:6379> getbit unique:users:2022-08-15 15 (integer) 1 10.1.1.11:6379> getbit unique:users:2022-08-15 16 (integer) 0
3、获取Bitmaps指定范围值为1的个数
BITCOUNT key [start end] #获取Bitmaps指定范围值为1的个数 start和end代表起始和结束字节数
示例:
//前两个字节中1的个数 10.1.1.11:6379> bitcount unique:users:2022-08-15 0 1 (integer) 4
4、Bitmaps间的运算
- bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。
BITOP operation destkey key [key ...]
5、计算Bitmaps中第一个值为targetBit的偏移量
BITPOS key bit [start] [end] [start]和[end],分别代表起始字节和结束字节
5.3、Bitmaps分析
- 假设网站有1亿用户,每天独立访问的用户有5千万,如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表3-3。
- 很明显,这种情况下使用Bitmaps能节省很多的内存空间,尤其是随着时间推移节省的内存还是非常可观的,见表3-4。
- 但Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如只有10万(大量的僵尸用户),那么两者的对比如表3-5所示,很显然,这时候使用Bitmaps就不太合适了,因为基本上大部分位都是0。
6、HyperLogLog
- HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法。
- 通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等。
- HyperLogLog提供了3个命令:pfadd、pfcount、pfmerge。
- HyperLogLog的算法是由Philippe Flajolet(https://en.wikipedia.org/wiki/Philippe_Flajolet)在The analysis of a near-optimal cardinality estimation algorithm这篇论文中提出,读者如果有兴趣可以自行阅读。
- HyperLogLog内存占用量非常小,但是存在错误率,开发者在进行数据结构选型时只需要确认如下两条即可:
- 只为了计算独立总数,不需要获取单条数据。
- 可以容忍一定误差率,毕竟HyperLogLog在内存的占用量上有很大的优势。
1、添加
- pfadd用于向HyperLogLog添加元素,如果添加成功返回1。
PFADD key element [element ...]
示例:
10.1.1.11:6379> pfadd 2022-08-16:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4" (integer) 1
2、计算独立用户数
- pfcount用于计算一个或多个HyperLogLog的独立总数。
PFCOUNT key [key ...]
示例:
10.1.1.11:6379> pfcount 2022-08-16:unique:ids (integer) 4 //添加多个元素,但只有一个不重复的元素 10.1.1.11:6379> pfadd 2022-08-16:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-90" (integer) 1 10.1.1.11:6379> pfcount 2022-08-16:unique:ids (integer) 5
- 表3-6列出了使用集合类型和HperLogLog统计百万级用户的占用空间对比。
- 可以看到,HyperLogLog内存占用量小得惊人,但是用如此小空间来估算如此巨大的数据,必然不是100%的正确,其中一定存在误差率。Redis官方给出的数字是0.81%的失误率。
3、合并
- pfmerge可以求出多个HyperLogLog的并集并赋值给destkey。
PFMERGE destkey sourcekey [sourcekey ...]
示例:
10.1.1.11:6379> pfadd 2022-08-16-1:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4" (integer) 1 10.1.1.11:6379> pfadd 2022-08-16-2:unique:ids "uuid-4" "uuid-5" "uuid-6" "uuid-7" (integer) 1 10.1.1.11:6379> pfmerge 2022-08-16-3:unique:ids 2022-08-16-1:unique:ids 2022-08-16-2:unique:ids OK 10.1.1.11:6379> pfcount 2022-08-16-3:unique:ids (integer) 7
7、发布订阅
- Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息,如图3-16所示。
7.1 命令
- Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。
1、发布消息
PUBLISH channel message
示例:
//向channel:sports频道发布一条消息“Tim won the championship”,返回结果为订阅者个数,因为此时没有订阅,所以返回结果为0。 10.1.1.11:6379> publish channel:sports "Tim won the championship" (integer) 0
2、订阅消息
- 订阅者可以订阅一个或多个频道。
SUBSCRIBE channel [channel ...]
示例:
//当前客户端订阅channel:sports频道。 10.1.1.11:6379> subscribe channel:sports Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "channel:sports" 3) (integer) 1 //此时使用另一个客户端发布一条消息。 10.1.1.11:6379> publish channel:sports "James lost the championship" (integer) 1 //当前订阅者客户端会收到如下消息。 10.1.1.11:6379> subscribe channel:sports Reading messages... (press Ctrl-C to quit) ... 1) "message" 2) "channel:sports" 3) "James lost the championship"
- 有关订阅命令有两点需要注意:
- 客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四个命令。
- 新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。
3、取消订阅
- 客户端可以通过unsubscribe命令取消对指定频道的订阅,取消成功后,不会再收到该频道的发布消息。
UNSUBSCRIBE [channel [channel ...]]
示例:
10.1.1.11:6379> unsubscribe channel:sports 1) "unsubscribe" 2) "channel:sports" 3) (integer) 0
4、按照模式订阅和取消订阅
- Redis命令支持glob风格的订阅命令psubscribe和取消订阅命令punsubscribe。
PSUBSCRIBE pattern [pattern ...] PUNSUBSCRIBE [pattern [pattern ...]]
示例:
//阅以it开头的所有频道: 10.1.1.11:6379> psubscribe it* Reading messages... (press Ctrl-C to quit) 1) "psubscribe" 2) "it*" 3) (integer) 1
5、查询订阅
PUBSUB CHANNELS [<pattern>] #查看活跃的频道(default: all)。活动的频道是指当前频道至少有一个订阅者。 PUBSUB NUMSUB [channel-1 .. channel-N] #查看指定频道的订阅者数量(excluding patterns, default: none) PUBSUB NUMPAT [argument [argument ...]] #查看通过模式匹配订阅消息的订阅者数量
示例:
//查看活跃的频道 10.1.1.11:6379> pubsub channels 1) "channel:sports" //查看指定频道的订阅者数量 10.1.1.11:6379> pubsub numsub channel:sports 1) "channel:sports" 2) (integer) 1 //查看通过模式匹配订阅消息的订阅者数量 10.1.1.11:6379> pubsub numpat (integer) 1
7.2、使用场景
- 聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式,下面以简单的服务解耦进行说明。如图3-18所示,图中有两套业务,上面为视频管理系统,负责管理视频信息;下面为视频服务面向客户,用户可以通过各种客户端(手机、浏览器、接口)获取到视频信息。
- 假如视频管理员在视频管理系统中对视频信息进行了变更,希望及时通知给视频服务端,就可以采用发布订阅的模式,发布视频信息变化的消息到指定频道,视频服务订阅这个频道及时更新视频信息,通过这种方式可以有效解决两个业务的耦合性。
//视频服务订阅video:changes频道 subscribe video:changes //视频管理系统发布消息到video:changes频道 publish video:changes "video1,video3,video5" //当视频服务收到消息,对视频信息进行更新 for video in video1,video3,video5 update {video}
8、GEO
- Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需要实现这些功能的开发者来说是一大福音。GEO功能是Redis的另一位作者Matt Stancliff借鉴NoSQL数据库Ardb实现的,Ardb的作者来自中国,它提供了优秀的GEO功能。
- 表3-7展示5个城市的经纬度。
1、增加地理位置信息
GEOADD key longitude latitude member [longitude latitude member ...] longitude、latitude、member分别是该地理位置的经度、纬度、成员
- 如果需要更新地理位置信息,仍然可以使用geoadd命令,虽然返回结果为0。
示例:
//将北京的地理位置信息添加到地理位置信息的集合cities:locations,返回结果代表添加成功的个数 10.1.1.11:6379> geoadd cities:locations 116.28 39.55 beijing (integer) 1 //同时添加多个地理位置信息 10.1.1.11:6379> geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding (integer) 4
2、获取地理位置信息
GEOPOS key member [member ...]
示例:
//获取天津的经维度 10.1.1.11:6379> geopos cities:locations tianjin 1) 1) "117.12000042200088501" 2) "39.0800000535766543"
3、获取两个地理位置的距离
GEODIST key member1 member2 [m|km|ft|mi] m(meters)代表米。 km(kilometers)代表公里。 ft(feet)代表尺。 mi(miles)代表英里。
示例:
//计算天津到北京的距离,并以公里为单位: 10.1.1.11:6379> geodist cities:locations tianjin beijing km "89.2061"
4、获取指定位置范围内的地理信息位置集合
- georadius和georadiusbymember两个命令的作用是一样的,都是以一个地理位置为中心算出指定半径内的其他地理信息位置,不同的是georadius命令的中心位置给出了具体的经纬度,georadiusbymember只需给出成员即可。
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] radiusm|km|ft|mi是必需参数,指定了半径(带单位) withcoord:返回结果中包含经纬度。 withdist:返回结果中包含离中心节点位置的距离。 withhash:返回结果中包含geohash,有关geohash后面介绍。 COUNT count:指定返回结果的数量。 asc|desc:返回结果按照离中心节点的距离做升序或者降序。 store key:将返回结果的地理位置信息保存到指定键。 storedist key:将返回结果离中心节点的距离保存到指定键。
示例:
//计算五座城市中,距离北京150公里以内的城市: 10.1.1.11:6379> georadiusbymember cities:locations beijing 150 km 1) "beijing" 2) "tianjin" 3) "tangshan" 4) "baoding"
5、获取geohash
- Redis使用geohash将二维经纬度转换为一维字符串。
GEOHASH key member [member ...]
- geohash有如下特点:
- GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
- 字符串越长,表示的位置更精确,表3-8给出了字符串长度对应的精度。
- 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
- geohash编码和经纬度是可以相互转换的。
示例:
10.1.1.11:6379> geohash cities:locations beijing 1) "wx48ypbe2q0" 10.1.1.11:6379> type cities:locations zset
6、删除地理位置信息
- GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。
ZREM key member [member ...]