Redis篇
数据类型及其业务场景
字符串(String)
字符串类型是最基本的数据类型,value 最多可以容纳的数据长度是 512M
。
- 存储任意类型的数据,包括数字、文本等。适用于缓存、计数器、分布式锁等场景。共享 Session 信息
哈希(Hash)
哈希类型是键值对的集合,适用于存储对象的多个属性。
Redis 为了解决哈希冲突,采用了链式寻址法,也就是采用链表的方式来保存同一个hash 桶中的多个元素。如果出现大量的 key 的冲突导致链表过长的情况下,为了保持高效,Redis 会对哈希表做 rehash 操作,也就通过增加哈希桶来减少冲突。
为了 rehash 更高效,Redis 还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表
- 常用于存储用户信息、商品信息等。减少内存消耗,最大程度利用缓存资源,可以结合VO类来说
列表(List)
列表类型是一个有序的 字符串列表,可以从两端添加或删除元素,支持快速的插入和删除操作。列表的最大长度为 2^32 - 1
,由双向链表 或压缩列表实现,
在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
javastruct quicklist{ quicklistNode *head; // 头部节点 quicklistNode *tail; // 尾部节点 unsigned long count; // 所有节点中元素的总数 unsigned long len; // 所有节点中元素的总数 int fill: 16; // ziplist节点的最大大小 unsigned int compress: 16; // 节点压缩深度,表示节点是否使用LZF算法压缩 }
- 适用于最新消息、队列等场景。
集合(Set)
集合类型是一个无序唯一 的字符串集合,支持添加、删除和查找元素。同时还支持多个集合取交集、并集、差集 。底层数据结构是由哈希表或整数集合实现的
- 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
- 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
-
适用于:
- 点赞功能:点了就加进来,取消就删除。Set 类型可以保证一个用户只能点一个赞
- 共同关注功能:Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号
有序集合(Sorted Set / Zset)
有序集合类型是一个有序的、不重复的字符串集合,每个元素都会关联一个分数用于排序。
Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score
(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。Zset 类型的底层数据结构是由压缩列表 和跳表实现的:
- 有序集合保存的元素数量小于128个 / 有序集合保存的所有元素的长度小于 64字节------>压缩列表
- 反之采用跳表
压缩列表
压缩列表本质上就是个数组,只不过增加了上面几个黄色的属性,有利于快速的寻找列表的首、尾的节点
跳表
跳表在链表的基础上增加了多级索引,通过多级索引位置的转跳实现了快速查找元素。说的那么厉害实际上就是隔位取节点通过 二分查找
的思想一次遍历过滤几个节点,很明显 ,这个只能基于 有序
的特性下,时间复杂度为 logN
为什么用跳表而不用红黑树或者二又树呢?
- 跳表实现比红黑树简单,易懂
- 范围查找效率更高
Bitmap
位图类型是一种紧凑、高效的数据结构,本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型
- 用于对大量的二进制数据进行存储和操作。适用于两种状态的统计业务 、布隆过滤器等场景。
随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增) 。
HyperLogLog
一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。
- 简单来说 HyperLogLog 提供不精确的去重计数。
适用于百万级网页 UV 计数,也就是看一个网站有多少人访问
GEO
Redis GEO 是 Redis 3.2 版本新增的数据类型,直接使用了 Sorted Set
集合类型
GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的"按权重进行有序范围查找"的特性,实现 LBS 服务中频繁使用的"搜索附近"的需求。
主要用于存储地理位置信息,并对存储的信息进行操作。
Stream
Redis 专门为消息队列设计的数据类型。
在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:
- 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
- List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。
基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。
缓存穿透
缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库
项目业务
TiketHub项目中的用户注册板块针对恶意重复用户名注册请求进行缓存穿透风险的预防处理。项目中的解决方案是通过布隆过滤器来对用户名数据进行拦截校验,当注册了一个用户后就会将该用户名通过多次Hash计算后记录在布隆过滤器中,注册用户时校验用户名在布隆过滤器中如果其对应的多个位中有一个不为1,则认为该用户名可用,进入下一逻辑,反之打回请求。由于请求无边界,布隆过滤器的BitMap位图长度范围有边界,所以就会有一个逻辑误判的风险,所以我们布隆过滤器放行后还会再次查一次数据库进行一个校验操作。此外,在项目中针对布隆过滤器不支持元素删除的缺点,对于用户注销后用户名再次可用的需求,项目通过在布隆过滤器下一层通过Redis的Set结构添加了一个用户名注销可用表,将注销后的用户名存储在此,如果布隆过滤器说用户名存在,再返回响应前再次校验一次注销可用,如果存在就放行注册,并删除注销可用表中的该数据。
有一个问题可能会出现:如果用户频繁申请账号再注销,可能导致用户注销可复用的 Username Redis Set 结构变得庞大,增加了存储和查询的负担。
为了防止这种情况,我采取了以下解决方案:
- 异常行为限制:每次用户注销时,记录用户的证件号,并限制证件号仅可用于注销五次。超过这个限制的次数,将禁止该证件号再次用于注册账号。
- 缓存分片处理:对 Username Redis Set 结构进行分片。即使我们对异常行为进行了限制,如果有大量用户注销账户,存储这些数据在一个 Redis Set 结构中可能成为一个灾难,可能出现 Redis 大 Key 问题。因此,我将 Set 结构进行分片,根据用户名的 HashCode 进行取模操作,将数据分散存储在 1024 个 Set 结构中,从而有效地解决了这个问题。
其他解决方案
- 不存在的 Key 进行缓存值设为 Null
对不存在的 Key 进行缓存,值设为 Null,并设置短暂过期时间,如 60 秒。
消耗内存,可能会发生不一致的问题(一开始没有,设置了空对象,后面突然有了,在空对象ttl到期前有不一致性)
- Redis Set 存储已注册用户名
使用确定的数据结构如 Redis 的 Set 集合来存储已注册用户名,判断时检查是否在集合内。
- 永久存储十几亿的用户信息到 Redis 缓存中显然不太现实,因为这会占用大量的内存资源。
- 即使是临时存储,如果在缓存中查询不到数据,仍然无法避免查询数据库的场景。
- 此外,对于这么多的用户信息,是否应该将其存储在一个 Key 中呢?显然是不可行的。即使进行分片,也会增加系统的复杂度。
结论:由于该方案占用内存较多且复杂度较高,因此不适合实际应用。
- 分布式锁
针对高并发注册场景,可以先查询缓存,如果不命中则使用分布式锁来保证只有一个线程访问数据库,避免重复查询。
- 相对于上述解决方案,该方案在一定程度上可以解决会员注册缓存穿透的问题。
- 但是,如果在用户注册高峰期,只有一个线程访问数据库,这可能会导致大量用户的注册请求缓慢或超时。
结论:这对用户的使用体验来说并不友好,因此我们不建议使用该方案。
缓存击穿
缓存击穿:给某一个热点key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,或者说redis宕机,这些并发的请求可能会瞬间把DB压垮。
项目业务
TiketHub项目中的车票购买板块就针对缓存击穿问题进行的处理,因为项目中的车票余额等数据存储在redis中,在节假日抢票期间属于一个热点key。然后针对抢票期间对于车票余额是属于一个追求强一致性的需求,所以我项目中采用的是一种同步上锁的操作,将该车票余额数据设置一个热度期之后的TTL,确保该热点key不会在关键时候因为TTL而过期失效;如果由于Redis宕机或者TTL过期等因素导致该车票余额数据的丢失,项目中就通过分布式锁同步的操作来确保一个强一致性的操作(这里不用担心MQ的消费堆积导致的双方不一致性,因为前面说了TTL属于热度期后,此外如果针对redis宕机,我们亦可以搭建集群架构确保高可用1),同时采用逻辑双判操作来避免无用上锁逻辑。
其它解决方案
- 互斥锁。
- 逻辑过期(PiPayShop项目的国家字段)
- 多级缓存:一级缓存使用本机内存,二级缓存使用Redis集群,三级缓存使用MySQL数据库。将热点数据放入应用程序的内存,可以避免缓存与后端存储(如Redis、数据库等)之间的请求和响应延迟,从而提高系统性能。热点数据通常是指在特定时间段内访问频率非常高的数据。
- 降级限流
缓存雪崩
缓存雪崩:同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
项目业务
TiketHub项目中的选票界面由于采用的是redis来保存车列数据和余票数据等大量数据,为了避免缓存雪崩问题,利用Redis集群提高服务的可用性。我们搭建的是一各哨兵集群的架构,通过哨兵机制的自动检测恢复机制,避免了Redis宕机带来的雪崩问题,此外,针对大量的缓存key同时失效问题,项目中从MySQL中获取到数据会基于一个随机值基于将多个key在一个区间散列分布,避免在一个时间点大量key的同时失效。
其他解决方案
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性 (哨兵模式、集群模式)
- 给缓存业务添加降级限流策略 (ngxin或spring cloud gateway)
- 给业务添加多级缓存 (uava或Caffeine)
数据一致性
如何保证缓存和MySQL间的数据一致性问题 ------> 强一致性 / 最终一致性
项目业务
TiketHub项目中,在购票逻辑中先将数据变更信息发布到MySQL中,通过Canal监听binlog将同步数据异步推送到消息队列中,最后再更新缓存。保证Redis与MySQL中的缓存数据保持一致
其他解决方案
- 方案一:缓存双删
最新技术架构流程如下所示:
如果消息队列更新缓存失败了呢?其实这一点还好,凭借消息队列客户端消费的重试规则,如果更新失败次数都达到客户端重试阈值还是不行,那一定是数据或者缓存中间件有问题。
当然,如果重试次数多了,也必然会面临缓存与数据库不一致的时间变长了,这个是需要清楚的。
通过该技术方案,可以很好达到缓存与数据库最终一致性。
- 方案二:先写数据库再删除缓存
读请求第一次查询时,会查询到一个错误的数据,因为写请求还没有更新到缓存,写请求写入 MySQL 成功后会删除缓存中的历史数据。后续读请求查询缓存没有值就会再请求数据库 MySQL 进行重新加载,并将正确的值放到缓存中。
也就是说这种模型会存在一个很小周期的缓存与数据库不一致的情况,不过对于绝大多数的情况来说,是可以容忍的。除去一些电商库存、列车余票等对数据比较敏感的情况,比较适合绝大多数业务场景。
当缓存过期(可能是缓存正常过期也 可能是 Redis 内存满了触发清理策略)条件满足,同时读请求的回写缓存 Redis 的执行周期在数据库删除之前,那么就有可能触发缓存数据库不一致问题。
上面说的两种情况,缺一不可,不过能同时满足这两种情况概率极低,低到可以忽略这种情况。
使用推荐
- 缓存双删:如果公司现有消息队列中间件,可以考虑使用该方案,反之则不需要考虑。
- 先写数据库再删缓存:这种方案从实时性以及技术实现复杂度来说都比较不错,推荐大家使用这种方案。
- Binlog 异步更新缓存:如果希望实现最终一致性以及数据多中心模式,该方案无疑是最合适的。
Redis持久化
Redis持久化主要分为两类:RDB 和 AOF
RDB
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
RDB的执行原理?
bgsave开始时会fork主进程得到子进程,将主进程中的页表复制拷贝到子进程,子进程通过页表共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。fork采用的是copy-on-write技术:
- 当主进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。主进程会将写操作记录下来,并将记录保存在内存中,子进程会通过读取主进程的写操作记录来重放写操作,并在自己的数据副本中执行相同的写操作。这样,子进程就能保持与主进程的数据一致性。
RDB因为是二进制文件,在保存的时候体积也是比较小的,在进行数据恢复的时候速度较快。
AOF
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
AOF的命令记录的频率也可以通过redis.conf文件来配:
重写机制
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
触发时机
- 比上次文件增长超过多少百分比则触发重写 (auto-aof-rewrite-percentage 100)
- AOF文件体积最小多大以上才触发重写 (auto-aof-rewrite-min-size 64mb )
实际项目中,我们一般采用RDB + AOF 混合使用的方式,主要是为了兼顾数据恢复效率和数据安全性。
AOF 重写的基本过程
-
Redis会创建一个子进程来执行AOF重写操作,这个子进程会遍历内存中的数据结构。
-
子进程通过遍历内存中的数据结构,将遇到的每个键的最新操作记录写入新的AOF日志文件。
-
子进程在遍历数据结构时会跳过一些特殊的操作,例如过期键的操作、部分不需要记录的命令等。
在进行AOF重写时,Redis会使用一个偏移量(offset)来记录上一次AOF文件的写入位置。这个偏移量表示上一次AOF文件记录的位置,Redis会将其保存在内存中。
在AOF重写过程中,Redis会遍历当前内存中的数据结构,并生成新的AOF文件。对于每个键的操作,Redis会判断其执行时间是否晚于上一次AOF的写入位置偏移量。以此来确保这次遍历到的这个键的更新操作是在上一次aof后执行的
-
在遍历数据结构的过程中,子进程会记录每个出现的键以及对应的操作。如果遍历过程中出现了相同的键多次,子进程会根据数据结构的特性和操作记录的先后顺序,只保留最新的操作。
-
子进程遍历完所有的键后,将得到一个新的AOF日志文件。最后,子进程将新的AOF日志文件替换原来的AOF文件。
数据删除策略
惰性删除
设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
- 优点 :对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
- 缺点 :对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放
定期删除
每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。
两种模式:
-
SLOW模式:执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
-
FAST模式:执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
-
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存
-
缺点:难以确定删除操作执行的时长和频率。
Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用可以兼顾实时性和效率。
数据淘汰策略
当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
三个维度考虑
- 是否有置顶需求(不设置TTL的数据不参与淘汰)
- 冷热分离(LRU:最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。)
- 数据访问频率差别(LFU:最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。)
默认策略:不淘汰任何key,但是内存满时不允许写入新数据,会抛出异常
使用建议
- 业务有明显的冷热数据区分,建议使用allkeys-lru。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。 (常用)
- 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。
- 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
- 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。
分布式锁
Redis 实现
Redis实现分布式锁主要利用Redis的 setnx
命令
实现原理
当执行 SETNX
命令时,它会尝试将指定的键 key
设置为给定的值 value
,但只有在键 key
不存在时,才会进行设置。如果键不存在,则设置成功,返回值为 1;如果键已经存在,则设置失败,返回值为 0。需要在业务上针对返回值手动进行分支判断实现上锁逻辑。
不足之处
- 只能在单个 Redis 节点上实现分布式锁,不适用于集群架构
- 锁的功能单一,需要手动维护上锁逻辑
- 有死锁风险,无法实现锁超时自动释放
- 不支持锁重入
Redison 实现
Redison 实现的分布式锁基于 Redis 的 Lua 脚本
和 Redis 的 setnx指令
来实现
Redisson 的分布式锁支持多种功能,例如可重入锁、公平锁、读写锁等。
主从一致性
主从一致性是指在 Redisson 中,分布式锁的主节点和从节点之间的数据一致性。当一个 Redisson 分布式锁实例在主节点上获取锁成功后,该锁状态会被同步到所有的从节点上,确保所有节点对这个锁的状态是一致的。这样,即使主节点发生故障或者网络分区等情况导致主节点不可用,其他从节点仍然可以继续提供对这个锁的服务
解决方法
RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n / 2 + 1),避免在一个redis实例上加锁。
如果业务中非要保证数据的强一致性,建议采用zookeeper实现的分布式锁
好处
强大的容错性和高可用性:Redison 通过 Redisson 的 Redis Sentinel 自动故障转移和 Redis Cluster 自动分片功能
看门狗机制:每隔(releaseTime / 3)的时间做一次锁续期,根据业务复杂度动态调整锁超时释放时间,如果状态异常,则视为故障发生,不再续期,超时释放锁,避免死锁
CAS优化:支持原地自旋等待获取锁,减少线程上下文交换提高CPU利用率
线程安全:加锁、设置过期时间等操作都是基于lua脚本完成,确保操作原子性
锁可重入:利用hash结构记录线程id和重入次数
Redis集群
- 主从复制
- 哨兵模式
- 分片集群
主从集群:高并发读
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
全量同步
全量同步执行时机
- 第一次进行数据同步时
- 当repl_baklog数据覆盖原先数据导致增量同步无法同步数据时
增量同步
哨兵模式:高可用
- 监控:Sentinel 会不断检查您的master和slave是否按预期工作
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
- 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
哨兵选主规则
- slave-priority值(越小优先级越高)
- offset值(越大优先级越高)
- 运行id大小(越小优先级越高)
集群脑裂
集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区(当主节点失去连接并且无法正常工作时) ,使得sentinel没有能够心跳感知到主节点 ,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失
解决办法
- min-replicas-to-write 1 表示最少的salve节点为1个
- min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒
我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
分片集群:高并发写
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
分片集群结构-数据读写
Redis 分片集群引入了哈希槽 的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
- 根据key的有效部分计算哈希值,对16384取余 (有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)
- 余数做为插槽,寻找插槽所在的实例
单线程Redis效率快
-
Redis是纯内存操作,执行速度非常快
-
- 访问速度:内存中的数据可以直接通过内存地址访问,而外存(例如磁盘)需要通过磁头、磁盘转速等机械部件进行寻址和读写,因此访问速度较慢。内存的访问速度通常是纳秒级别,而外存通常是毫秒级别或更长。
- I/O 开销:外存的访问需要进行I/O操作,包括磁盘寻址、数据传输等,这些操作都会引起较大的开销。相比之下,内存操作不需要进行I/O操作,减少了这部分开销。
- 随机访问:内存具有随机访问的特性,即可以直接访问任意地址的数据,而外存通常需要按照顺序进行读写,无法实现像内存那样的随机访问。在一些场景下,如缓存、索引等,需要频繁随机访问数据,这时内存的速度优势就更加明显。
-
-
采用单线程,避免不必要的上下文切换可竞争条件,无需上锁同步,多线程还要考虑线程安全问题
-
使用I/O多路复用模型,非阻塞IO,通过事件驱动机制来处理客户端的请求。当有新的请求到达时,Redis 会将其加入到事件队列中,在事件循环中逐个处理。这种事件驱动的方式避免了线程切换和上下文切换的开销,提高了系统的响应速度。
-
简化的数据结构:Redis 是键值存储系统,支持的数据结构相对简单,例如字符串、哈希、列表等。这些数据结构的实现都非常轻量,没有复杂的逻辑和耗时的操作,因此可以在单线程下高效地处理请求。
I/O模型
Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟 而不是执行速度, I/O多路复用模型主要就是实现了高效的网络请求
阻塞IO(NIO)
非阻塞IO(NIO)
(通过While循环持续获取对内核空间的等待数据,如果获取不到就直接返回0,然后持续进行获取数据-返回数据的状态,直到获取到等待数据才会进行复制数据返回的步骤,在等待数据阶段非阻塞,但是在复制数据的阶段还是阻塞的)
异步IO(AIO)
AIO本质上也是一种NIO,不过他比普通的NIO还吊,最常见的例子就是Netty中异步处理。AIO的本质就是开启一个新的线程去异步处理相关请求
异步守护进程进行,注意的是,由于守护进程会受主线程的影响,如果主线程终止了,不管守护线程是否完成了他的工作都会直接中断,为此,我们需要为主线程想办法让他阻塞一下或者把进行的程序拉长点,给守护进程的结果有呈现的机会
多路复用
IO多路复用:是利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
阻塞IO和多路复用的区别在于,阻塞IO每一次都需要进行一个等待链接,获取到就进行消费。也就是说他的读取消费是单次的,哪怕有多个事务要进行,他也需要重新读取,而多路复用可以一次性读取到多个事件来进行队列缓存,依次遍历进行消费,不需要重新进行读取
监听Socket的方式、通知的方式又有多种实现,常见的有
- select
- poll
- epoll
select和poll只会通知用户进程有Socket就绪,但不确定具体是哪个Socket ,需要用户进程逐个遍历Socket来确认
epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间
Redis网络模型
Redis网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求
- 连接应答处理器命令回复处理器,在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件
- 命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
大Key问题
所谓的大key问题是某个key的value比较大,所以本质上是大value问题。大Key问题的坏处最典型的就是阻塞线程,并发量下降,导致客户端超时,服务端业务成功率下降。
快速找出大Key
下面列出常用的两种,其实最方便的是通过第三方工具去监控
-
redis-cli --bigkeys 命令
- 该命令可以列出 Redis 中大小最大的 key。这个命令只在 Redis 4.0 或更高版本中可用。
-
Redis内置命令对目标Key进行分析
- STRING类型:执行STRLEN命令,返回对应Key的value的字节数。
- LIST类型:执行LLEN命令,返回对应Key的列表长度。
- HASH类型:执行HLEN命令,返回对应Key的成员数量。
- SET类型:执行SCARD命令,返回对应Key的成员数量。
- ZSET类型:执行ZCARD命令,返回对应Key的成员数量。
- STREAM类型:执行XLEN命令,返回对应Key的成员数量。
优化大Key
- 分割大 key:将大 key 拆分成多个小 key 来存储数据。例如,如果一个大型哈希表存储了大量的数据,可以将它拆分成多个小的哈希表,每个哈希表存储一部分数据。这样可以降低每个 key 的大小,并使 Redis 更加稳定和高效。
- 使用适当的数据结构:选择适当的 Redis 数据结构,以减少单个 key 的大小。例如,如果要存储大量元素,应该使用 Redis 集合或有序集合,而不是使用列表。
- 定期清理数据:定期清理 Redis 中的过期数据和不必要的数据,以避免大 key 的大小增长。可以使用 Redis 内置的过期机制或手动清理不必要的数据。
- 压缩数据:使用 Redis 的数据压缩功能,将大 key 中的数据进行压缩,可以减少每个 key 的大小,从而提高 Redis 的性能和可用性。
- 按需加载数据:不要在一次性将整个大 key 加载到内存中,而是按需加载数据,可以降低 Redis 的内存使用率,从而提高性能。