Redis

积累知识,分享智慧,让成长之路不再孤单

数据类型

常见基本数据类型

● String(set、get)可用于保存用户登录信息
● Hash(hset、hget)可用于保存用户登录信息, 方便修改
● List(lpush、lpop)可实现消息队列
● Set(sadd)
● SortedSet(zadd)可用于排行榜
● Geo
● BitMap
● HyperLogLog(UV 统计)
● Stream

String

最基本的 k - v 结构,value 最多存储的数据长度是 512 M
底层的数据结构实现: SDS(简单动态字符串)
SDS:数据存放在 buf[] 字符数组中,len 存储字符串长度,空间不足会自动扩容。
并不是 C 语言中的字符数组,C 语言中字符数组依赖于 \0;
应用场景:
● 缓存对象
● 计数
● 分布式锁
● 共享 session 信息

List

列表,可以从头或尾进行添加或删除元素,列表的最大长度 2^32 - 1
底层的数据结构实现:双向链表或压缩列表;Redis 3.2 版本之后采用 quicklist
应用场景:
● 消息队列(Redis 提供了 BRPOP,阻塞式读取,节省 CPU 开销)

Hash

结构是:k - v,v 的结构是 field,v,适合存储对象
底层的数据结构实现:压缩列表或哈希表(元素少于512,压缩列表);Redis 7.0 采用 listpack 或哈希表
应用场景:
● 缓存对象
● 购物车(用户 id为 key,商品 id 为 field,商品数量为 value)

Set

最多可存储 2^32 - 1 个元素
底层的数据结构实现:哈希表或整数集合(元素都是整数且小于 512采用整数集合)
应用场景:
● 集合的交集、并集、差集
● 去重
● 点赞(SADD 文章id 用户id)

ZSet

有序集合,相比 set 多了一个排序的属性 score,
底层的数据结构实现:压缩列表或跳表(元素小于 128 个,每个元素值小于64 字节),Redis 7.0 采用 listpack或 跳表
相比 set,zset 不支持差集运算,支持并集和交集运算
应用场景:
● 排行榜
● 排序(电话、姓名)

BitMap

位图,二进制数组,存储 0,1;‘bit’ 数组
内部实现:采用 String 类型
应用场景:
● 签到
● 判断用户登录状态

HyperLogLog

基数统计(统计一个集合中不重复的元素个数,通过概率实现,标准误算率 0.81%),空间很小
应用场景:
● UV :独立访客
● PV:页面访问量

Geo

存储地理位置信息
内部实现:采用 Sorted Set

Stream

实现消息队列
包括消息持久化、消息确认、消费组等
缺点:
Redis 作为消息队列,可能会丢失数据,消息积压
消息队列的实现:
● List
● PubSub:无法持久化
● Stream
Feed 流:内容推送
● TimeLine(按发布时间排序)
○ 拉模式
○ 推模式
○ 推拉结合
● 智能排序(智能算法,用户是否感兴趣)

数据结构

Redis 快的原因之一是其高效的数据结构
键值对的存储:
key 是字符串对象,使用哈希表存储,数组中的元素叫哈希桶,哈希桶存放键值对的指针,分别执行 k 和 v

dict 存放两张表,正常情况使用哈希表1,rehash 时使用哈希表2

SDS

没有使用 C 语言中的 char *,封装的是 SDS
char * 缺点
● 获取长度 o(n)
● 不能存储 \0
● 可能导致缓冲区溢出

o(1) 读取长度,自动扩容,节省空间

双向链表

链表

压缩列表

优点:内存紧凑,占用连续的内存空间,节省空间
缺点:不能存储过多的元素,否则查询效率低,修改操作可能需要重新分配内存
类似数组,记录了节点数量,查询是顺序查找,每个节点的空间都不同,占用多少就用多少,有区间,在更新时就可能会导致连续的空间扩展,即连锁更新

哈希表

哈希表实际上就是数组,通过哈希计算得到数组下标,因此查询复杂度 o(1),但存在哈希冲突
Redis 采用链地址法解决哈希冲突,将哈希值相同的通过链表连起来

