一、Redis简介
-
Redis是单线程的吗? 其实这么说不完全正确,我们知道Redis是一个Key-Value的非关系型数据库,我们所理解的Redis单线程主要是指网络IO和K-V的读写是由一个主线程来完成的。但Redis的其他功能,比如说持久化、异步删除、集群数据同步,其实是开启了额外的线程来完成的。 -
Redis单线程为什么还能这么快? 因为Redis是基于内存的,所有的运算都是内存级别的,而且单线程避免了多线程的切换性能耗损问题。 -
Redis单线程如何处理那么多并发客户端连接? 这里就要扯到NIO多路复用模型了,由于本篇主要是Redis的学习记录,这里等Netty的时候再详细学习。
二、Redis的基本数据结构在大厂是怎么用的?
String
-
单值缓存(可以实现常规的缓存) set key value get key -
对象缓存 (可以实现分布式Session, Key为sessionId, Value为用户对象) mset user_name boom user_age 25 mget user_name user_age -
计数器(可以做限流, 阅读数,点赞数,分布式ID等等) 自增:incr num 自减:decr num 加N:incrby num N 减N:decrby num N
-
分布式锁 setnx key value (返回1获取锁成功,0失败)
Hash
-
对象缓存 (它相比于String, 更适合存放对象) hmset user name boom age 25 hmget user name age 很经典的一个例子:购物车 添加商品到购物车:hset cart_用户id 商品id 购买数量 增加购物车商品数量:hincrby cart_用户id 商品id 要增加的数量 获取商品总数: hlen cart_用户id 删除商品 hdel cart_用户id 获取购物车所有的商品: hgetall cart_用户id
List
-
实现队列(FIFO) Lpush(左边进) + Rpop(右边出) -
实现栈(FILO) Lpush(左边进) + Lpop(左边出) -
实现阻塞队列 Lpush(左边进) + BRpop(相比于Rpop会阻塞) -
很经典的一个例子:公众号、微博消息推送 我关注了公众号A
公众号A发了篇文章:Lpush msg_公众号A的id 文章id 我要查看公众号A新的消息(一页四个消息):Lrange msg_公众号A的id 0 4
Set
-
很经典的一个例子:微博的关注模型
-
集合操作
boom关注了a,b,c: sadd boom a b c Tom关注了b,c,d: sadd tom b c d b关注了tom: sadd b tom boom和tom的共同关注的人: sinter boom tom 得到c boom关注的人也关注了tom: sismember tom b boom可能认识的人: sdiff tom b
ZSet
ZSet集合操作
-
很经典的一个例子:微博热搜排行榜
-
ZSet常用操作 并集计算: ZUNIONSTORE destkey numkeys key [key ...] 交集计算: ZINTERSTORE destkey numkeys key [key…]
往有序集合key中加入带分值元素: ZADD key score member [[score member]…] 从有序集合key中删除元素: ZREM key member [member…] 返回有序集合key中元素member的分值: ZSCORE key member 为有序集合key中元素member的分值加上increment: ZINCRBY key increment member 返回有序集合key中元素个数: ZCARD key 正序获取有序集合key从start下标到stop下标的元素: ZRANGE key start stop [WITHSCORES] 倒序获取有序集合key从start下标到stop下标的元素: ZREVRANGE key start stop [WITHSCORES] 点击新闻: ZINCRBY hotNews_20210728 基金大跌 展示当日排行前十: ZREVRANGE hotNews_20210728 0 9 WITHSCORES 七日搜索榜单计算: ZUNIONSTORE hotNews_20210722_20210728 7 hotNews_20210722 hotNews_20210723... hotNews_20210728 展示七日排行前十: ZREVRANGE hotNews_20210722_20210728 0 9 WITHSCORES
三、Redis持久化
RDB
在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时,自动保存一次数据集。
比如说, 设置`save 60 1000`会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集。
关闭RDB只需要将所有的save保存策略注释掉即可
还可以手动执行命令生成RDB快照,客户端执行命令save或bgsave可以生成dump.rdb文件,
每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。
save是同步命令,bgsave是异步命令,bgsave会从Redis主进程fork(fork()是linux函数)出一个子进程专门用来生成rdb快照文件
Redis默认是使用的bgsave
AOF
AOF 持久化: 将修改的每一条指令记录进文件appendonly.aof中
你可以通过修改配置文件来打开 AOF 功能:`appendonly yes`
每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。
这样的话, 当 Redis 重新启动时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。
你可以配置 Redis 多久才将数据 fsync 到磁盘一次。
有三个选项:
1. appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
2. appendfsync everysec:每秒 fsync 一次,足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
3. appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。
推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
AOF和RDB对比:
混合持久化(加强版的AOF)
重启 Redis 时,我们很少使用 RDB来恢复数据,因为会丢失大量数据。
我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 为了解决这个问题,带来了一个新的持久化方式——混合持久化。
通过如下配置可以开启混合持久化:`aof-use-rdb-preamble yes`
如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,
而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,
新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,原子的覆盖原有的AOF文件,完成新旧两个AOF文件的替换。
于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,
因此重启效率大幅得到提升。
混合持久化AOF文件结构:
四、Redis主从、哨兵、集群分析
主从架构
如果你为master配置了一个slave,不管这个slave是否是次连接上Master,它都会发送一个SYNC命令(redis2.8版本之前的命令) master请求复制数据。(从2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据) master收到SYNC命令后,会在后台进行数据持久化通过bgsave生成新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。 当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。 当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,master和slave断开重连后支持部分复制。
Redis主从全量复制:
Redis主从部分复制:
哨兵架构
集群架构
redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单
Redis集群原理分析
Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。
当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。
槽位定位算法
Cluster默认会对 key 值使用 CRC16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位:HASH_SLOT = CRC16(key) mod 16384
跳转重定位
当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。
维护集群的元数据有两种方式:集中式和gossip 集中式: 优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 gossip:
Redis集群选举原理分析
slave发现自己的master变为FAIL 将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack 尝试failover的slave收集master返回的FAILOVER_AUTH_ACK slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的) slave广播Pong消息通知其他集群节点。
当redis.conf的配置cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,
集群仍然可用,如果为yes则集群不可用。
因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,
是达不到选举新master的条件的。
奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,
大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,
所以奇数的master节点更多的是从节省机器资源角度出发说的。
五、Redis缓存淘汰算法
缓存淘汰策略
定时删除:
1、从过期key字典中,随机找20个key。
2、删除20个key中过期的key。
3、如果2中过期的key超过1/4,则重复步。
惰性删除:
内存淘汰机制
内存淘汰机制
。没有配置时,默认为no-eviction
Redis中的内存淘汰机制:
六、从Redis底层搞懂它的渐进式Rehash
哈希表0
里面的所有键值对 rehash 到 哈希表1
里面, 但是, 这个 rehash 动作并不是一次性完成的, 而是分多次、渐进式地完成的。渐进式rehash的详细步骤
为 哈希表1
分配空间,且空间大小为哈希表0
的两倍, 让字典同时持有哈希表0
和哈希表1
两个哈希表。在字典中维持一个索引计数器变量 rehashidx(即哈希表的下标) , 并将它的值设置为 0 , 表示 rehash 工作正式开始。 在 rehash 进行期间, 每次对字典执行CRUD操作时, 程序除了执行指定的操作以外, 还会顺带将 哈希表0
在 rehashidx 索引上的所有键值对 rehash 到哈希表1
, 当 rehash 工作完成之后, 程序将 rehashidx 属性的值+1。随着字典操作的不断执行, 终在某个时间点上, 哈希表0
的所有键值对都会被 rehash 至哈希表1
, 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
渐进式rehash期间的CRUD操作
哈希表0
和 哈希表1
两个哈希表, 所以在渐进式 rehash 进行期间, 字典的CRUD操作会在两个哈希表上进行, 比如要在字典里面查找一个键的话, 程序会先在 哈希表0
里面进行查找, 如果没找到的话, 就会继续到 哈希表1
里面进行查找, 诸如此类。哈希表1
里面, 而 哈希表0
则不再进行任何添加操作:这一措施保证了 哈希表0
包含的键值对数量会只减不增, 并随着 rehash 操作的执行而终变成空表。渐进式rehash带来的问题
七、BitMap如何解决上亿日活的统计问题?
什么是 BitMap
BitMap 有啥用?
用MySQL实现,虽然用MySQL是能实现的,但是为了实现一个统计功能,对MySQL来说将会是一次灾难性的打击。 用Redis的自增,用户登录,我就+1,但是这样统计日活、周活的时候,会出现重复的情况。 在涉及到大数据统计的时候,不妨想想BitMap, 它就是为大数据统计而生。
BitMap统计日活
setbit key user_id 1
。将bitmap中对应位置的值置为1,时间复杂度是O(1)。执行bitcount key
统计bitmap结果有多少个1(即活跃用户数)。operation
可以是 AND
、 OR
、 NOT
、 XOR
这四种操作中的任意一种:BITOP AND destkey key [key ...]
,对一个或多个key
求逻辑并,并将结果保存到destkey
。BITOP OR destkey key [key ...]
,对一个或多个key
求逻辑或,并将结果保存到destkey
。BITOP XOR destkey key [key ...]
,对一个或多个key
求逻辑异或,并将结果保存到destkey
。BITOP NOT destkey key
,对给定key
求逻辑非,并将结果保存到destkey
。
NOT
操作之外,其他操作都可以接受一个或多个 key
作为输入。 destkey
的字符串的长度,和输入 key
中长的字符串长度相等key
进行位元操作,并将结果保存到 destkey
上。getbit result 用户id
, 返回1,即七天内连续登录bitop or result activity_20210725 activity_20210726 activity_20210727 activity_20210728 activity_20210729 activity_20210730 activity_20210731
bitcount result
即得到周活。八、大名鼎鼎的Redis跳跃表
SkipList 跳跃表
struct zslnode{
string value;
double score;
zslnode*[] forwards; //多层连接的指针
zslnode* backward; //回溯指针
}
struct zsl{
zslnode* header; //跳跃表头指针
int maxLevel; //当前节点的高层
map<String,zslnode*> ht; //hash 中的键值对
}
查找
比较 21, 比 21 大,往后面找 比较 37, 比 37大,比链表大值小,从 37 的下面一层开始找 比较 71, 比 71 大,比链表大值小,从 71 的下面一层开始找 比较 85, 比 85 大,从后面找 比较 117, 等于 117, 找到了节点。
插入
删除
例子:删除 71
九、MySQL和Redis双写、读写不一致问题
双写不一致
读写并不一致
解决方案
对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。 就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。 如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁 也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。
作者:Boom
来源:juejin.cn/post/6991080701365846046
版权申明:内容来源网络,仅供分享学习,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!