Redis的学习

Redis

什么是redis?

redis是一款非关联性数据库,他的数据可以直接存储在内存里,主要用来数据的缓存

redis的数据结构

SDS

动态字符串(Simple Dynamic String------------>简单动态字符串),简称SDS

这是SDS的内部结构

len:他记录的是字符串的的长度,这样获得该数组的时间复杂就是O(1)

alloc:分配给的字符串的空间长度,当我们要修改字符串长度时,通过alloc-len来判断当前空间是否足够,如果不

够的话,就进行扩容。

SDS具备动态扩容的能力,当我们要申请扩容的时候,

如果新的字符串没超过1M,那么新的空间大小为扩展后的字符串的2倍加1(加1是因为还有结束符号)

如果新的字符串超过1M, 那么新空间的大小为扩展后字符串的大小加1M+1

上述这种方式叫做内存预分配。

优点:

1.获取字符串长度的时间复杂度为O(1)

2.支持动态扩容,存在内存预分配

3.减少内存分配次数,较低开销

4.他是二进制安全的

原因:因为 SDS 不需要用 "\0" 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可

存储包含 "\0" 的数据。

IntSet(整数集合)

intset他是redis中set集合的一种实现方式,基于整数数组来实现

为了方便查找,redis会将整数数组中的元素按照升序以此保存到contents[]数组中:

intset升级

假设有一个intset,采用的编码方式是int16,那么当我们向其中添加一个超过这个编码方式的数字,此时intset就会自动的将编码方式升级到合适的大小

首先,升级编码方式为32,并按照新的编码方式及元素个数扩容数组

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

第三,将甙添加的元素放入到数组末尾

最后,将intset的编码方式改为32,length的长度也进行改变

Dict(哈希表)

结构如下:

当我们要向哈希表中加入元素,那么此时先根据key计算出hash值(h),之后让h&sizemask(&------>与运算,和取余效果一样)来计算元素应该存储到那个位置,在这之后,还有一个新的问题,那就是如果不同的key计算出的hash值和sizemask进行&运算之后,得到的值想同,那么怎么添加新的元素,此时就会让哈希表的索引的位置指向新的元素,新的元素中的next指向旧的队首元素,这样是因为从队头插入更块,从队尾插入的话还需要遍历

Dict中的rehash(渐进式哈希)

Dict的扩容

Dict 中的 HashTable 就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低。

Dict 在每次新增键值对时都会检查负载因子(LoadFactor = used/size),满足以下两种情况时会触发哈希表扩容:

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

◆ 哈希表的 LoadFactor > 5;


Dict的收缩

Dict除了扩容以外,每次删除元素时,也会对负载因子做检查,当LoadFactor < 0.1时,会做哈希表收缩:


Dict的rehash

不管是扩容合适收缩,都一定会创建新的hash表,导致hash表的size和sizemask发生变化,而key的查询和sizemask有关,所以此时要重新计算每一个key的索引,插入到新的hash表中,这个过程就叫做rehash。

但是呢,dict中的rehash并不是一次性完成的,试想一下,如果hash表中存储着数百万的数据,要在一次rehash中完成,那么就极有可能导致线程阻塞,因此dict的rehash是分步的,渐进的,因此被称为渐进式rehash

流程如下:

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

◆ 如果是扩容,则新 size 为第一个大于等于dict.ht[0].used + 1``2ⁿ

◆ 如果是收缩,则新 size 为第一个大于等于dict.ht[0].used2ⁿ(不得小于 4)

② 按照新的 size 申请内存空间,创建 dictht,并赋值给dict.ht[1](新的hash表)

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

④ 将dict.ht[0](旧的hash表)中的每一个 dictEntry 都 rehash 到dict.ht[1]

④ 每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于 - 1,如果是则将dict.ht[0].table[rehashidx]的 entry 链表 rehash 到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都 rehash 到dict.ht[1]

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

redis的作用?

缓存

排行榜 zset可以进行排序

去重 set集合不允许数据重复

分布式锁 微服务中要用到分布式锁

消息队列 list

计数器

为什么要用redis?

redis主要用来存储更新比较少(例如 新闻类型 商品类型)或者短时间内更新比较多(秒杀 抢购)的数据

MySQL中的数据存储在硬盘上,redis的数据存储在内存中,可以减轻数据库的压力

redis线程模型

redis6.0之前,redis是完全的单线程,处理客户端连接和执行命令都是由一个线程完成的

redis6.0之后,引入了多线程,处理客户端连接交给一部分线程完成

