【三】redis特点功能

系列其他文章

  1. redis简介
  2. redis基础命令与使用场景
  3. redis特点功能

redis特点功能

慢查询分析

慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阈值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,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"

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 命令的执行结果按顺序返回给客户端

当你要执行很多的命令并返回结果的时候,需要考虑 List 对象的大小,因为它会“吃掉”服务器上许多的内存空间,严重时会导致内存不足,引发 JVM 溢出异常,所以在工作环境中,是需要自己去评估的,可以考虑使用迭代的方式去处理。

事务与 Lua

multi 和 exec 命令

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简单事务不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,主要有以下几点:

  1. 不够满足原子性。一个事务执行过程中,其他事务或 client 是可以对相应的 key 进行修改的(并发情况下,例如电商常见的超卖问题),想要避免这样的并发性问题就需要使用 WATCH 命令,但是通常来说,必须经过仔细考虑才能决定究竟需要对哪些 key 进行 WATCH 加锁。然而,额外的 WATCH 会增加事务失败的可能,而缺少必要的 WATCH 又会让我们的程序产生竞争条件。
  2. 后执行的命令无法依赖先执行命令的结果。由于事务中的所有命令都是互相独立的,在遇到 exec 命令之前并没有真正的执行,所以我们无法在事务中的命令中使用前面命令的查询结果。我们唯一可以做的就是通过 watch 保证在我们进行修改时,如果其它事务刚好进行了修改,则我们的修改停止,然后应用层做相应的处理。
  3. 事务中的每条命令都会与 Redis 服务器进行网络交互。Redis 事务开启之后,每执行一个操作返回的都是 queued,这里就涉及到客户端与服务器端的多次交互,明明是需要一次批量执行的 n 条命令,还需要通过多次网络交互,显然非常浪费(这个就是为什么会有 pipeline 的原因,减少 RTT 的时间)。

Redis 事务缺陷的解决 – Lua

Lua 是一个小巧的脚本语言,用标准 C 编写,几乎在所有操作系统和平台上都可以编译运行。一个完整的 Lua 解释器不过 200k,在目前所有脚本引擎中,Lua 的速度是最快的,这一切都决定了 Lua 是作为嵌入式脚本的最佳选择。

Redis 2.6 版本之后内嵌了一个 Lua 解释器,可以用于一些简单的事务与逻辑运算,也可帮助开发者定制自己的 Redis 命令(例如:一次性的执行复杂的操作,和带有逻辑判断的操作),在这之前,必须修改源码。

在 Redis 中执行 Lua 脚本有两种方法:eval 和 evalsha,这里以 eval 做为案例介绍:

eval 语法:

eval script numkeys key [key ...] arg [arg ...]

其中:

  • script 一段 Lua 脚本或 Lua 脚本文件所在路径及文件名
  • numkeys Lua 脚本对应参数数量
  • key [key …] Lua 中通过全局变量 KEYS 数组存储的传入参数
  • arg [arg …] Lua 中通过全局变量 ARGV 数组存储的传入附加参数
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");

Bitmaps

许多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使用率和开发效率。Redis 提供了 Bitmaps 这个“数据结构”可以实现对位的操作。把数据结构加上引号主要因为:

  • Bitmaps 本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作。
  • Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标在 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 并不是一种新的数据结构(实际类型为字符串类型),而 是一种基数算法,通过 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 结构选型时只需要确认如下两条即可:

  1. 只为了计算独立总数,不需要获取单条数据。
  2. 可以容忍一定误差率,毕竟 HyperLogLog 在内存的占用量上有很大的优势。

例如:如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 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 来解决。对于这三种解决方案,这边做下对比:

  • HashMap:算法简单,统计精度高,对于少量数据建议使用,但是对于大量的数据会占用很大内存空间;
  • BitMap:位图算法,具体内容可以参考我的这篇文章,统计精度高,虽然内存占用要比 HashMap 少,但是对于大量数据还是会占用较大内存;
  • HyperLogLog:存在一定误差,占用内存少,稳定占用 12k 左右内存,可以统计 2^64 个元素,对于上面举例的应用场景,建议使用。

GEO

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

Stream 其实就是redis实现的类似kafka的消息功能,概念上也有借鉴kafka。
以下只做简单介绍:
Stream 是 Redis 5.0 引入的一种新数据类型。 上述的发布/订阅有如下缺点:

  • 消费者下线, 数据会丢失
  • 消息无法持久化
  • 消息堆积,缓冲区溢出,消费者会被强制踢下线,数据也会丢失(默认缓冲区超过32m或者缓冲器超过8m且持续60秒就会下线消费者)

所以redis使用Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置。

Redis 客户端

java常用的客户端有三种:
Jedis、lettuce、Redisson。

  • Jedis:老牌的 Redis 的 Java 实现客户端,提供了比较全面的 Redis 命令的支持
    • 官方文档:https://github.com/redis/jedis
    • 优点:支持全面的 Redis 操作特性(可以理解为API比较全面)。
    • 缺点:使用阻塞的 I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行,不支持异步
      Jedis 客户端实例不是线程安全的,所以需要通过连接池来使用 Jedis。
  • lettuce:( [ˈletɪs]),是一种可扩展的线程安全的 Redis 客户端,支持异步模式。lettuce 底层基于 Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。
    • 官网地址:https://lettuce.io/
    • 优点:支持同步异步通信模式;
      Lettuce 的 API 是线程安全的,如果不是执行阻塞和事务操作,如BLPOP和MULTI/EXEC,多个线程就可以共享一个连接
    • 缺点:API 更抽象,学习使用成本高
  • Redisson:提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务。Redisson 提供了使用Redis 的最简单和最便捷的方法,让使用者能够将精力更集中地放在处理业务逻辑上。
    • 官网地址:https://redisson.org/
    • 优点:使用者对 Redis 的关注分离,可以类比 Spring 框架,这些框架搭建了应用程序的基础框架和功能,提升开发效率,让开发者有更多的时间来关注业务逻辑;
      提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列等。
    • 缺点:Redisson 对字符串的操作支持比较差。

其他编程语言对应的常用 Redis 客户端,例如:

  • python -> redis-py
  • node -> ioredis

具体使用语法,大家可以根据自己的需要查找对应的官方文档:

redis-py 文档:https://github.com/redis/redis-py

ioredis 文档:https://github.com/luin/ioredis


文章参考:https://mp.weixin.qq.com/s/-3fcK4WspGk6SEsaVrdx8A

你可能感兴趣的:(redis,redis,数据库,缓存)