链表长度过长导致查询性能降低,通过 rehash 对哈希表进行扩容
哈希结构是定义了两个哈希表,在正常请求情况下都写入哈希表1,哈希表2没有分配空间;数据量增大的时候就会对哈希表2分配2倍空间,将哈希表1中数据迁移到哈希表2,完成之后再将哈希表2改为哈希表1,释放原来哈希表空间。存在问题:数据量过大迁移导致阻塞,采用渐进式 rehash
渐进式 rehash
为哈希表分配空间后,在每次哈希表进行修改操作,除了执行该操作外,还会将哈希表1的数据往哈希表2进行迁移,也就是把大量数据的迁移分摊到多次处理请求的过程中,避免一次 rehash 的耗时操作。
触发 rehash 条件,负载因子=哈希表节点数量/哈希表大小
采用渐进式 rehash 而非 hashmap 的方式:
redis 需要考虑并发访问的情况,hashmap 是单个 jvm 使用,并发容易控制;
redis 需要考虑持久化,且 redis 受限于内存和 CPU;hashmap 调整堆大小;
主要是从性能来考量,redis 作为缓存,尽可能减少任何的客户端请求的阻塞

整数集合

连续的内存空间,int 类型的数组,int8_t, int16_t 32 64 …
插入数据类型大时,进行扩容,不支持降级

跳表

查询复杂度 o(logN)
zset 结构是:跳表 + 哈希表,因此既能支持范围查询,也支持常数复杂度查询元素
跳表是在链表的基础上改进的,是一种多层的有序链表,查询时就会从多个层级上跳,最后找到元素
跳表相较于平衡树等:节点数更多,查询遍历更方便,更容易实现,操作简单

quicklist

是双向链表 + 压缩列表
压缩列表存在连锁更新,quicklist 控制了每个链表节点中压缩链表的个数,避免连锁更新或者更新范围更小。
没有根本解决连锁更新

listpack

在压缩列表上的改进,每个节点不记录上一个节点的长度了,只记录当前节点长度,若更新时空间小于该节点,则直接更新,不影响其他节点,否则创建新的 listpack。应用于 hash 和 zset

持久化方式

RDB

RDB 是一个快照文件,记录某一瞬间的内存数据,恢复数据时直接读入 RDB 到内存中即可。
快照是一个全量快照,因此写入磁盘很耗时,所以设置触发时间也就长,丢失数据可能更多
触发时机:
● 执行 save 命令(主进程执行,其他命令阻塞)
● 执行 bgsave 命令(开启子进程执行)
● Redis 停机时 (默认执行)
● 触发 RDB 条件 (配置文件中,例如 save 900 1 900秒内有一个key 被修改,则执行bgsave)
bgsave:
bgsave 开始时会 fork 主进程得到子进程,子进程共享主进程的内存数据。
完成 fork 后读取内存数据写入RDB,用新的 RDB 替换旧的 RDB文件, 采用 cope-on-write 技术
缺点:RDB 执行间隔时间长,可能有数据丢失,fork 子进程、压缩、写出 RDB 文件都比较耗时

AOF

AOF 是追加文件,redis 的每一条写操作会记录到 AOF 文件中。
先执行写操作命令,后写入 AOF 中,避免语句有语法错误,同时也不会阻塞当前命令(但可能数据丢失)
AOF 的刷盘策略:
● Always:同步刷盘(最大程度保证数据不丢失)
● EverySec:先将写命令写入缓冲区,再每隔一秒进行刷盘(性能适中)
● No:由操作系统决定(性能好)
AOF 重写机制:
在触发阈值时会进行重写,因为是记录命令,多个操作可能只有最后一次操作才有意义。
重写过程中是写到新的 AOF 文件,写完后再替换原来的。
写入 AOF 日志的操作是在主进程当中的,写入内容不多,影响不大,但在触发重写时很耗时,因此
重写 AOF 是由后台子进程 bgrewriteaof 来完成,(fork)使用子进程而非线程,是因为线程是共享内存的,修改共享内存需要加锁等操作,子进程的话就会创建一份副本,不会影响主进程,实现 写时复制 COW
触发阈值条件:
AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