执行命令依然是由单线程执行,这样保证并发安全(不管访问量多大,都是一个一个的执行)

为什么redis单线程执行命令速度很快?

redis操作都是在内存中

底层基于哈希结构(可以粗浅的把内存看成哈希结构),可以通过key计算出哈希值,快速定位到位置

单线程模式,避免了执行命令时的线程切换

redis持久化

因为redis数据都是存储在内存中,一断电就没有了,所以需要redis提供数据持久化

redis数据持久化有两种方式:

RDB(Redis DateBase):这是redis默认的持久化方式,它使用快照方式的方式来使数据持久化(快照:指的 是 在某个时间点将当前数据的全部信息记录下来),将redis中的数据都写入一个dump.rdb文件中。当满足条件时,自动执行持久化。也可以用save手动持久化。

**AOF:**需要在配置里进行修改,默认不开启。

以日志的方式,将redis中的写操作命令记录下来(写入,删除,更新,查询不改变数据,所以不记录),当下次还原时,将命令行按顺序执行,只允许追加文件,不允许改变文件,以此来还原数据。

redis事务

redis中的事务其实就是一组被序列化指令的集合,所有的命令在事务中,并没有被执行。multi:开启事务;

exec:执行事务。mysql中的原子性是保证几条命令按顺序执行,中间不会插入其他命令,如果有三条

命令,如果其中一条失败,其他两条也不会被成功执行。redis中的原子性,只保证几条命令按顺序执行,

中间不会插入其他命令,但并不保证执行成功的原子性。

key的过期策略

其实指的是当key的时间到期后,redis以何种方式删除过期key

**惰性删除:**是当一个key过期后,不会立马删除,而是等到下一次使用时,被检查到过期,才会被删除

缺点:一直占据内存,还要设置一个字典记录key的状态。

**定期删除:**在key到期后,将key的状态改为不可用,定时定期的主动删除过期的key。

key会绑定一个回调函数,过期后,会自动将状态改为不可用

redis同时使用这两种策略

redis中的数据淘汰策略

数据淘汰策略指得是当redis的内存不够用的时候,在向redis中添加一些数据时,redis就会按照某种策略删除一部分数据。

在redis里有八种数据淘汰策略:

noeviction:

不淘汰任何key,但是满存满后不允许写入数据,他是redis中默认的数据淘汰策略

allkeys-lru:

所有的key,基于LRU算法进行淘汰

volatile-lru:

对设置了**TTL(Time to live 存活时间)**的key,基于LRU算法进行淘汰

allkeys-lfu:

对所有的key,基于LFU算法进行淘汰

volatile-lfu:

对设置了**TTL(Time to live 存活时间)**的key,基于LFU算法进行淘汰

LRU(Least Recently Used):

最近最少使用, 就是用当前的时间减去最后一次访问的时间,间隔时间越大,淘汰优先级越高

LFU(Least Frequently Used):

最少频率使用,记录每个key的访问频率,访问频率越少淘汰的优先级越高

Redis实现分布式锁

什么是分布式锁?为什么需要它?

1. 核心定义

分布式锁是用于解决分布式系统中,多个节点(进程 / 服务)竞争同一共享资源时的并发控制工具,它能保证同一时间只有一个节点可以操作该共享资源,从而避免数据不一致、重复执行等问题。

2. 为什么需要它?(对比单机锁)

在单机应用中,你可以用 synchronized(Java)、threading.Lock(Python)这类单机锁解决多线程并发问题,因为所有线程都在同一个 JVM / 进程内,共享内存,锁的状态可以直接维护在内存中。

但在分布式系统中(比如微服务集群、多台服务器部署同一个应用),不同节点的进程不在同一个内存空间,单机锁完全失效

redis是如何实现分布式锁的?

使用redis中的sentx命令和 Lua脚本(保证操作执行的原子性)

那如何有效的控制redis锁的过期时间?

利用redission中实现的看门狗机制

看门狗机制 指的就是当一个线程加锁成功之后,会有一个其他的线程来监视这个线程,给这个线程增加锁的持有时间,就叫做续期,每隔锁的过期时间的三分之一就会续期,锁的过期时间默认是30s,也就是默认10s续一次期,每次都会重置锁的过期时间。

当其他的线程来获取锁的时候,如果锁已经释放了,那么就获得锁,如果锁还在使用,那么会进行一个循环来获取锁,设置一个阈值,超过阈值就结束。

redission的加锁,设置过期时间操作是基于lua脚本实现的

redission的这个锁,可以重入吗? 这个锁是可以重入的,在redis中会有一个hash结构来记录锁的信息,key是锁,value来存储线程的信息和重入的次数

