Redis常用命令

通用命令

通用指令是不分数据类型,都可以使用的指令,常见的有:

  • KEYS:查看符合模板的所有key

  • DEL:删除一个指定的key

  • EXISTS:判断key是否存在

  • EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除

  • TTL:查看一个KEY的剩余有效期

String命令

  • SET:添加或者修改已经存在的一个String类型的键值对

  • GET:根据key获取String类型的value

  • MSET:批量添加多个String类型的键值对

  • MGET:根据多个key获取多个String类型的value

  • INCR:让一个整型的key自增1

  • INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2

  • INCRBYFLOAT:让一个浮点类型的数字自增并指定步长

  • SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行

  • SETEX:添加一个String类型的键值对,并且指定有效期

Hash命令

  • HSET key field value:添加或者修改hash类型key的field的值

  • HGET key field:获取一个hash类型key的field的值

  • HMSET:批量添加多个hash类型key的field的值

  • HMGET:批量获取多个hash类型key的field的值

  • HGETALL:获取一个hash类型的key中的所有的field和value

  • HKEYS:获取一个hash类型的key中的所有的field

  • HINCRBY:让一个hash类型key的字段值自增并指定步长

  • HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行

List命令

  • LPUSH key element ... :向列表左侧插入一个或多个元素

  • LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil

  • RPUSH key element ... :向列表右侧插入一个或多个元素

  • RPOP key:移除并返回列表右侧的第一个元素

  • LRANGE key star end:返回一段角标范围内的所有元素

  • BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil

Set命令

  • SADD key member ... :向set中添加一个或多个元素

  • SREM key member ... : 移除set中的指定元素

  • SCARD key: 返回set中元素的个数

  • SISMEMBER key member:判断一个元素是否存在于set中

  • SMEMBERS:获取set中的所有元素

  • SINTER key1 key2 ... :求key1与key2的交集

  • SDIFF key1 key2 ... :求key1与key2的差集

  • SUNION key1 key2 ..:求key1和key2的并集

SortedSet命令

  • ZADD key score member:添加一个或多个元素到sorted set ,如果已经存在则更新其score值

  • ZREM key member:删除sorted set中的一个指定元素

  • ZSCORE key member : 获取sorted set中的指定元素的score值

  • ZRANK key member:获取sorted set 中的指定元素的排名

  • ZCARD key:获取sorted set中的元素个数

  • ZCOUNT key min max:统计score值在给定范围内的所有元素的个数

  • ZINCRBY key increment member:让sorted set中的指定元素自增,步长为指定的increment值

  • ZRANGE key min max:按照score排序后,获取指定排名范围内的元素

  • ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素

  • ZDIFF.ZINTER.ZUNION:求差集.交集.并集

Redis底层数据结构

动态字符串SDS

SDS之所以叫做动态字符串,是因为它具备动态扩容的能力

如果扩容后新字符串小于1M,则新空间为扩展后字符串长度的两倍+1;

如果扩容后新字符串串大于1M,则新空间为扩展后字符串长度+1M+1。称为内存预分配。

总结

IntSet

encoding包括三种模式如下图

intset中的元素都是整数且按照升序排列为了提高查询效率采用二分查找

intset每个元素使用相同编码格式(每个数据项大小相等)并且空间连续,支持编码格式的升级

编码升级过程如下所示

现在,数组中每个数字都在int16_t的范围内,因此采用的编码方式是INTSET_ENC_INT16,每部分占用的字节大小为:

encoding:4字节

length:4字节

contents:2字节 * 3 = 6字节

我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。以当前案例来说流程如下:

  • 升级编码为INTSET_ENC_INT32, 每个整数占4字节,并按照新的编码方式及元素个数扩容数组

  • 倒序依次将数组中的元素拷贝到扩容后的正确位置

  • 将待添加的元素放入数组末尾

  • 最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4

总结

  • Redis会确保Intset中的元素唯一、有序

  • 具备类型升级机制,可以节省内存空间

  • 按照升序排列元素,底层采用二分查找方式来查询

Dict

Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的。

Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)

如下图所示

Dict的扩容和收缩

Dict在每次新增键值对时都会检查负载因子(LoadFactor = used/size)

满足以下两种情况时会触发哈希表扩容:

