目录

  • 一、key
  • 1.1 redis官网对key的描述有如下的一些规则
  • 二、查找key
  • 2.1keys 命令
  • 2.2 scan 命令
  • scan 遍历顺序
  • Redis 字典扩容
  • 大Key的处理
  • 三、key过期策略
  • 3.1Redis的中key过期时间
  • 3.2 过期的key 集合
  • 3.3 从节点过期策略
  • 四、LRU


一、key

redis 是典型K-V 数据库,数据库中的Key 是由字典组成
dict本质上是为了解决算法中的查找问题(Searching),一般查找问题的解法分为两个大类:一个是基于各种平衡树,一个是基于哈希表。我们平常使用的各种Map或dictionary,大都是基于哈希表实现的。在不要求数据有序存储,且能保持较低的哈希值冲突概率的前提下,基于哈希表的查找性能能做到非常高效,接近O(1),而且实现简单。

redis 中定义的dict和Java中的hash map类似,注意原理类似,所以Redis中的key的查找定位使用了比较key的hash 算法,如果hash冲突少,接近o(1)

Redis 官网对Key的规范

1.1 redis官网对key的描述有如下的一些规则

非常长的key是不推荐的。一个1024 bytes是一个非常坏的注意,不仅仅是因为内存浪费,更是因为在数据集中搜索对比的时候需要耗费更多的成本。当要处理的是匹配一个非常大的值,从内存和带宽的角度来看,使用这个值的hash值是更好的办法(比如使用SHA1)。
特别短的key通常也是不推荐的。在写像u100flw这样的键的时候,有一个小小的要点,我们可以用user:1000:followers代替。可读性更好,对于key对象和value对象增加的空间占用与此相比来说倒是次要的。当短的key可以很明显减少空间占用的时候,你的工作就是找到正确的平衡。
尝试去固定一个schema。比如object-type:id是一个好主意,-和.通常用于多个字符的域,就像comment🔢reply.to,或者comment🔢reply-to。
最大的key允许512MB。

链接:https://zhuanlan.zhihu.com/p/147841729

经过实践:
当key的长度不超过1024即1kb的长度的时候,基本上对性能不造成影响,但是一旦超过1024长度,随着key长度的增加,耗时也会随之增加。
所以,key长度对redis读写性能的影响是当key长度超过1024字节!因此我们在实际开发过程中可以根据自己的key长度预估对redis是否存在性能影响。
在实际业务开发中,基本上大家的key不会超过1024字节,因此可以在命名的时候,尽量取一些能见名知义的key,不必刻意为了缩短key长度而降低key的可读性。
当有这种key就必须特别长的时候,或者不确定是否超过1024字节,我们可以对key做一次hash后取哈希值作为redis的key,这样就可以大幅提高redis的性能了。这里推荐大家使用Murmurhash算法,算法详情见我的文章:MurmurHash算法及应用场景

Key的规范

(1)【建议】: 可读性和可管理性 以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id (2)【建议】:
简洁性
保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:U_O_P_HS:#userId代表用户已经购买的商品Id的hash存储
(3)【强制】:不要包含特殊字符 反例:包含空格、换行、单双引号以及其他转义字符

Value的规范

(1)【强制】:拒绝bigkey(防止网卡流量、慢查询)
string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。
反例:一个包含200万个元素的list。非字符串的bigkey,不要使用del删除,
使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查)),查找方法和删除方法
(2)【推荐】:选择适合的数据类型。
例如:实体类型(要合理控制和使用数据结构内存编码优化配置,例如ziplist,但也要注意节省内存和性能之间的平衡) 正例:hmset
user:1 name tom age 19 favor football (3).【推荐】:控制key的生命周期,redis不是垃圾桶。
建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime。

命令使用

【推荐】O(N)命令关注N的数量
例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。
【推荐】禁用命令
禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。
【推荐】使用批量操作提高效率 原生命令:例如mget、mset。 非原生命令:可以使用pipeline提高效率。
但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。

二、查找key

2.1keys 命令

没有offset、limit 参数,一次性返回所有满足条件的key
百万个key性能效率低

keys是遍历算法,复杂度O(n),如果服务中有千万级key,造成服务的卡顿
,其他指令被延时或者包超时。 Redis 的单线程。

2.2 scan 命令

1.复杂度O(n),通过游标分布进行,不会阻碍主线程。

注意:Redis的并不是严格意义的单线程,但对于设计数据查询,增删的数据服务线程是单一的,这样避免了复杂的锁,简化了实现难度,Redis吧内存内存管理依托于操作系统,疑问C语言的实现应该是自己管理内存

2.提供limit 参数,可以控制返回结果的最大条数,但是limit只是一个hint

3.同 keys 一样,它也提供模式匹配功能;
4.服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
5.返回的结果可能会有重复,需要客户端去重复,这点非常重要;
6.遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
7.单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;

回顾:字典扩容 数组每次翻倍
扩容时机根据代码研读获取,目前c语言基础不足

scan 指令返回的游标是字典数组的位置索引,称为槽 slot

注意这里的槽和Redis cluster 中的slot 不是一个概念,尽管大家名称一样,这点可以利用定语区分,字典槽(hash-slot)

scan 遍历顺序

