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 ...]
posted @ 2022-08-12 17:27  麦恒  阅读(37)  评论(0编辑  收藏  举报