慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阈值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis 也提供了类似的功能
slowlog-log-slower-than: 慢查询时间阈值设置
单位是微妙(1 秒 = 1000 毫秒 = 1000000 微秒),默认是 10000 微秒,如果把 slowlog-log-slower-than 设置为 0,将会记录所有命令到日志中。如果把 slowlog-log-slower-than 设置小于 0,将会不记录任何命令到日志中。
slowlog-max-len: 慢查询存储条数
当慢查询日志列表已经达到最大长度时,最早插入的那条命令将被从列表中移出
建议将慢查询日志的长度调整的大一些。比如可以设置为 1000 以上
除了去配置文件中修改,也可以通过 config set 命令动态修改配置
\> config set slowlog-log-slower-than 1000
OK
\> config set slowlog-max-len 1200
OK
\> config rewrite
OK
slowlog get
: 获取慢查询日志. 后面加数字,用于指定获取慢查询日志的条数
> slowlog get 3
1) 1) (integer) 6107 //唯一标识id
2) (integer) 1616398930 //命令执行的时间戳
3) (integer) 3109 //命令执行时长
4) 1) "config" //执行的命令和参数
2) "rewrite"
2) 1) (integer) 6106
2) (integer) 1613701788
3) (integer) 36004
4) 1) "flushall"
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 命令的执行结果按顺序返回给客户端
当你要执行很多的命令并返回结果的时候,需要考虑 List 对象的大小,因为它会“吃掉”服务器上许多的内存空间,严重时会导致内存不足,引发 JVM 溢出异常,所以在工作环境中,是需要自己去评估的,可以考虑使用迭代的方式去处理。
Redis 提供了简单的事务功能,将一组需要一起执行的命令放到 multi 和 exec 两个命令之间。Multi 命令代表事务开始,exec 命令代表事务结束,它们之间的命令是原子顺序执行的
使用案例:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> SET msg "hello chrootliu"
QUEUED
127.0.0.1:6379> GET msg
QUEUED
127.0.0.1:6379> EXEC
1) OK
1) hello chrootliu
redis简单事务不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,主要有以下几点:
Lua 是一个小巧的脚本语言,用标准 C 编写,几乎在所有操作系统和平台上都可以编译运行。一个完整的 Lua 解释器不过 200k,在目前所有脚本引擎中,Lua 的速度是最快的,这一切都决定了 Lua 是作为嵌入式脚本的最佳选择。
Redis 2.6 版本之后内嵌了一个 Lua 解释器,可以用于一些简单的事务与逻辑运算,也可帮助开发者定制自己的 Redis 命令(例如:一次性的执行复杂的操作,和带有逻辑判断的操作),在这之前,必须修改源码。
在 Redis 中执行 Lua 脚本有两种方法:eval 和 evalsha,这里以 eval 做为案例介绍:
eval 语法:
eval script numkeys key [key ...] arg [arg ...]
其中:
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
SCRIPT LOAD 与 EVALSHA 命令
对于不立即执行的 Lua 脚本,或需要重用的 Lua 脚本,可以通过 SCRIPT LOAD 提前载入 Lua 脚本,这个命令会立即返回对应的 SHA1 校验码
当需要执行函数时,通过 EVALSHA 调用 SCRIPT LOAD 返回的 SHA1 即可
SCRIPT LOAD "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
"232fd51614574cf0867b83d384a5e898cfd24e5a"
EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
通过 Lua 脚本执行 Redis 命令
在 Lua 脚本中,只要使用 redis.call()
或 redis.pcall()
传入 Redis 命令就可以直接执行:
eval "return redis.call('set',KEYS[1],'bar')" 1 foo
//等同于在服务端执行 set foo bar
案例,使用 Lua 脚本实现访问频率限制:
--
-- KEYS[1] 要限制的ip
-- ARGV[1] 限制的访问次数
-- ARGV[2] 限制的时间
--
local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]
local is_exists = redis.call("EXISTS", key)
if is_exists == 1then
if redis.call("INCR", key) > limit then
return0
else
return1
end
else
redis.call("SET", key, 1)
redis.call("EXPIRE", key, expire_time)
return1
end
使用方法,通过:
eval(file_get_contents(storage_path("limit.lua")), 3, "127.0.0.1", "3", "100");
许多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使用率和开发效率。Redis 提供了 Bitmaps 这个“数据结构”可以实现对位的操作。把数据结构加上引号主要因为:
在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录, 签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位, 365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。
语法:
setbit key offset value // 设置或者清空 key 的 value(字符串)在 offset 处的 bit 值
getbit key offset // 返回 key 对应的 string 在 offset 处的 bit 值
bitcount key [start end] // start end 范围内被设置为1的数量,不传递 start end 默认全范围
使用案例,统计用户登录(活跃)情况
127.0.0.1:6379> setbit userLogin:2021-04-10 66666 1 //userId=66666的用户登录,这是今天登录的第一个用户。
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 999999 1 //userId=999999的用户登录,这是今天第二个登录、的用户。
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 3333 1
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 8888 1
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 100000 1
(integer) 0
127.0.0.1:6379> getbit active:2021-04-10 66666
(integer) 1
127.0.0.1:6379> getbit active:2021-04-10 55555
(integer)
127.0.0.1:6379> bitcount active:2021-04-10
(integer) 5
由于 bit 数组的每个位置只能存储 0 或者 1 这两个状态;所以对于实际生活中,处理两个状态的业务场景就可以考虑使用 bitmaps。如用户登录/未登录,签到/未签到,关注/未关注,打卡/未打卡等。同时 bitmap 还通过了相关的统计方法进行快速统计。
HyperLogLog 并不是一种新的数据结构(实际类型为字符串类型),而 是一种基数算法,通过 HyperLogLog 可以利用极小的内存空间完成独立总数的统计,数据集可以是 IP、Email、ID 等
HyperLogLog 提供了 3 个命令:pfadd、pfcount、pfmerge。
// 用于向 HyperLogLog 添加元素
// 如果 HyperLogLog 估计的近似基数在 PFADD 命令执行之后出现了变化, 那么命令返回 1 , 否则返回 0
// 如果命令执行时给定的键不存在, 那么程序将先创建一个空的 HyperLogLog 结构, 然后再执行命令
pfadd key value1 [value2 value3]
// PFCOUNT 命令会给出 HyperLogLog 包含的近似基数
// 在计算出基数后, PFCOUNT 会将值存储在 HyperLogLog 中进行缓存,知道下次 PFADD 执行成功前,就都不需要再次进行基数的计算。
pfcount key
// PFMERGE 将多个 HyperLogLog 合并为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的并集基数。
pfmerge destkey key1 key2 [...keyn]
使用示例:
127.0.0.1:6379> pfadd totaluv user1
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 1
127.0.0.1:6379> pfadd totaluv user2
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 2
127.0.0.1:6379> pfadd totaluv user3
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 3
127.0.0.1:6379> pfadd totaluv user4
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 4
127.0.0.1:6379> pfadd totaluv user5
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 5
127.0.0.1:6379> pfadd totaluv user6 user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 10
HyperLogLog 内存占用量非常小,但是存在错误率,开发者在进行数据 229 结构选型时只需要确认如下两条即可:
例如:如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 UV 数据,然后让你来开发这个统计模块,你会如何实现?
如果统计 PV(页面访问量,同一用户访问累计) 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器 的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。
但是 UV(独立访问用户数,同一用户访问不累计) 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就 要求每一个网页请求都需要带上用户的 ID,无论是登录用户还是未登录用户都需要一个唯一 ID 来标识。
你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合来存储所 有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可 以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。没错,这是一个非常简单的方案。
但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大 的 set 集合来统计,这就非常浪费空间。如果这样的页面很多,那所需要的存储空间是惊人 的。为这样一个去重功能就耗费这样多的存储空间,值得么?其实老板需要的数据又不需要 太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解 决方案呢?
Redis 提供了 HyperLogLog 数据结构就是用来解决 这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。
对于上面的场景,同学们可能有疑问,我或许同样可以使用 HashMap、BitMap 和 HyperLogLog 来解决。对于这三种解决方案,这边做下对比:
Redis3.2 版本提供了 GEO(地理信息定位)功能,支持存储地理位置信 息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需 要实现这些功能的开发者来说是一大福音。GEO 功能是 Redis 的另一位作者 Matt Stancliff 借鉴 NoSQL 数据库 Ardb 实现的,Ardb 的作者来自中国,它提供了优秀的 GEO 功能。
Redis GEO 相关的命令如下:
// 添加一个空间元素,longitude、latitude、member分别是该地理位置的经度、纬度、成员
// 这里的成员就是指代具体的业务数据,比如说用户的ID等
// 需要注意的是Redis的纬度有效范围不是[-90,90]而是[-85,85]
// 如果在添加一个空间元素时,这个元素中的menber已经存在key中,那么GEOADD命令会返回0,相当于更新了这个menber的位置信息
GEOADD key longitude latitude member [longitude latitude member]
// 用于添加城市的坐标信息
geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding
// 获取地理位置信息
geopos key member [member ...]
// 获取天津的坐标
geopos cities:locations tianjin
// 获取两个坐标之间的距离
// unit代表单位,有4个单位值
- m (meter) 代表米
- km (kilometer)代表千米
- mi (miles)代表英里
- ft (ft)代表尺
geodist key member1 member2 [unit]
// 获取天津和保定之间的距离
GEODIST cities:locations tianjin baoding km
// 获取指定位置范围内的地理信息位置集合,此命令可以用于实现附近的人的功能
// georadius和georadiusbymember两个命令的作用是一样的,都是以一个地理位置为中心算出指定半径内的其他地理信息位置,不同的是georadius命令的中心位置给出了具体的经纬度,georadiusbymember只需给出成员即可。其中radiusm|km|ft|mi是必需参数,指定了半径(带单位),这两个命令有很多可选参数,参数含义如下:
// - withcoord:返回结果中包含经纬度。
// - withdist:返回结果中包含离中心节点位置的距离。
// - withhash:返回结果中包含geohash,有关geohash后面介绍。
// - COUNT count:指定返回结果的数量。
// - asc|desc:返回结果按照离中心节点的距离做升序或者降序。
// - store key:将返回结果的地理位置信息保存到指定键。
// - storedist key:将返回结果离中心节点的距离保存到指定键。
georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
// 获取geo hash
// Redis使用geohash将二维经纬度转换为一维字符串,geohash有如下特点:
// - GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
// - 字符串越长,表示的位置更精确,表3-8给出了字符串长度对应的精度,例如geohash长度为9时,精度在2米左右。长度和精度的对应关系,请参考:https://easyreadfs.nosdn.127.net/9F42_CKRFsfc8SUALbHKog==/8796093023252281390
// - 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
// - geohash编码和经纬度是可以相互转换的。
// - Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。
geohash key member [member ...]
// 删除操作,GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。
zrem key member
使用案例,例如咋部门是做直播的,那直播业务一般会有一个“附近的直播”功能,这里就可以考虑用 Redis 的 GEO 技术来完成这个功能。
数据操作主要有两个:一是主播开播的时候写入主播 Id 的经纬度,二是主播关播的时候删除主播 Id 元素。这样就维护了一个具有位置信息的在线主播集合提供给线上检索。
大家具体使用的时候,可以去了解一下 Redis GEO 原理,主要用到了空间索引的算法 GEOHASH 的相关知识,针对索引我们日常所见都是一维的字符,那么如何对三维空间里面的坐标点建立索引呢,直接点就是三维变二维,二维变一维。这里就不再详细阐述了
Redis 提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消 息,订阅该频道的每个客户端都可以收到该消息:
主要对应的 Redis 命令为:
subscribe channel [channel ...] // 订阅一个或多个频道
unsubscribe channel // 退订指定频道
publish channel message // 发送消息
psubscribe pattern // 订阅指定模式
punsubscribe pattern // 退订指定模式
使用案例:
打开一个 Redis 客户端,如向 TestChanne 说一声 hello:
127.0.0.1:6379> publish TestChanne hello
(integer) 1 // 返回的是接收这条消息的订阅者数量
这样消息就发出去了。发出去的消息不会被持久化,也就是有客户端订阅 TestChanne 后只能接收到后续发布到该频道的消息,之前的就接收不到了。
打开另一 Redis 个客户端,这里假设发送消息之前就打开并且订阅了 TestChanne 频道:
127.0.0.1:6379> subscribe TestChanne // 执行上面命令客户端会进入订阅状态
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 消息类型
2) "TestChanne" // 频道
3) "hello" // 消息内容
我们可以利用 Redis 发布订阅功能,实现的简单 MQ 功能,实现上下游的解耦。不过需要注意了,由于 Redis 发布的消息不会被持久化,这就会导致新订阅的客户端将不会收到历史消息。所以,如果当前的业务场景不能容忍这些缺点,那还是用专业 MQ 吧。
Stream 其实就是redis实现的类似kafka的消息功能,概念上也有借鉴kafka。
以下只做简单介绍:
Stream 是 Redis 5.0 引入的一种新数据类型。 上述的发布/订阅有如下缺点:
所以redis使用Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置。
java常用的客户端有三种:
Jedis、lettuce、Redisson。
其他编程语言对应的常用 Redis 客户端,例如:
具体使用语法,大家可以根据自己的需要查找对应的官方文档:
redis-py 文档:https://github.com/redis/redis-py
ioredis 文档:https://github.com/luin/ioredis
文章参考:https://mp.weixin.qq.com/s/-3fcK4WspGk6SEsaVrdx8A