reidssion中的锁可以解决主从一致性问题吗?

不能解决,但是可以使用redission中的红锁,但是性能太低了。

Redis集群

主从复制(解决高并发问题读的问题)

介绍一下主从同步

主从同步是指一个redis的并发能力是有限的,所以需要多个redis搭建主从集群,以此来提高redis的并发能力,实现读写分离,一个主节点负责写数据,多个从节点负责读数据。

说一下主从同步数据的流程

主从同步数据的流程分为两个阶段,

首先是全量同步

1.从节点向主节点发送同步数据请求,携带自己的replication id(版本号)和offset(偏移量)

2.主节点根据replication判断是否是第一次同步,如果是第一次同步,那么就与从节点同步版本信息,也就是

replication id(版本号)和offset(偏移量)

3.主节点执行bgsave,生成RDB文件,发送给从节点执行

4.在rdb文件生成中,记录期间的所有命令,放到一个日志文件中

5.把之后的日志文件发送给从节点执行

接下来是增量同步

1.从节点向主节点发送同步请求,主节点判断是不是第一次同步,不是的话就获取从节点的offset值

2.将从节点offset值之后的数据发送给从节点执行

哨兵模式(解决高可用问题)

哨兵的作用:

redis中的哨兵是为了实现主从机制出现故障后自动恢复

哨兵结构模式如下:

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

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

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

服务状态监控

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

主观下线:某个sentinel发现某个实例未在规定时间内响应,则认为该实例主管下线

客观下线:若有超过指定数量的的sentinel都认为某个实例下线,那么这个实例就是客观下线了,通常这个数量

在sentinel数量的一半

哨兵选主的规则

1.首先判断从节点与主节点的断开时间是否超过了阈值,如果超过了,那么就直接排除该从节点

2.判断从节点的salave-priority(从节点的优先级)值。这个值越小优先级越高

3.如果slave-priority一样,判断从节点中的offset值,这个值越大优先级越高

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

脑裂问题:

第一步:

第二步:

第三步:

如何解决脑裂问题:

分片集群:

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

  • 海量数据存储问题

  • 高并发写的问题

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

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

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

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

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

缓存穿透,击穿,雪崩

缓存穿透

缓存穿透是指一直查询某个mysql中没有的值,比如查询id=-1的用户,这时redis中没有,就会去数据库中查询

解决方法:

1.当遇到redis中没有的数据,向mysql中查询也没有时,则向redis中加入key-value,value设置一个值,作为标记,当查询到value是这个值的时候,就表明数据库没有这个数据,不再查询了。

2.对参数合法性进行验证的,例如id没有等于-1的

3.设置布隆过滤器(有的可能找不到,没有的一定没有)

缓存击穿

击穿是指某个key在某个时间节点过期了。此时有大量并发请求过来,可能会压垮数据库。

解决方法: 1.key设置较长的过期时间

2.加锁。在mysql中查询时加锁,还可以在锁内二次查询是否已经查询到,如果查询到了就不再查询数据库

缓存雪崩

在高并发的状态下,大量的缓存失效,或者缓存层出现问题,所有请求到会跑到数据库,从而压垮数据库。

解决方法:

1.随机设置key的失效时间,避免大量key同时失效

2.集群部署,将热点key缓存在几个redis中,避免key全部失效

3.跑定时任务,在缓存刷新前加入新的缓存

相关推荐
小白自救计划2 小时前
【计算机视觉】学习历程
人工智能·学习·计算机视觉
冰糖拌面3 小时前
mysql 和 pg ip 白名单
数据库·tcp/ip·mysql·postgresql
怪侠_岭南一只猿3 小时前
爬虫阶段一实战练习题:爬取豆瓣电影 Top250 复盘
css·经验分享·爬虫·python·学习·正则表达式
数据知道3 小时前
MongoDB:如何将读请求分流到从节点,减轻主节点压力(读偏好)
数据库·mongodb
喵叔哟3 小时前
08-依赖注入与服务容器
数据库·oracle
’长谷深风‘3 小时前
从零开始学 SQLite:从基础命令到 C 语言编程实战
c语言·数据库·sqlite·软件编程
jackletter3 小时前
在pgsql中封装一个json函数,让它完全模拟mysql中的json_set
数据库·mysql·json·pgsql·json_set
冬夜戏雪3 小时前
【学习日记】
java·开发语言·数据库
LaughingZhu3 小时前
Product Hunt 每日热榜 | 2026-03-11
大数据·数据库·人工智能·经验分享·搜索引擎