基本数据结构
- Redis的每一条数据外面都是套一层 redisObject 壳子,里面的数据结构会改变,因为 redis很扣内存,数据少,内容少的时候要用省内存的紧凑结构,数据多,内容变长的时候要换成跑得快的复杂结构。
- String类型:热点数据,缓存对象,分布式锁,共享Session信息;三种存储模型,第一种纯数字 int,第二种: 短字符串 <= 44 字节,壳子和字符串存储在一块连续空间内,读取速度快。如果是长字符串,使用 SDS 这个 redis 的专属字符串,他自带两个标签:字符串总长度,剩余空间,拿长度一秒查到不用遍历,剩余空间如果追加文字不用因为内存不够频繁的再去申请空间。
- List类型:消息队列;底层使用 quicklist: 整体多条数据之间是双向链表,但是每个节点内部数据存储使用的是 ziplist 压缩列表
- hash类型:缓存对象购物车,redission中可重入锁实现; 底层元素少的时候使用 ziplist ,字段和值挨个紧凑排列,内容多使用 dict哈希表
- set类型: 交集,并集,差集场景,比如点赞,共同关注;底层数据少的时候,只存整数的连续数组,内存是一块连续的空间;数据多的时候使用哈希表,利用哈希表 key 的唯一性去重
- Zset类型:排序场景,比如排行榜;有序集合,带分数,去重自动排序,少量数据压缩列表,大量数据有两套数据配合:哈希表负责根据成员一秒查到分数,跳跃表负责排序分页,取名单的前 N 名,每一个成员身上带着一个 double 浮点数
- BitMap:适用于二值状态统计,比如说签到,判断用户的登录状态
- GEO:存地理位置经纬度,比如滴滴打车
- Stream:消息队列,能自动生成全局唯一ID
- ziplist 压缩列表:普通的链表节点要存指针,前后引用,不连续内存开销大;压缩列表申请一块连续的空间去掉多余的指针,每一条数据有头部(固定5字节,总字节长度,尾部偏移,方便快速定位头尾,不用遍历),每一条数据存储三样东西:前一个条目的长度(用于反向遍历),当前数据长度表示(区分是数字还是字符串),实际内容;最后末尾结束标志固定1个字节。一个巨大的缺点就是如果要插入删除就会引发一个连锁的更新,导致性能雪崩
- 跳表:普通的链表,查找时从前往后遍历,时间复杂度 O(n),跳表 = 多层有序链表。他的最底层是一条完整的有序链表,每一次新增数据就用抛硬币的方法,随机决定层高,如果是正面就多上升一层,如果是反面就停止,这样根据抛硬币 50%的概率,每一层个数 1/2 递减
第二层:1 ----------9
第一层:1 ----5 ----9 ----13
第0层: 1-3-5-7-9-11-13-15
Zset实现排行榜和String
- Zset是有序,唯一元素集合,并且带有score分数,zadd添加元素,zincrby可以给这个scope+1,Zscore返回分数,Zrank获得排名
- 底层是这个压缩列表和跳表实现的,当元素个数小于128个并且每个元素值小于4的时候使用压缩列表,否则使用跳表
- 跳表的查找就是在多个层级上跳来跳去,最后到这个指定元素,复杂度O(logN)。在mysql中使用B+树是为了降低树的高度,从而减少磁盘I/O的次数。但是redis是纯内存操作,指针跳转速度非常快,所以哪怕多跳转几次开销也很小,而且实现难度低
- String使用的是SDS结构,主要由len(字符串长度),alloc(分配空间长度),flags(展示不同类型的SDS),字符数组
Redis为什么快
- 大部分操作直接在内存中进行,也就是在 RAM中操作,CPU 直接读写,内容极快,而 mysql 是在和硬盘进行 I/O 交互,所以速度容易很慢
- 采用的是单线程模型,避免了多线程之间的竞争
- 阻塞I/O模式:你站 1 号桌等客人点菜,不点完不能去 2 号桌,其他桌全晾着;
- IO 多路复用(epoll):你雇一个服务员(操作系统内核 epoll),帮你盯着所有桌子。哪一桌客人开口说话(有数据发来、连接可读写),服务员再单独通知你,你只处理有消息的桌子,空闲的桌子完全不用管。
- Redis 启动主线程,调用 epoll,把所有客户端 TCP 连接交给操作系统内核监听;当任意客户端发来命令(网络缓冲区有数据),内核 epoll 唤醒 Redis 主线程;主线程一次性取出所有有事件的连接,挨个读取客户端发来的指令;单线程可以同时维护几万、几十万客户端连接,不会因为某一个空闲连接卡住;全程单线程执行命令,无锁、无线程竞争,性能稳定。
Redis的两种持久化方式
- Redis的操作是不和磁盘进行交互的,比如set/get操作,但是这个AOF文件,会记录有哪些操作,然后刷入到磁盘里面,因为redis内存在重启之后清空,所以这个时候就需要重新读取AOF文件,把数据内容恢复到使用之前。
- AOF日志:每一次操作命令追加到一个文件。redis重启的时候会读取这个文件记录的命令,然后注意执行来进行数据的恢复,有三种回写磁盘策略:同步写回(可靠性高,最大程度上保证数据不丢失),每秒写回(性能适中,但是宕机可能丢失1秒的数据),由操作系统写回(宕机会丢掉很多数据)。优点:能很好的保证数据的安全性,而且支持多种同步策略。缺点就是AOF的文件通常比RDB文件更大,消耗更多的磁盘空间。而且 AOF 文件追加会导致文件不断增大,这个时候就会进行一个压缩重建的过程,比如一条数据 反复修改了100次,那么最后就保存这一条就够了。
- RDB快照:将某一时刻内存数据以二进制的方式写入磁盘,只保存某一时刻的数据状态文件体积比较小,备份和恢复速度很快。缺点是如果两次快照之间服务器崩掉,这段时间的数据将会丢失。但是恢复速度快,相比于AOF的文件小一些
过期删除和内存淘汰
- 内存淘汰是在内存满了的时候,redis触发淘汰策略删除一些数据;过期删除是对已经过期的数据进行删除
- 淘汰策略:1.不进行数据淘汰,内存满了如果有数据写入会报错禁止写入。 2.随机淘汰设置了过期时间的任意键值 3.优先淘汰更早过期的键值 4.淘汰所有设置了过期时间中最久没使用的 5.淘汰所有设置了过期时间中最少使用的键值 6.随机淘汰任意键值 6.随机任意淘汰 7.随机淘汰最久未使用的 8.淘汰整个键值中使用最少的键值
- Redis 的每个数据其实外面都是一个包装的 redisObject 壳子,这个壳子里面存储了一个 lru就是用户上一次被读取使用的时间戳,但是这个时间戳又不是时分秒那种精确的,因为会占用很大内存,所以 redis 启动的时候会有一个全局的简易时钟开始计时,比如从 1 开始记录,当这个key被使用的时候就把当前这个全局的时间记录进去,当做时间戳,然后进行淘汰策略的时候,会比较每个数据和当前系统的时钟时间差,差值越大说明闲置时间越长。但是还有一个问题,就是 redis 他是操作内存的,如果关机掉之后这些数据都会重置,所以我们需要这个 RDB快照,把这一刻数据保存,下一次恢复的时候就可以继续累计时间,而 AOF 只能进行一些命令的追加,不能持久化这个时间数据,所以需要最好持久化 AOF 和 RDB 一起
- 过期删除策略:惰性删除+定期删除
- 惰性删除:在访问或者修改key之前,检查是否过期,如果过期删除该key,如果没有过期不做任何处理
- 定期删除:每隔一段时间随机选出一定数量(默认20个)key进行检查,并且删除其中的过期key
LRU 和 LFU 算法
- LRU算法:淘汰最长时间没使用的,递增采用的是双向链表和哈希表,对于双向链表表尾是最久没有用到的,然后通过哈希表记录key到链表节点的映射,以O(1)的时间找到节点,哈希表主要是为了如果某个key最新更新了,就需要移动到表头,就需要这样进行查询。但是缺点比如某个低频的key突然被大量访问,但是之后长期不用,只因为最近使用过久会被LRU保存会挤占空间,所以适合那些最近访问了的还会再访问的,像什么新闻,临时会话
- LFU淘汰算法:淘汰频率最低的。核心是给每个key维护一个访问评率的计数器,但是直接计数有两个问题,计数器无限增长会消耗内存,第二个旧数据高频计数不会过期,比如说一个key半年前被高频访问,但是现在不用了,但是他的计数仍很高,就会被错误的保留。所以计数有一个对数衰减的策略,访问key的时候不是简单的+1,而是对数增长,避免溢出,然后长时间没访问的key计数会按照时间戳按照一定的比例下降,适用于一些爆款商品
大Key问题
- 大key问题就是key的value占用内存空间过大,导致redis性能下降,内存占用过高。可以对大Key进行拆分,对过期的数据定期清理。
布隆过滤器
- 主要是由初始值为0的位图数组和N个哈希函数两部分组成。数据会先使用N个哈希函数对数据做哈希计算,得到N个哈希值,然后对数组长度取模,得到在位图数组中对应的位置,然后设置成1
- 当数据来查找是否存在的时候也进行布隆过滤器计算,在对应的位置上只有有一个是0,就表示这个数据库一定不存在这个数据。
分布式锁
- 分布式系统中有CAP理论,分别对应一致性,可用性,分区容错性,用redis实现的是AP也就是高可用性和分区容错性
- 因为redis是单线程机制,使用SETNX获取锁成功返回1,失败返回0,这个命令是天然原子性的。万一某一个线程持有锁挂掉了,锁没有被释放,所以设置一个过期时间,通常结合expireTime设置。但是这样A线程时间到期了,B线程来持有锁,A线程在finally里面把锁给释放了,这就放错锁了,所以我们要设置vaule值,在释放锁的时候要比较一下这个锁的value是否和自己一直,但是这个比较的操作是非原子性的,所以我们采取lua脚本保证这个校验是原子性的。但是这个锁存在一些缺点:第一个锁没办法续期,如果执行方法时间很长但是过期时间很短就有可能锁提前释放。第二个:因为一些手动操作设置万一忘记设置过期时间,很容易产生死锁的。第三个SETNX不支持可重入。
- redisson实现分布式锁:
- 第一:设置唯一标识field是UUID+线程ID(集群环境下线程ID可能重复所以拼接UUID)以及设置过期时间 。
- 第二:看门狗机制:加锁成功之后会设置一个定时任务向redis发送一个lua脚本,这个定时任务的时间是过期时间除以3,比如30s过期时间就在10s的时候触发定时任务,然后这个脚本会检查这个锁是不是归属当前的客户端,是的话就重置过期时间为30秒,设置为1/3是为了有一定的容错空间。然后他的生命周期是锁绑定的,如果解锁或者客户端崩溃就会自动取消掉这个定时任务,避免无限续期。
- 第三:为了避免这个等待锁的不停自旋轮询对redis造成很大压力,当线程加锁失败就不去再轮讯了,订阅一个锁释放的通道,当锁释放的时候就发布通知对等待线程进行唤醒。
- 第四:可重入锁的实现,有些递归嵌套方法调用,那么同一个锁就有可能被一个线程多次获得,如果锁不能重入那就自己把自己阻塞形成死锁。核心的数据结构使用了redis的hash结构,key就是锁的名字,然后哈希值的field就是UUID+线程ID,value就是重入的次数,(key就是 redis这个存数据要有一个key对应数据,然后我这个数据是 hash 结构,所以 field 是hash结构的 key,而重入次数是 value),为什么不使用字符串存储呢,因为字符串要通过get去解析获得重入数,然后校验再set就不是原子性的就要再去使用lua脚本,但是hash的单个命令inrement增加就是原子性的,操作简单安全
- 第五:锁丢失如果redis是一主多从的话,当setNX还没写到主节点的时候主节点就挂掉了,那就会选择一个从节点转从为主,但是新节点是没有加锁,但是服务器以为加锁成功,这个新节点没有锁数据那么其他线程还是能加锁,就会导致A,B都持有同一把锁。redisson有RedLock红锁,部署多个独立的redis节点,完全独立,互不主从,要给半数以上的节点加锁成功来保证锁的有效性,否则就是加锁失败,去所有节点释放锁。
缓存雪崩、击穿、穿透是什么?怎么解决?
- 缓存雪崩:大量缓存 key 同一时间集体过期失效,所有请求不再走 Redis,全部打到 MySQL 数据库,瞬间压垮 DB,数据库卡死宕机。
- 均匀设置过期时间(根源预防):基础过期时长 + 随机偏移值(例:2 小时基础有效期,追加 1~30 分钟随机数)
- 互斥锁:缓存未命中 → 获取分布式锁 → 仅一个线程查询 DB 并回填缓存 → 释放锁;其余线程等待或返回默认值。关键注意点:锁必须设置超时时间,防止线程异常阻塞导致死锁、服务卡死。
- 后台定时更新缓存(永不过期方案):缓存不设置expire过期时间,独立定时任务定期刷新数据。
- 缓存击穿:单个热点 key 过期,高并发瞬间全部打到数据库
- 分布式互斥锁和雪崩的互斥锁逻辑一致,同一时间只放行一条请求更新缓存,其余请求阻塞等待,控制并发数据库请求量。
- 完全不设置过期时间,后台异步定时刷新;
- 主动续期:临近过期前,后台提前更新数据并重设过期时间。
- 缓存穿透:查询数据库根本不存在的数据,缓存永远不会命中,请求每次都直达数据库。
- 当大量恶意请求访问的时候也会产生缓存穿透,所以在 API 的入口处要进行非法数值和字段的检测,如果是恶意请求就直接返回错误避免进一步打到数据库
- 设置空值或者默认值,在 mysql 中查询确定该值不存在,就设置空值或默认值在 redis 进行返回
- 使用布隆过滤器:这个不能判断一定存在,但是一定能判断不存在
posted @
2025-12-20 15:40
Huangyien
阅读(
31)
评论()
收藏
举报