哈希表的 LoadFactor >= 1,并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程;

哈希表的 LoadFactor > 5 ;

当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作

不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的size和sizemask变化,而key的查询与sizemask有关。因此必须对哈希表中的每一个key重新计算索引,插入新的哈希表,这个过程称为rehash。过程是这样的:

  • 计算新hash表的realeSize,值取决于当前要做的是扩容还是收缩:

  • 如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的2^n

  • 如果是收缩,则新size为第一个大于等于dict.ht[0].used的2^n (不得小于4)

  • 按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]

  • 设置dict.rehashidx = 0,标示开始rehash

  • 将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]

  • 将dict.ht[1]赋值给dict.ht[0](交换),给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存

  • 将rehashidx赋值为-1,代表rehash结束

  • 在rehash过程中,新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终为空

总结

Dict的结构:

  • 类似java的HashTable,底层是数组加链表来解决哈希冲突

  • Dict包含两个哈希表,ht[0]平常用,ht[1]用来rehash

Dict的伸缩:

  • 当LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容

  • 当LoadFactor小于0.1时,Dict收缩

  • 扩容大小为第一个大于等于used + 1的2^n

  • 收缩大小为第一个大于等于used 的2^n

  • Dict采用渐进式rehash,每次访问Dict时执行一次rehash

  • rehash时ht[0]只减不增,新增操作只在ht[1]执行,其它操作在两个哈希表

ZipList

ZipList 是一种特殊的“双端链表” ,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作, 并且该操作的时间复杂度为 O(1)。

每个entry记录前一个entry的长度,由此实现任意一端O(1)压入/弹出

连锁更新问题

ZipList的每个Entry都包含previous_entry_length来记录上一个节点的大小,长度是1个或5个字节:

如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值如果前一节点的长度大于等于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据

现在,假设我们有N个连续的、长度为250~253字节之间的entry,因此entry的previous_entry_length属性用1个字节即可表示,如图所示:

ZipList这种特殊情况下产生的连续多次空间扩展操作称之为连锁更新(Cascade Update)。新增、删除都可能导致连锁更新的发生。

总结

  • 压缩列表的可以看做一种连续内存空间的"双向链表"

  • 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低

  • 如果列表数据过多,导致链表过长,可能影响查询性能

  • 增或删较大数据时有可能发生连续更新问题

QuickList

由于ZipList需要申请连续空间,当数据过多或过长时可能影响查询性能

所以衍生出QuickList,使用链表连接管理多个ZipList解决以上问题如下图所示

为了避免QuickList中的每个ZipList中entry过多,Redis提供了一个配置项:list-max-ziplist-size来限制。

如果值为正,则代表ZipList的允许的entry个数的最大值

如果值为负,则代表ZipList的最大内存大小,分5种情况:

  • -1:每个ZipList的内存占用不能超过4kb

  • -2:每个ZipList的内存占用不能超过8kb

  • -3:每个ZipList的内存占用不能超过16kb

  • -4:每个ZipList的内存占用不能超过32kb

  • -5:每个ZipList的内存占用不能超过64kb

默认值为-2

以下是QuickList的和QuickListNode的结构源码:

我们接下来用一段流程图来描述当前的这个结构

总结

QuickList的特点:

  • 是一个节点为ZipList的双端链表

  • 节点采用ZipList,解决了传统链表的内存占用问题

  • 控制了ZipList大小,解决连续内存空间申请效率问题

  • 中间节点可以压缩,进一步节省了内存

SkipList

SkipList(跳表)首先是链表,但与传统链表相比有几点差异:元素按照升序排列存储节点可能包含多个指针,指针跨度不同。

总结

SkipList的特点:

  • 跳跃表是一个双向链表,每个节点都包含score和ele值

  • 节点按照score值排序,score值一样则按照ele字典排序

  • 每个节点都可以包含多层指针,层数是1到32之间的随机数

  • 不同层指针到下一个节点的跨度不同,层级越高,跨度越大

  • 增删改查效率与红黑树基本一致,实现却更简单

RedisObject

Redis五种基本数据结构

String

String是Redis中最常见的数据存储类型:

其基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb。

如果存储的SDS长度小于44字节,则会采用EMBSTR编码,此时object head与SDS是一段连续空间。申请内存时