RDB、AOF 混合持久化

比较:RDB 是二进制文件,文件比较小,恢复快,但可能丢失数据;AOF 恢复数据慢,丢数据风险小
开启:aof-use-rdb-preamble yes
● RDB 和 AOF 混合持久化
混合持久化是在 AOF 重写过程,在 AOF 重写的时候会把 RDB 文件中的内容写到 AOF 文件开头,快速加载数据,缺点是可读性差

Big key 对 RDB 和 AOF 影响,对 RDB 来说,Always 会导致写时间长,可能阻塞,另外两张不影响;对于 AOF 来说很快触发 AOF

缓存

缓存雪崩、穿透、击穿

● 缓存雪崩
○ 大量 key 失效导致请求直接访问数据库
■ 避免大量 key 同时失效,设置不同的 ttl
■ (互斥锁和设置永不过期)
○ Redis 宕机
■ 熔断(暂停服务,直接返回错误)或限流
■ 集群
● 缓存穿透(数据库和缓存中都不存在)
○ 布隆过滤器
○ 缓存空对象
○ 限制非法请求,做好请求数据格式校验
● 缓存击穿(热点数据过期了)
○ 互斥锁
○ 软过期(不设置过期时间,在逻辑上添加过期时间,若过期还是加互斥锁去更新,其他请求直接返回过期数据)
布隆过滤器
布隆过滤器是由位图数组组成,存储 0 和 1,通过多个哈希函数对数据进行计算,得到多个哈希值,并对位图数组长度取模得到对应下标,设置为 1,表示数据已写入,查询时判断对应下标是否为 1,只要有 0 就不存在;但是存在哈希冲突,因此查询到数据存在,不一定真的就存在,但查到不存在,就一定不存在,有一定的误判。

数据库和缓存的数据一致性

(双写方案)
● 先更新数据库,再更新缓存(问题:A 先修改数据为 1,B 后修改数据为 2,B 先更新缓存,A 后更新缓存)
● 先更新缓存,再更新数据库(问题:A 先更新缓存为 1,B 后更新缓存为 2,B 先修改数据,A 后修改数据)
无论先更新缓存还是先更新数据库,都存在数据不一致性
● 先更新数据库,再删除缓存(更新数据库时缓存失效,其他请求读到旧数据,仍可能导致数据不一致)
● 先删除缓存,再更新数据库(删除完成后可能读到旧数据,旧数据可能在新数据写入缓存后覆盖)
延迟双删
先删除缓存,再更新数据库,延迟一段时间再删除缓存。(兼顾请求能获取最新数据)
综上,要求不是很高的话,先更新数据库,再删除缓存比较好(否则没有缓存,多个请求阻塞更新数据,或加锁)
两个操作都能执行成功
如果两个操作出现一个失败,就可能导致数据不一致
解决:
● 事务
● 消息队列重试(删除缓存的操作,需要修改代码)
● 订阅 MySQL binlog,再操作缓存(使用 canal)
canal + 消息队列,采集 binlog 日志信息发送到 MQ,避免代码侵入(均是异步操作缓存)
canal 实现数据同步,相当于 mysql 的一个从节点,数据库更新后 canal 读取 binlog ,然后更新缓存。

内存回收

