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].used的2ⁿ(不得小于 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.跑定时任务,在缓存刷新前加入新的缓存