只需要调用一次内存分配函数,效率更高。

(1)底层实现⽅式:动态字符串sds 或者 longString的内部存储结构⼀般是sds(Simple Dynamic String,可以动态扩展内存),但是如果⼀个String类型的value的值是数字,那么Redis内部会把它转成long类型来存储,从⽽减少内存的使用。

如果存储的字符串是整数值,并且大小在LONG_MAX范围内,则会采用INT编码:直接将数据保存在RedisObject的ptr指针位置(刚好8字节),不再需要SDS了。

List

Redis的List类型可以从首、尾操作列表中的元素:

哪一个数据结构能满足上述特征?

  • LinkedList :普通链表,可以从双端访问,内存占用较高,内存碎片较多

  • ZipList :压缩列表,可以从双端访问,内存占用低,存储上限低

  • QuickList:LinkedList + ZipList,可以从双端访问,内存占用较低,包含多个ZipList,存储上限高

Redis的List结构类似一个双端链表,可以从首、尾操作列表中的元素:

在3.2版本之前,Redis采用ZipList和LinkedList来实现List,当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码。

在3.2版本之后,Redis统一采用QuickList来实现List:

Set

Set是Redis中的单列集合,满足下列特点:

  • 不保证有序性

  • 保证元素唯一

  • 求交集、并集、差集

元素全是整数且元素数量不超过set-max-intset-entries时,Set会采用IntSet编码

否则使用set采用HT编码(Dict)。Dict中的key用来存储元素,value统一为null

ZSet

ZSet也就是SortedSet,其中每一个元素都需要指定一个score值和member值:

  • 可以根据score值排序后

  • member必须唯一

  • 可以根据member查询分数

当元素不多时使用ZipList结构

  • 元素数量小于zset_max_ziplist_entries,默认值128

  • 每个元素都小于zset_max_ziplist_value字节,默认值64

ziplist本身没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现:

  • ZipList是连续内存,因此score和element是紧挨在一起的两个entry, element在前,score在后

  • score越小越接近队首,score越大越接近队尾,按照score值升序排列

当元素数量或大小超过阈值时使用Dict + SkipList

  • SkipList:可以排序,并且可以同时存储score和ele值(member)

  • HT(Dict):可以键值存储,并且可以根据key找value

Hash

Hash结构与Redis中的Zset非常类似:

  • 都是键值存储

  • 都需求根据键获取值

  • 键必须唯一

底层实现方式:压缩列表ziplist 或者 字典dict当Hash中数据项比较少的情况下,Hash底层才⽤压缩列表ziplist进⾏存储数据,随着数据的增加,底层的ziplist就可能会转成dict,具体配置如下:

hash-max-ziplist-entries 512

hash-max-ziplist-value 64

Redis的hash之所以这样设计,是因为当ziplist变得很⼤的时候,它有如下几个缺点:

  • 每次插⼊或修改引发的realloc操作会有更⼤的概率造成内存拷贝,从而降低性能。

  • ⼀旦发生内存拷贝,内存拷贝的成本也相应增加,因为要拷贝更⼤的⼀块数据。

  • 当ziplist数据项过多的时候,在它上⾯查找指定的数据项就会性能变得很低,因为ziplist上的查找需要进行遍历。

持久化机制

RDB持久化

RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为RDB文件,默认是保存在当前运行目录。

执行时机

  • 执行save命令                     立即执行一次RDB

  • 执行bgsave命令                 开启一个独立的线程异步完成RDB

  • Redis停机时                        停机时会执行一次RDB

  • 触发RDB条件时

Redis内部有触发RDB的机制,可以在redis.conf文件中找到

# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000 # 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes# RDB文件名称
dbfilename dump.rdb  # 文件保存的路径目录
dir ./ 

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存;

  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作。

RDB的缺点?

  • RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险

  • fork子进程、压缩、写出RDB文件都比较耗时

AOF持久化

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。

AOF配置

AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

AOF的命令记录的频率也可以通过redis.conf文件来配:

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

AOF文件重写

Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:

# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb 

RDB和AOF的对比

集群

主从集群

单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。

主从集群同步原理

主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点,流程:

这里有一个问题,master如何得知salve是第一次来连接呢?

  • Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid

  • offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据。