关于scan命令的遍历顺序,我们可以用一个小栗子来具体看一下。

127.0.0.1:6379> keys *
1) "db_number"
2) "key1"
3) "myKey"
127.0.0.1:6379> scan 0 MATCH * COUNT 1
1) "2"
2) 1) "db_number"
127.0.0.1:6379> scan 2 MATCH * COUNT 1
1) "1"
2) 1) "myKey"
127.0.0.1:6379> scan 1 MATCH * COUNT 1
1) "3"
2) 1) "key1"
127.0.0.1:6379> scan 3 MATCH * COUNT 1
1) "0"
2) (empty list or set)

我们的Redis中有3个key,我们每次只遍历一个一维数组中的元素。如上所示,SCAN命令的遍历顺序是
0->2->1->3
这个顺序看起来有些奇怪。我们把它转换成二进制就好理解一些了。
00->10->01->11
我们发现每次这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的。这一点我们在redis的源码中也得到印证。
在dict.c文件的dictScan函数中对游标进行了如下处理
v = rev(v);
v++;
v = rev(v);

意思是,将游标倒置,加一后,再倒置,也就是我们所说的“高位加1”的操作。
这里大家可能会有疑问了,为什么要使用这样的顺序进行遍历,而不是用正常的0、1、2……这样的顺序呢,这是因为需要考虑遍历时发生字典扩容与缩容的情况(不得不佩服开发者考虑问题的全面性)。

链接:https://www.jianshu.com/p/be15dc89a3e8

Redis 字典扩容

举例:假设开始的时候字典槽位二进制数是XXX,那么该槽位中的元素被rehash到0XX和1XXX(XXX+8)中,如果字典长度由16位扩展到32位,那么对于二进制槽位XXXX中元素,将被rehash到0XXXX和1XXXX(XXXX+16)中。

大Key的处理

如果key太大,大于1kb
在集群的情况下迁移会有问题,在内存分配时,一个key太大,申请扩容时,一次性申请更大的一块内存也会导致卡顿,如果这个大key被删除,内存会被一次性回收,

上面这样的过程需要编写脚本,比较繁琐,不过 Redis 官方已经在 redis-cli 指令中提供了这样的扫描功能,我们可以直接拿来即用。
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys

如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参数。
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1

三、key过期策略

3.1Redis的中key过期时间

Redis所有的数据结构都可以设置过期时间,时间一到,就会被自动删除。

EXPIRE key seconds  //将key的生存时间设置为ttl秒
PEXPIRE key milliseconds  //将key的生成时间设置为ttl毫秒
EXPIREAT key timestamp  //将key的过期时间设置为timestamp所代表的的秒数的时间戳
PEXPIREAT key milliseconds-timestamp  //将key的过期时间设置为timestamp所代表的的毫秒数的时间戳

3.2 过期的key 集合

Redis 会将每个设置了过期时间的key放入一个独立的字典中,以后会定时遍历这个字典来删除过期的到期的key。

  • 1.定时遍历
  • 2.惰性策略 访问这个key的时候,Redis 对key的过期时间进行检验,若过期则立即删除。

思考扩展:定时开卖怎么设计,如果是10点钟开始抢,那么逻辑怎么处理。

Redis 默认每秒进行10次过期扫描,扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略

  1. 从过期字典中随机选择20个key
  2. 删除20个key中已经过期的key
  3. 若过期的key的比例超过四分之一,则重复步骤一

Redis设置扫描时间不会超过25ms,避免出现循环过度,线程卡死。
说明 Redis中过期key的处理线程就是Redis的主线程。

假设一个大型Redis所有key 在同一时间过期

这个问题从服务层的角度考虑:
1.避免查询达到mysql

同缓存雪崩
2.从Redis 角度考虑

Redis 会持续扫描过期字典(循环多次),
直到过期字典中的key变的稀疏,才会停止,循环次数明显下降。
这就会导致主线程上的读写请求出现明显卡顿,导致这种卡顿的另外一种原因是内存管理器需要频繁回收内存,这也会产生一定的cpu消耗。

若超时的时间设置过短10ms
就会出现大量连接因为超时关闭,业务端异常,并且无法从slowlog 中看出慢查询记录,慢查询指的是逻辑处理过慢,不包含等待时间。

3.3 从节点过期策略

从节点不会进行过期扫描,从节点对过期的处理是被动的,通过AOF指令中增加一条del指令,同步到所有的从节点,从节点执行del 来删除过期的key.

因为指令同步是异步执行的,会出现主从节点不一致。
会影响分布式锁。主从节点切换锁没有同步过来。

四、LRU

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

LRU逻辑可以使用多种数据结构实现

当redis内存超出物理内存的限制时,内存的数据就会和磁盘产生频繁的交换,swap
,这会让Redis的性能急剧下降,对于访问量比较大的Redis 来说,这样基本等于不可用

生产环境不允许Redis 出现交换的行为,为了限制最大内存使用,Redis 提供了配置参数maxmemory来限制内存超出期望的大小。

当实际内存超出maxmemory时,Redis提供了几种可选策略:

noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。淘汰最近最少使用的key
volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。
volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。淘汰随机的key数据
allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。
volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰。
设置缓存的最大内存+ 淘汰策略:
maxmemory
maxmemory-policy noeviction