内存回收有过期删除和内存淘汰两张策略,内存淘汰是因为内存不足了,即使没有过期的数据也需要淘汰
判断 key 过期:Redis 对设置了过期时间的 key 存储到了一个过期字典中(存储键值对)能快速查找是否过期
过期删除
● 定时删除(key 过期时创建一个定时事件执行删除 key,缺点是占用 CPU)
● 惰性删除(不主动删除过期 key,等到访问时检测是否过期,过期则删除,缺点是占用内存)
● 定期删除(每隔一段时间随机取出一些 key 检查是否过期,过期则删除)
推荐做法是 惰性删除 + 定期删除
内存淘汰
redis 自动进行,当 redis 内存到达设定的 max-memory 时会触发淘汰机制。
○ 不淘汰,内存满了不允许写入新数据
○ volatile-ttl:对设置了 ttl 的key,根据 ttl ,越小的被淘汰
○ allkeys-random:对全体key 随机删除
○ volatile-random:设置了 ttl 的随机淘汰
○ allkeys-lru:最近最少使用
○ volatile-lru
○ allkeys-lfu
○ volatile-lfu:最少频率使用
LRU (Least Recently Used 最近最少使用)存在问题:无法解决缓存污染问题,一次读取了大量的数据,这些数据只会被读取一次,导致保存较长时间
LFU:(Least Frequently Used 最近不常用)访问次数越多,频率越高

高可用

Redis 集群
单机 Redis 存在的问题: 单点故障
数据丢失、并发能力、故障恢复、存储能力
主从同步
主从搭建:replicaof ip port
主从同步的实现:分为全量同步和增量同步,在从节点第一次和主节点建立连接时会采用全量同步,在数据不一致后进行增量同步。