master判断发现slave发送来的replid与自己的不一致,说明这是一个全新的slave,就知道要做全量同步了。

master会将自己的replid和offset都发送给这个slave,slave保存这些信息。以后slave的replid就与master一致了。

因此,master判断一个节点是否是第一次同步的依据,就是看replid是否一致。

完整流程描述:

  • slave节点请求增量同步

  • master节点判断replid,发现不一致,拒绝增量同步

  • master将完整内存数据生成RDB,发送RDB到slave

  • slave清空本地数据,加载master的RDB

  • master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave

  • slave执行接收到的命令,保持与master之间的同步

全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步。

通过偏移量确定主从节点之间的数据差异并且更新

repl_baklog文件是一个固定大小的数组只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。

repl_baklog中会记录Redis处理过的命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset:

随着不断有数据写入,master的offset逐渐变大,slave也不断的拷贝,追赶master的offset

如果master更新速度过快slave跟不上了并且master覆盖掉了slave没有同步的数据那么只能进行全量同步来解决问题了

哨兵集群

主从集群如何优化

可以从以下几个方面来优化Redis主从就集群:

  • 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。

  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO

  • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步

  • 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力

哨兵原理

哨兵的作用如下:

  • 监控:Sentinel 会不断检查您的master和slave是否按预期工作

  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主

  • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。

客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点

  • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举

  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高

  • 最后是判断slave节点的运行id大小,越小优先级越高。

哨兵集群脑裂问题

由于网络原因哨兵监测不到主节点,但客户端还在向主节点写入数据,哨兵会在从节点中选出一个主节点,这时就有了两个主节点,而原来的主节点会强制降级为从节点去同步新主节点的数据,导致客户端写入的数据丢失

解决方案

可以设置redis配置参数来保证向主节点写入数据时至少有一个从节点或配置主从数据的延迟不超过5秒中,如果达不到这两个要求就拒绝写入数据

分片集群

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题

  • 高并发写的问题

使用分片集群可以解决上述问题,如图:

分片集群特征:

  • 集群中有多个master,每个master保存不同数据

  • 每个master都可以有多个slave节点

  • master之间通过ping监测彼此健康状态

  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

散列插槽

数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值

内存淘汰

过期key处理

Redis之所以性能强,最主要的原因就是基于内存存储。然而单节点的Redis其内存大小不宜过大,会影响持久化或主从同步性能。我们可以通过修改配置文件来设置Redis的最大内存:

当内存使用达到上限时,就无法存储更多数据了。为了解决这个问题,Redis提供了一些策略实现内存回收:

内存过期策略

在学习Redis缓存的时候我们说过,可以通过expire命令给Redis的key设置TTL(存活时间):

可以发现,当key的TTL到期以后,再次访问name返回的是nil,说明这个key已经不存在了,对应的内存也得到释放。从而起到内存回收的目的。

Redis本身是一个典型的key-value内存存储数据库,因此所有的key、value都保存在之前学习过的Dict结构中。不过在其database结构体中,有两个Dict:一个用来记录key-value;另一个用来记录key-TTL。

这里有两个问题需要我们思考:Redis是如何知道一个key是否过期呢?

利用两个Dict分别记录key-value对及key-ttl对

是不是TTL到期就立即删除了呢?

惰性删除:顾明思议并不是在TTL到期后就立刻删除,而是在访问一个key的时候,检查该key的存活时间,如果已经过期才执行删除。

周期删除:顾明思议是通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。执行周期有两种:Redis服务初始化函数initServer()中设置定时任务,按照server.hz的频率来执行过期key清理,模式为SLOWRedis的每个事件循环前会调用beforeSleep()函数,执行过期key清理,模式为FAST

SLOW模式规则:

  • 执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。

  • 执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms

  • 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期

  • 如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束

FAST模式规则(过期key比例小于10%不执行 ):

  • 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms

  • 执行清理耗时不超过1ms

  • 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束

小总结:

RedisKey的TTL记录方式:

在RedisDB中通过一个Dict记录每个Key的TTL时间

过期key的删除策略:

惰性清理:每次查找key时判断是否过期,如果过期则删除

定期清理:定期抽样部分key,判断是否过期,如果过期则删除。定期清理的两种模式:

