共计 4829 个字符,预计需要花费 13 分钟才能阅读完成。
键的过期策略
在 redis 中当键到了过期的时间,就会立马被删除掉吗?
我们了解一下删除策略的知识,删除策略可分为三种
-
定时删除
(对内存友好,对 CPU 不友好) -
惰性删除
(对 CPU 极度友好,对内存极度不友好) -
定期删除
(折中)
惰性删除是指每次从键空间取键的时候,判断一下该键是否过期了,如果过期了就删除。
Redis 采用的是惰性删除 + 定期删除两种策略,所以说,在 Redis 里边如果键到了过期的时间了,未必被立马删除的!
所谓 定期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
假设 redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。
注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的 灾难。
实际上 redis 是每隔 100ms 随机抽取 一些 key 来检查和删除的。
但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?
所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。
但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?
答案是:走内存淘汰机制。
内存淘汰机制
如果定期删除漏掉了很多过期 key,也没及时去查(没走惰性删除),大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整?
我们可以设置内存最大使用量,当内存使用量超出时,会施行 数据淘汰策略。
Redis 的内存淘汰机制有以下几种:
volatile-lru: 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl: 从已设置过期时间的数据集中挑选将要过期的数据淘汰。
volatile-random: 从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru: 从数据集中挑选最近最少使用的数据淘汰
allkeys-random: 从数据集中任意选择数据淘汰
no-enviction: 当内存达到限制的时候,不淘汰任何数据
慢查询
简介
和很多关系型数据库一样,Redis 也提供了慢查询日志记录,Redis 会把执行比较慢的命令放到内部的一个 list 列表中。
注意:慢查询记录的只是命令的执行时间,不包括网络传输和排队时间。
慢查询配置
关于 Redis 慢查询的配置有两个,分别是 slowlog-log-slower-than
和 slowlog-max-len
slowlog-log-slower-than
用来控制慢查询的阈值,所有执行时间超过该值的命令都会被记录下来。该值的单位为 微秒
,默认值为 10000,如果设置为 0,那么所有的记录都会被记录下来,如果设置为小于 0 的值,那么对于任何命令都不会记录,即关闭了慢查询。可以通过在配置文件中设置,或者用 config set 命令来设置:
config set slowlog-log-slower-than 10000
slowlog-max-len
用来设置存储慢查询记录列表的大小,默认值为 128,当该列表满了时,如果有新的记录进来,那么 Redis 会把队最旧的记录清理掉,然后存储新的记录。在生产环境我们可以适当调大,比如调成 1000,这样就可以缓冲更多的记录,方便故障的排查。配置方法和 slowlog-log-slower-than 类似,可以在配置文件中指定,也可以在命令行执行 config set 来设置:
config set slowlog-max-len 1000
查看慢查询日志
查询日志
Redis 专门提供了一组命令来查询慢查询日志:
SLOWLOG GET
可以看到这里查到了两条慢查询记录,分别是 ZINCRRBY 和 ZREVRANGE 命令
那么记录的中的 1)2)3)4)分别表示什么呢?
1)表示日志唯一标识符 uid
2)命令执行时系统的时间戳
3)命令执行的时长,以微妙来计算
4)命令和命令的参数
获取当前慢查询日志记录数
SLOWLOG LEN
慢查询日志重置
SLOWLOG RESET
Pipeline
pipeline
命令用来批量操作 redis 命令
由于 redis 是 单线程
的,下一次请求必须等待上一次请求执行完成后才能继续执行。执行 N 次命令需要 N 次网络时间 + 执行时间
,然而使用 Pipeline 模式,客户端可以一次性的发送多个命令,也就是 1 次网络时间 + N 次执行时间
,这样就大大的减少了网络往返时间,提高了系统性能。
下面看下伪代码:
Jedis redis = new Jedis("127.0.0.1", 6379);
Pipeline pipe = redis.pipelined();
for (int i = 0; i < 10000; i++) {pipe.hmset("key_" + i, data); // 将值封装到 PIPE 对象,此时并未执行,还停留在客户端
}
pipe.sync(); // 将封装后的 PIPE 一次性发给 redis
jedis.close;
那么问题来了,在什么样的情景下适合使用 pipeline 呢?
有些系统可能对可靠性要求很高,每次操作都需要立马知道这次操作是否成功,是否数据已经写进 redis 了,那这种场景就不适合。
还有的系统,可能是批量的将数据写入 redis,允许一定比例的写入失败,那么这种场景就可以使用了,比如 10000 条一下进入 redis,可能失败了 2 条无所谓,后期有补偿机制就行了,比如短信群发这种场景,如果一下群发 10000 条,按照第一种模式去实现,那这个请求过来,要很久才能给客户端响应,这个延迟就太长了,如果客户端请求设置了超时时间 5 秒,那肯定就抛出异常了,而且本身群发短信要求实时性也没那么高,这时候用 pipeline 最好了。
BitMap
Bitmap
是一串连续的 2 进制数字(0 或 1),每一位所在的位置为 偏移(offset)
,在 bitmap 上可执行 AND,OR,XOR 以及其它位操作。
API
SETBIT
SETBIT KEY OFFSET VALUE
该命令用于对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。时间复杂度 O(1)
在 redis 中,存储的字符串都是以 二进制
的形式存在的。
如何通过 SETBIT 命令将 ’a’ 变成 ’b’ 呢?即将 01100001 变成 01100010(b 的 ASCII 码是 98),其实就是将 ’a’ 中的 offset 6 从 0 变成 1,将 offset 7 从 1 变成 0。
每次 SETBIT 完毕之后,会返回该位置原先的 bit 值。
BITCOUNT
BITCOUNT KEY [START END]
该命令统计字符串中指定范围里 bit 被设置为 1 的数量
GETBIT
GETBIT KEY OFFSET
返回 key 对应的 string 在 offset 处的 bit 值
BITOP
BITOP operation destkey key [key…]
可以对多个二进制进行交集、并集等操作,并将结果保存到 destkey 上
实战
一个简单的例子:日活跃用户
为了统计今日登录的用户数,我们建立了一个 bitmap, 每一位标识一个用户 ID。当某个用户访问我们的网页或执行了某个操作,就在 bitmap 中把标识此用户的位置为 1。这样的话我们就可以通过 BITCOUNT
命令得到当日活跃用户数。
注意:使用 BitMap 统计流量适用于亿级流量系统,如果系统用户只有 10w 的话使用 set 数据结构会更好。
GEO
GEO
功能在 Redis3.2 版本提供,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。GEO 的数据类型为zset
。
geoadd
- 添加经纬度信息
geoadd cityGeo 116.405285 39.904989 "北京"
geoadd cityGeo 121.472644 31.231706 "上海"
geopos
- 查找指定 key 的经纬度信息,可以指定多个 key,批量返回
127.0.0.1:6379> geopos cityGeo 北京
1) "116.40528291463851929"
2) "39.9049884229125027"
geodist
- 返回两个地方的距离,可以指定单位,比如米 m,千米 km,英里 mi,英尺 ft
127.0.0.1:6379> geodist cityGeo 北京 上海
"1067597.9668"
127.0.0.1:6379> geodist cityGeo 北京 上海 km
"1067.5980"
georadius
- 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素
georadius cityGeo 116.405285 39.904989 100 km WITHDIST WITHCOORD ASC COUNT 5
可以指定 WITHDIST 返回距离,WITHCOORD 返回经纬度,WITHHASH 返回 geohash 值
可以指定 ASC 或 DESC,根据距离来排序
可以指定 COUNT 限定返回的记录数
georadiusbymember
- 和 georadius 一样,不过中心点是由给定的位置元素决定的,而不是像 georadius 那样, 使用输入的经度和纬度来决定中心点。
georadiusbymember cityGeo 北京 100 km WITHDIST WITHCOORD ASC COUNT 5
zrem
- GEO 没有提供删除成员的命令,但是因为 GEO 的底层实现是 zset,所以可以借用 zrem 命令实现对地理位置信息的删除.
zrem cityGeo 北京
布隆过滤器
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你“某样东西一定不存在或者可能存在”。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
原理
其本质就是一个只包含 0 和 1 的 bit 数组。具体操作当一个元素被加入到集合里面后,该元素通过 K 个 Hash 函数运算得到 K 个 hash 后的值,然后将 K 个值映射到这个位数组对应的位置,把对应位置的值设置为 1。查询是否存在时,我们就看对应的映射点位置如果全是 1,他就很可能存在(跟 hash 函数的个数和 hash 函数的设计有关),如果有一个位置是 0,那这个元素就一定不存在。
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值“baidu”和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为
Ok,我们现在再存一个值“tencent”,如果哈希函数返回 3、4、8 的话
值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询“dianping”这个值是否存在,哈希函数返回了 1、5、8 三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说“dianping”这个值不存在。而当我们需要查询“baidu”这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说“baidu”存在了么?答案是不可以,只能是“baidu”这个值可能存在。
这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值“taobao”即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1,那么程序还是会判断“taobao”这个值存在。
应用场景
常见的适用应用场景有,利用布隆过滤器减少磁盘 IO 或者网络请求,因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求,比如可以用来解决 缓存穿透
的问题。