psync 命令:包含主服务器的 runID 和 复制进度, ? 表示还不知道服务器id,-1 表示第一次同步
全量同步:在确定是第一次主从建立连接时,主节点会执行 bgsave 生成 RDB 文件发送给从节点,从节点读取文件实现数据一致。在同步期间,新产生的数据记录到缓冲区(日志文件)。replication buffer
增量同步:主从数据不一致时,从日志文件中读取数据进行同步。
Redis 主从:(读写分离)
主从数据同步原理:
全量同步:
主从第一次建立连接时,会执行全量同步,将 master 节点的所有数据都拷贝给 slave 节点
- slave节点请求增量同步
- master节点判断replid,发现不一致,拒绝增量同步
- master将完整内存数据生成RDB,发送RDB到slave
- slave清空本地数据,加载master的RDB
- master将发送RDB期间的命令记录在repl buffer,并持续将log中的命令发送给slave
- slave执行接收到的命令,保持与master之间的同步
增量同步:
只更新slave与master存在差异的部分数据
全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了。
因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步。
主从优化:在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。
Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
主从复制过程中,key 过期的话,主节点会发送 del 给从节点
replication buffer 和repl backlog buffer 的区别:
replication 是在全量同步期间记录临时的命令,在全量同步完成后清除
repl backlog buffer 是用于增量同步的一个循环的缓冲区
主从节点间的命令复制是异步进行的,可能存在数据不一致
只能尽可能减少影响:保持主从网络状况良好;
主从无法做到故障自动切换,需要手动处理
哨兵机制
实现主从集群的自动故障恢复。(Sentinel)
哨兵对主从节点进行监控,若主节点宕机,会选举一个从节点提升为主节点
sentinel 通过 ping 检测节点是否健康
Redis 哨兵:
Redis 提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。
哨兵用于监控 redis 节点,若有master 故障,Sentinel 会选一个 slave 为 master,通知客户端
集群监控原理:
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线:sentinel节点发现某实例未在规定时间响应
- 客观下线:超过指定数量(quorum)的sentinel都认为该实例主观下线
Spring的 RedisTemplate 底层利用lettuce实现了节点的感知和自动切换
分片集群
多主多从部署,多个 master 保存不同的数据
master 之间通过 ping 相互检测健康状况
Redis 引入 hash 槽,集群中的主节点和 hash 槽绑,有 16384 个 hash 槽,分配给主节点,
key 通过 CRC16 校验后对 16384 取余确定 key 所在的槽,然后找到对应的节点存储
16384 是 2 ^ 14 ,相较于 2 ^ 16 ,所需空间小,分片不至于过多,方便数据迁移。
CRC16 是一个循环冗余校验码,基于多项式除法,最终得到一个 16 位的校验码,计算速度快。
哈希槽映射到 Redis 节点上是可以通过 平均分配或手动分配
Redis 分片集群:
集群中有多个master,每个master保存不同数据
每个master都可以有多个slave节点
master之间通过ping监测彼此健康状态
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
散列插槽:Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上
数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,
分两种情况:
- key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分
- key中不包含“{}”,整个key都是有效部分
例如:key是num,那么就根据num计算,如果是{edc}num,则根据edc计算。
计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
集群伸缩:
添加节点和删除节点
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致
Redis 使用哈希槽,而不使用一致性哈希
● 哈希取模的方式:是在扩容时不太友好,需要迁移大量数据,多倍扩容,3 -> 6,只需要移动 50%
● 一致性哈希算法:先计算 key 的槽位,0~2^32 -1;再然后将槽位和节点映射,顺时针找到最近的节点,
在进行扩容时,只会影响一个节点,存在的问题:可能存在数据倾斜;解决:采用虚拟节点;设置更多的虚 拟节点,也就是一个节点拥有多个虚拟节点(映射)
Redis 采用哈希槽,是因为 Redis Cluster 的特点去中心化,自动伸缩;哈希槽采用 crc16 对 16384个
哈希槽进行取模,这与一致性哈希相似,一致性hash 有 2^32 个槽位,二者节点映射也不同,一致性hash是哈希环顺时针映射, redis 哈希槽是静态映射。不设置更多的槽位,网络带宽问题,占 2KB 而65535 占 8 KB,避免网络拥堵;一致性 hash 考虑的是最少数据迁移,而哈希槽考虑数据的均匀
redis cluster 采用 gossip(流言蜚语)协议,一传十、十传百的方式使得所有节点的元数据达成一致(元数据:哈希槽和节点的映射信息)(映射到某个节点,先查看槽位是否在当前节点,不在则转发到其他节点)
脑裂问题:
出现多个主节点(多个大脑)
可能发生在网络分区或主节点出现问题的时候:
网络分区:对于主从,master ,哨兵和slave 被分割为两个网络,哨兵会发现master 连不上,而从slave 中选取新的master,导致出现两个主节点
主节点问题:master 出现问题时,在哨兵选新节点时,master 又恢复了,也可能出现两个 master
危害:数据不一致、数据重复、数据丢失
如何避免?
配置 min-slaves-to-write 和 min-slaves-max-lag
主库能进行数据同步的最少从库数和主从复制时,从库发送给主库 ACK 最大延迟秒数
(但无法彻底解决,只能合理配置)
多级缓存
Nginx 本地缓存、在 Tomcat 中实现 JVM 进程缓存
JVM 进程缓存:
Caffeine、HashMap、GuavaCache:
Nginx :
OpenResty + lua
数据同步:
设置有效期
同步双写
异步通知: 基于 MQ 或者 Canal

分布式锁

setNx
误删:一个线程执行完成准备删除锁,但此时 key 过期了,导致其他线程获得锁执行相关逻辑,而第一个线程可能删除掉其他线程加的锁。(对 value 设置线程标识)
原子性:即使对 value 设置了线程标识,过程是 获取锁、检查 value 值,删除锁,但仍然可能在检查完 value 值后 key 过期,然后发生误删。保证原子性,可以使用 lua 脚本。
删除时要确保是同一个请求,即要识别,又要删除,无法保证原子性,需要采用 lua 脚本
setNx 存在的问题:
● 不可重入
● 不可重试
● 超时释放
● 集群问题(主从 ==> 加锁后,同步数据到从节点,若主节点宕机,却无法释放锁,导致死锁)
○ Redis 提供了 Redlock 红锁,向多个节点加锁,如果能超过半数则加锁成功
引入 Redission
● 可重入(利用 hash 结构存储锁)
● 可重试(获取锁失败后可等待一段时间重试)
● watch dog 实现超时续约
● 联锁(MultiLock)在集群情况下,主从为例,加锁是对主从所有节点都获得锁才算加锁成功,有节点宕机就加锁失败。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值