SLOW模式执行频率默认为10,每次不超过25ms

FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

内存淘汰策略

内存淘汰:就是当Redis内存使用达到设置的上限时,主动挑选部分key删除以释放更多内存的流程。Redis会在处理客户端命令的方法processCommand()中尝试做内存淘汰:

Redis支持8种不同策略来选择要删除的key:

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。

  • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰

  • allkeys-random:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选

  • volatile-random:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。

  • allkeys-lru: 对全体key,基于LRU算法进行淘汰

  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰

  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰

  • volatile-lfu: 对设置了TTL的key,基于LFI算法进行淘汰

比较容易混淆的有两个:

  • LRU(Least Recently Used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

  • LFU(Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

网络模型

IO读取数据的流程

当用户应用想要读取磁盘时是没有权限的,只能调用内核对外提供的指令或者函数,如果数据没有准备好,就必须等待数据就绪

阻塞IO

当用户应用调用recvfrom请求IO时,如果内核没有准备好数据用户应用会一直等待

非阻塞IO

当用户应用调用recvfrom请求IO时,如果内核没有准备好数据用户应用多次反复请求IO

IO多路复用

无论阻塞IO还是非阻塞IO,用户应用都需要调用recvfrom来获取数据差别在于

如果调用recvfrom时,恰好没有数据,阻塞IO会一直等待,非阻塞IO会反复调用recvfrom使CPU空转,它们都没有充分的发挥CPU的作用

文件描述符(FD)用来关联Linux中的文件

IO多路复用:是利用单个线程来同时监听多个FD,并且在FD可读或可写时得到通知,从而避免无效等待,充分利用CPU资源

IO多路复用由三种实现方式:Select、poll、epoll

select和poll只能告诉用户应用我的内核有数据已经就绪了(不知道是哪个FD)需要用户应用自己去遍历,找到已经就绪的数据

epoll能告诉具体是哪个FD就绪了,用户进程直接去获取就行了不用去遍历

select

  • Select内部用数组来存储分别存储读、写、异常事件的FD,并且可以设置超时时间

  • 当FD就绪时会通知用户进程有几个FD就绪了并不能知道哪个数据就绪了所以需要遍历

  • 最多只能监听1024个FD

poll

  • 相比于select使用多个数组去保存不同的事件进行了改进,直接使用一个数组来存储FD事件类型和实际发生的事件类型

  • 相比于select最多监听1024个FD,poll理论上监听的FD数量可以无上限

epoll

  • 用红黑树和链表分别存储要监听的FD和就绪的FD,初始监听的数据都会放在红黑树中,当数据就绪后会从红黑树拷贝一份到链表中,用户进程直接拷贝链表上就绪的FD即可

总结

Redis网络模型

使用IO多路复用结合事件处理器来应对多个请求

连接应答处理器

命令回复处理器:6.0版本之后引入了多线程处理回复请求

命令请求处理器:6.0版本之后请求数据并把数据转化为redis命令的操作引入了多线程

最佳实践

BigKey问题

key的设计

Redis的Key虽然可以自定义,但最好遵循下面的几个最佳实践约定:

  • 遵循基本格式:[业务名称]:[数据名]:[id]

  • 长度不超过44字节

  • 不包含特殊字符

key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小。当字节数大于44字节时,会转为raw模式存储,在raw模式下,内存空间不是连续的,而是采用一个指针指向了另外一段内存空间,在这段空间里存储SDS内容,这样空间不连续,访问的时候性能也就会收到影响,还有可能产生内存碎片

避免BigKey

BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:

  • Key本身的数据量过大:一个String类型的Key,它的值为5 MB

  • Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个

  • Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB

推荐值:

  • 单个key的value小于10KB

  • 对于集合类型的key,建议元素数量小于1000

BigKey的危害

  • 网络阻塞

  • 对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢

  • 数据倾斜

  • BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡

  • Redis阻塞

  • 对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞

  • CPU压力

  • 对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用

发现BigKey

①redis-cli --bigkeys

利用redis-cli提供的--bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key

②scan扫描

自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)

③第三方工具

④网络监控

删除BigKey

  • redis 3.0 及以下版本

  • 如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey

  • Redis 4.0以后

  • Redis在4.0后提供了异步删除的命令:unlink

批处理优化

单个命令的执行流程

N条命令的执行流程

redis处理指令是很快的,主要花费的时候在于网络传输。于是乎很容易想到将多条指令批量的传输给redis

Redis提供了很多Mxxx这样的命令,可以实现批量插入数据,例如:

  • mset

  • hmset

应用场景

Redis实现分布式锁

Redis实现分布式锁主要利用setnx命令和lua脚本,用setnx命令实现阻塞效果再设置过期时间(防止死锁)

释放锁时不能单纯的删除key,而是要先判断是不是当前线程获得了锁,并且用lua脚本保证判断和删除的操作是原子性的

Redission是怎么实现分布式锁的

看门狗机制:获取锁成功后会新开一个线程进行监控(看门狗)给获取锁的线程续期(默认每过10s重置一次过期时间),防止线程还没有执行完任务锁就过期了,释放锁时会通知看门狗

重试机制:获取锁失败的线程会重试几次,可以设置一个阈值来限制重试的次数

可重入:利用redis的Hash结构存储获取锁的线程(key)和获取锁的次数(value),当获取失败时会判断锁是不是当前线程获取的,如果是则重入次数-1

redission可以使用红锁来解决主从一致性问题,但是不推荐效率很低,如果追求数据一致性用ZK

数据一致性问题

数据库和redis是一个分布式系统,分布式系统之间数据修改时会产生数据一致性问题

各种解决方案的优劣势

  1. 首先不能去更新缓存,因为更新缓存会有很多冗余操作,还不如让redis数据进行懒加载

  1. 先删除缓存再更新数据,删除缓存后,正在进行更新数据库时可能会有线程进行查询操作,并且把脏数据写入缓存造成数据在缓存过期之前都是脏数据,而且发生概率不小因为更新耗时比删除长

  1. 先更新数据库再删除缓存,更新数据库之后缓存不会立刻被删除会出现短期的数据不一致问题,但是时间很短而且遵循了最终一致,相对来说是个不错的方案

  1. 延迟双删、加锁这些策略违背了redis的初衷,使用redis是为了提高效率如果用这些策略还不如不用redis

延迟双删是更新数据库前后都要删除缓存,但更新数据库后要等一小会再删是为了保证主从一致性

缓存穿透

客户端请求的数据在缓存和数据库中都不存在,这样缓存就不会生效,所有请求打到数据库上造成压力

各种解决方案的优劣势

  1. 缓存空对象,将空对象也加到redis缓存中实现简单,但是要浪费一部分内存来缓存空对象

  1. 布隆过滤器,使用bitmap+Hash算法对空对象进行拦截,实现复杂,占用内存变少,但出现哈希冲突时会误判

增加id复杂度、做好数据格式校验、做好用户权限校验是为了避免缓存穿透而不是解决方案

缓存击穿

也叫热点key问题,当有一个被高并发访问的key突然过期,很多请求打到数据库上,瞬间给数据库造成巨大压力

各种解决方案的优劣势

  1. 互斥锁,发现缓存中没有数据时尝试去获取锁获取成功后读数据库并把数据写到缓存之后,否则进行休眠,休眠好再次多次尝试读缓存,互斥锁可以用reentrantlock

  1. 逻辑过期,设置逻辑过期时间,如果查询缓存时发现逻辑过期了尝试获取互斥锁,获取锁成功后让异步线程(建议使用线程池)进行缓存重构然后读取逻辑过期的数据,获取锁失败的线程也读取逻辑过期数据

缓存雪崩

同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力

解决方案

  • 给不同的Key的TTL添加随机值

  • 利用Redis集群提高服务的可用性

  • 给业务添加多级缓存

  • 给缓存业务添加降级限流策略

Redis八股文笔记相关推荐

  1. Redis学习笔记 - 数据类型与API(1)Key

    Redis学习笔记 - 数据类型与API(1)Key Key相关命令 1. 常用命令 命令 含义 时间复杂度 keys 查找所有符合给定模式 pattern 的 key O(N), N 为数据库中 k ...

  2. Redis学习笔记~Redis在windows环境下的安装

    Redis是一个key-value的存储系统,它最大的特点就是可以将数据序列化到文件中. redis存储在服务器的内存或者文件中,它不是session,不是cookies,它只是个更安全,更稳定,更可 ...

  3. redis学习笔记-持久化

    redis学习笔记-持久化 前言 redis持久化有两种方式:RDB和AOF.分别对应着全量复制和增量复制.深刻理解各自的实现方式及适用场景对redis的使用和运维十分重要.下面就分别介绍. RDB持 ...

  4. StackExchange.Redis学习笔记(五) 发布和订阅

    StackExchange.Redis学习笔记(五) 发布和订阅 原文:StackExchange.Redis学习笔记(五) 发布和订阅 Redis命令中的Pub/Sub Redis在 2.0之后的版 ...

  5. SpringBoot集成Redis用法笔记

    今天给大家整理一下SpringBoot集成Redis用法笔记,希望对大家能有所帮助! 一.Redis优点介绍 1.速度快 不需要等待磁盘的IO,在内存之间进行的数据存储和查询,速度非常快.当然,缓存的 ...

  6. Redis学习笔记~分布式的Pub/Sub模式

    redis的客户端有很多,这次用它的pub/sub发布与订阅我选择了StackExchange.Redis,发布与订阅大家应该很清楚了,首先一个订阅者,订阅一个服务,服务执行一些处理程序(可能是写个日 ...

  7. Redis学习笔记——SpringDataRedis的使用

    与Spring集成 我需要哪些jar包? <dependency><groupId>org.springframework.data</groupId><ar ...

  8. redis 了 什么地方用到_细节拉满!美团首推“百万级”Redis进阶笔记究竟有什么魅力...

    Redis 相信大家现在项目里面都会用到一个技术--Redis.毫不夸张的说Redis作为现在最受欢迎的NoSQL数据库之一,不管是项目还是面试都会有所涉及!我们都知道在项目中使用redis,无非是从 ...

  9. Redis学习笔记(五)——持久化及redis.conf配置文件叙述

    对于日常使用来说,学习完SpringBoot集成Redis就够我们工作中使用了,但是既然学习了,我们就学习一些Redis的配置及概念,使我们可以更深层次的理解Redis,以及增强我们的面试成功概率,接 ...

最新文章

  1. yolov3(二:车牌识别)
  2. python编程基础与应用-有哪些适合零编程基础的人学习Python的书?
  3. day18--django3之Ajax
  4. 32要烧写3个bin文件_入门教程3:如何给ESP8266烧录Gagent固件,快速接入机智云实现透传功能...
  5. springboot读取src下文件_springboot获取src/main/resource下的文件
  6. Flex 加载pdf
  7. 7 netsnmp安装window_Linuxfx 10.2,一款来自巴西的Window操作系统,“山寨”出了高度...
  8. 设置SQL Server 2008 以允许远程连接
  9. 利用U盘引导进入pe系统修复操作系统
  10. 小米笔记本Air 13.3 指纹版安装黑苹果 macOS High Sierra 10.13 教程
  11. Anlink(电脑操控手机软件) v2.2.5官方版下载,推荐这两款
  12. unity webgl踩坑指南
  13. 《白话大数据与机器学习》读书笔记1
  14. 零基础系统化学习白帽黑客技术
  15. spark中RSS工具简介
  16. echarts中月份数据缺少怎么补齐呢?
  17. 微信软文的作用说到底就是营销的一种手段
  18. 我们将与操作系统工作谈一场无私的爱──《云情人》思考
  19. 前端通过后端返回的url下载图片方法
  20. 通过路由器子接口实现 VLAN 间的互访

热门文章

  1. CSS3文字描边效果
  2. wpa或者wpa2暴力破解WiFi
  3. 计算机应用基础》模拟考试卷一,计算机应用基础模拟试卷(含答案)
  4. python内核死亡的原因_python - Jupyter Notebook内核在编译神经网络时死亡 - SO中文参考 - www.soinside.com...
  5. 以视频搜视频时代来了!神目发布视频拷贝搜索引擎
  6. Backtrader 画图和指标
  7. 关于SSL原理的详解
  8. (Note)计算机中的Temp
  9. C6000嵌入汇编C与汇编对照及功能说明
  10. 从点点点到年薪30W的心理历程--测试君请进,绝对让你不虚此行!