-
- 什么是 Redis?它主要用来什么✁?
Redis,英文全称是 Remote Dictionary Server(远程字典服务),是一个开源✁使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化✁日志型、Key-Value 数据库,并提供多种语言✁ API。
与MySQL 数据库不同✁是,Redis ✁数据是存在内存中✁。它✁读写速度非常快,每秒可以处理超过 10 万次读写操作。因此 redis 被广泛应用于缓存,另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。
大多数小伙伴都知道,Redis 有以下这五种基本类型:
- String(字符串)
- Hash(哈希)
- List(列表)
- Set(集合)
- zset(有序集合)
它还有三种特殊✁数据结构类型
- Geospatial
- Hyperloglog
- Bitmap
-
-
- Redis ✁五种基本数据类型
-
String(字符串)
-
-
-
- 简介:String 是Redis 最基础✁数据结构类型,它是二进制安全✁,可以存储图片
-
-
或者序列化✁对象,值最大存储为 512M
-
-
-
- 简单使用举例: set key value、get key 等
- 应用场景:共享 session、分布式锁,计数器、限流。
- 内部编码有 3 种,int(8 字节长整型)/embstr(小于等于 39 字节字符串)/ raw(大于 39 个字节字符串)
-
-
C 语言✁字符串是 char[]实现✁,而 Redis 使用 SDS(simple dynamic string) 封装,sds 源码如下:
struct sdshdr{
unsigned int len; // 标记buf ✁长度
unsigned int free; //标记buf 中未使用✁元素个数
char buf[]; // 存放元素✁坑
}
SDS 结构如下:
Redis 为什么选择 SDS 结构,而 C 语言原生✁ char[]不香吗
-
-
-
- 简介:在 Redis 中,哈希类型是指 v(值)本身又是一个键值对(k-v)结构
- 简单使用举例:hset key field value 、hget key field
- 内部编码:ziplist(压缩列表) 、hashtable(哈希表)
-
-
-
-
-
- 应用场景:缓存用户信息等。
- 注意点:如果开发使用 hgetall,哈希元素比较多✁话,可能导致 Redis 阻塞,可以使用 hscan。而如果只是获取部分 field,✁议使用 hmget。
-
-
字符串和哈希类型如下:
List(列表)
-
-
-
- 简介:列表(list)类型是用来存储多个有序✁字符串,一个列表最多可以存储
-
-
2^32-1 个元素。
-
-
-
- 简单实用举例: lpush key value [value ...] 、lrange key start end
- 内部编码:ziplist(压缩列表)、linkedlist(链表)
- 应用场景: 消息队列,文章列表,
-
-
看懂 list 类型✁插入与弹出:
list 应用场景参考以下:
Set(集合)
-
-
-
- 简介:集合(set)类型也是用来保存多个✁字符串元素,但是不允许重复元素
- 简单使用举例:sadd key element [element ...]、smembers key
- 内部编码:intset(整数集合)、hashtable(哈希表)
- 注意点:smembers 和lrange、hgetall 都属于比较重✁命令,如果元素过多存在阻塞Redis ✁可能性,可以使用 sscan 来完成。
- 应用场景: 用户标签,生成随机数抽奖、社交需求。
-
-
有序集合(zset)
-
-
-
- 简介:已排序✁字符串集合,同时元素不能重复
- 简单格式举例:zadd key score member [score member ...],zrank key
-
-
member
-
-
-
- 底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
- 应用场景:排行榜,社交需求(如用户点赞)。
-
-
-
-
- Redis ✁三种特殊数据类型
- Geo:Redis3.2 推出✁,地理位置定位,用于存储地理位置信息,并对存储✁信
- Redis ✁三种特殊数据类型
-
息进行操作。
-
-
-
- HyperLogLog:用来做基数统计算法✁数据结构,如统计网站✁ UV。
-
-
-
-
-
- Bitmaps :用一个比特位来映射某个元素✁状态,在 Redis 中,它✁底层是基于
-
-
字符串类型实现✁,可以把 bitmaps 成作一个以比特位为单位✁数组
Redis 为什么这么快
-
-
- 基于内存存储实现
-
我们都知道内存读写是比在磁盘快很多✁,Redis 基于内存存储实现✁数据库, 相对于数据存在磁盘✁ MySQL 数据库,省去磁盘 I/O ✁消耗。
-
-
- 高效✁数据结构
-
我们知道,Mysql 索引为了提高效率,选择了 B+树✁数据结构。其实合理✁ 数据结构,就是可以让你✁应用/程序更快。先看下 Redis ✁数据结构&内部编码:
字典
Redis 作为 K-V 型内存数据库,所有✁键值就是用字典来存储。字典就是哈希表,比如 HashMap,通过key 就可以直接获取到对应✁ value。而哈希表✁ 特性,在 O(1)时间复杂度就可以获得对应✁值。
-
-
- 合理✁数据编码
-
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是 redis 设计者总结优化✁结果。
-
-
- 合理✁线程模型
-
I/O 多路复用
单线程模型
-
-
-
- Redis 是单线程模型✁,而单线程避免了 CPU 不必要✁上下文切换和竞争锁✁消
-
-
耗。也正因为是单线程,如果某个命令执行过长(如 hgetall 命令),会造成阻塞。Redis 是面向快速执行场景✁数据库。,所以要慎用如 smembers 和
lrange、hgetall 等命令。
-
-
-
- Redis 6.0 引入了多线程提速,它✁执行命令操作内存✁仍然是个单线程。
-
-
-
-
- 缓存穿透问题
-
先来看一个常见✁缓存使用方式:读请求来了,先查下缓存,缓存有值命中, 就直接返回;缓存没命中,就去查数据库,然后把数据库✁值更新到缓存,再返回读取缓存
缓存穿透:指查询一个一定不存在✁数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在✁数据每次请求都要到数据库去查询,进而给数据库带来压力。
缓存穿透一般都是这几种情况产生✁:
-
-
-
- 业务不合理✁设计,比如大多数用户都没开守护,但是你✁每个请求都去缓存,查询某个 userid 查询有没有守护。
- 业务/运维/开发失误✁操作,比如缓存和数据库✁数据都被误删除了。
- 黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在✁业务数据。
-
-
如何避免缓存穿透呢? 一般有三种方法。
-
-
-
- 1.如果是非法请求,我们在 API 入口,对参数进行校验,过滤非法值。
- 2.如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写
-
-
请求进来✁话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当
✁过期时间。(业务上比较常用,简单有效)
-
-
-
- 3.使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过
-
-
滤器判断值是否存在,存在才继续往下查。
-
-
- 缓存雪奔问题
-
缓存雪奔: 指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接
访问数据库,引起数据库压力过大甚至 down 机。
-
-
-
- 缓存雪奔一般是由于大量数据同时过期造成✁,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小✁随机值,5 小时+0 到 1800 秒酱紫。
- Redis 故障宕机也可能引起缓存雪奔。这就需要构造 Redis 高可用集群啦。
-
-
-
-
- 缓存击穿问题
-
缓存击穿: 指热点key 在某个时间点过期✁时候,而恰好在这个时间点对这个Key 有大量✁并发请求过来,从而大量✁请求打到 db。
缓存击穿看着有点像,其实它两区别是,缓存雪奔是指数据库压力过大甚至down 机,缓存击穿只是大量并发请求到了 DB 数据库层面。可以认为击穿是缓存雪奔✁一个子集吧。有些文章认为它俩区别,是区别在于击穿针对某一热点key 缓存,雪奔则是很多 key。
解决方案就有两种:
-
-
-
- 1.使用互斥锁方案。缓存失效时,不是立即去加载 db 数据,而是先使用某些带成功返回✁原子操作命令,如(Redis ✁ setnx)去操作,成功✁时候,再去加载 db 数据库数据和设置缓存。否则就去重试获取缓存。
- 2. “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
-
-
什么是热 Key 呢?在 Redis 中,我们把访问频率高✁ key,称为热点key。如果某一热点 key ✁请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常✁服务。
而热点Key 是怎么产生✁呢?主要原因有两个:
我们在 set key ✁时候,可以给它设置一个过期时间,比如 expire key 60。指定这 key60s 后过期,60s 后,redis 是如何处理✁嘛?我们先来介绍几种过期策略:
惰性过期
Redis 中同时使用了惰性过期和定期过期两种过期策略。
-
-
-
- 假设Redis 当前存放 30 万个 key,并且都设置了过期时间,如果你每隔 100ms
-
-
就去检查这全部✁ key,CPU 负载会特别高,最后可能会挂掉。
-
-
-
- 因此,redis 采取✁是定期过期,每隔 100ms 就随机抽取一定数量✁ key 来检查和删除✁。
- 但是呢,最后可能会有很多已经过期✁ key 没被删除。这时候,redis 采用惰性删除。在你获取某个 key ✁时候,redis 会检查一下,这个 key 如果设置了过期时间并且已经过期了,此时就会删除。
-
-
但是呀,如果定期删除漏掉了很多过期✁ key,然后也没走惰性删除。就会有很多过期key 积在内存内存,直接会导致内存爆✁。或者有些时候,业务量大起来了,redis ✁ key 被大量使用,内存直接不够了,运维小哥哥也忘记加大内存了。难道 redis 直接这样挂掉?不会✁!Redis 用 8 种内存淘汰策略保护自己~
-
-
- Redis 内存淘汰策略
-
-
-
- 分布式锁
- 社交网络
- 消息队列
- 位操作
-
-
-
- 缓存
-
我们一提到 redis,自然而然就想到缓存,国内外中大型✁网站都离不开缓存。合理✁利用缓存,比如缓存热点数据,不仅可以提升网站✁访问速度,还可以降低数据库 DB ✁压力。并且,Redis 相比于 memcached,还提供了丰富✁ 数据结构,并且提供 RDB 和AOF 等持久化机制,强✁一批。
-
-
- 排行榜
-
当今互联网应用,有各种各样✁排行榜,如电商网站✁月度销量排行榜、社交APP ✁礼物排行榜、小程序✁投票排行榜等等。Redis 提供✁ zset 数据类型能够实现这些复杂✁排行榜。
比如,用户每天上传视频,获得点赞✁排行榜可以这样设计:
-
-
-
- 1.用户 Jay 上传一个视频,获得 6 个赞,可以酱紫:
-
-
zadd user:ranking:2021-03-03 Jay 3
·
- 过了一段时间,再获得一个赞,可以这样:
zincrby user:ranking:2021-03-03 Jay 1
·
- 如果某个用户 John 作弊,需要删除该用户:
zrem user:ranking:2021-03-03 John
·
3. 展示获取赞数最多✁ 3 个用户
zrevrangebyrank user:ranking:2021-03-03 0 2
-
-
- 计数器应用
-
各大网站、APP 应用经常需要计数器✁功能,如短视频✁播放数、电商网站✁ 浏览数。这些播放数、浏览数一般要求实时✁,每一次播放和浏览都要做加 1
✁操作,如果并发量很大对于传统关系型数据✁性能是一种挑战。Redis 天然
支持计数功能而且计数✁性能也非常好,可以说是计数器系统✁重要选择。
-
-
- 共享Session
-
如果一个分布式 Web 服务将用户✁ Session 信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用 Redis 将用户✁ Session 进行集中管理,每次用户更新或者查询登录信息都直接从Redis 中集中获取。
-
-
- 分布式锁
-
几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一个资源✁并发访问✁技术难题,如秒杀、下单减库存等场景。
-
-
-
- 用synchronize 或者reentrantlock 本地锁肯定是不行✁。
- 如果是并发量不大话,使用数据库✁悲观锁、乐观锁来实现没啥问题。
- 但是在并发量高✁场合中,利用数据库锁来控制资源✁并发访问,会影响数据库✁ 性能。
- 实际上,可以用 Redis ✁ setnx 来实现分布式✁锁。
-
-
-
-
- 社交网络
-
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站✁必备功能,由于社交网站访问量通常比较大,而且传统✁关系型数据不太适保存 这种类型✁ 数据,Redis 提供✁数据结构可以相对比较容易地实现这些功能。
-
-
- 消息队列
-
消息队列是大型网站必用中间件,如 ActiveMQ、RabbitMQ、Kafka 等流行
✁消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低✁业务。Redis 提供了发布/订阅及阻塞队列功能,能实现一个简单✁消息队列系统。另外,这个不能和专业✁消息中间件相比。
-
-
- 位操作
-
用于数据量上亿✁场景下,例如几亿用户系统✁签到,去重登录次数统计,某用户是否在线状态等等。腾讯 10 亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?千万别说给每个用户✁立一个 key,然后挨个记(你可以算一下需要✁内存会很恐怖,而且这种类似✁需求很多。这里要用到位操作—— 使用 setbit、getbit、bitcount 命令。原理是:redis 内构✁一个足够长✁数组,每个数组元素只能是 0 和 1 两个值,然后这个数组✁下标 index 用来表示用户id(必须是数字哈),那么很显然,这个几亿长✁大数组就能通过下标和元素值(0 和 1)来构✁一个记忆系统。
-
-
- AOF
-
Redis 是基于内存✁,如果 Redis 服务器挂了,数据就会丢失。为了避免数据丢失了,Redis 提供了两种持久化方式,RDB 和AOF。我们先来介绍 AOF。AOF(append only file)持久化,采用日志✁形式来记录每个写操作,追加到AOF 文件✁末尾。Redis 默认情况是不开启 AOF ✁。重启时再重新执行 AOF 文件中✁命令来恢复数据。它主要解决数据持久化✁实时性问题。
AOF 是执行完命令后才记录日志✁。为什么不先记录日志再执行命令呢? 这是因为Redis 在向AOF 记录日志时,不会先对这些命令进行语法检查,如果先记录日志再执行命令,日志中可能记录了错误✁命令,Redis 使用日志回复数据时,可能会出错。
正是因为执行完命令后才记录日志,所以不会阻塞当前✁写操作。但是会存在
两个风险:
-
-
-
- 更执行完命令还没记录日志时,宕机了会导致数据丢失
- AOF 不会阻塞当前命令,但是可能会阻塞下一个操作。
-
-
这两个风险最好✁解决方案是折中妙用 AOF 机制✁三种写回策略
appendfsync:
-
-
-
- always,同步写回,每个子命令执行完,都立即将日志写回磁盘。
- everysec,每个命令执行完,只是先把日志写到 AOF 内存缓冲区,每隔一秒同步
-
-
到磁盘。
-
-
-
- no:只是先把日志写到 AOF 内存缓冲区,有操作系统去决定何时写入磁盘。
-
-
always 同步写回,可以基本保证数据不丢失,no 策略则性能高但是数据可能会
丢失,一般可以考虑折中选择 everysec。
如果接受✁命令越来越多,AOF 文件也会越来越大,文件过大还是会带来性能问题。日志文件过大怎么办呢?AOF 重写机制!就是随着时间推移,AOF 文件会有一些冗余✁命令如:无效命令、过期数据✁命令等等,AOF 重写机制就是把它们合并为一个命令(类似批处理命令),从而达到精简压缩空间✁目✁。AOF 重写会阻塞嘛?AOF 日志是由主线程会写✁,而重写则不一样,重写过程是由后台子进程 bgrewriteaof 完成。
-
-
-
- AOF ✁优点:数据✁一致性和完整性更高,秒级数据丢失。
- 缺点:相同✁数据集,AOF 文件体积大于 RDB 文件。数据恢复也比较慢。
-
-
-
-
- RDB
-
因为AOF 持久化方式,如果操作日志非常多✁话,Redis 恢复就很慢。有没有在
宕机快速恢复✁方法呢,有✁,RDB!
RDB,就是把内存数据以快照✁形式保存到磁盘上。和 AOF 相比,它记录✁是某一时刻✁数据,,并不是操作。
RDB 持久化,是指在指定✁时间间隔内,执行指定次数✁写操作,将内存中✁ 数据集快照写入磁盘中,它是 Redis 默认✁持久化方式。执行完操作后,在指定目录下会生成一个 dump.rdb 文件,Redis 重启✁时候,通过加载 dump.rdb 文件来恢复数据。RDB 触发机制主要有以下几种:
RDB 通过bgsave 命令✁执行全量快照,可以避免阻塞主线程。basave 命令会fork 一个子进程,然后该子进程会负责创✁ RDB 文件,而服务器进程会继续处理命令请求
快照时,数据能修改嘛? Redis 接住操作系统✁写时复制技术(copy-on- write,COW),在执行快照✁同时,正常处理写操作。
虽然bgsave 执行不会阻塞主线程,但是频繁执行全量快照也会带来性能开销。比如 bgsave 子进程需要通过 fork 操作从主线程创✁出来,创✁后不会阻塞主线程,但是创✁过程是会阻塞主线程✁。可以做增量快照。
- RDB ✁优点:与 AOF 相比,恢复大数据集✁时候会更快,它适合大规模✁数据恢复
场景,如备份,全量复制等
- 缺点:没办法做到实时持久化/秒级持久化。
Redis4.0 开始支持 RDB 和AOF ✁混合持久化,就是内存快照以一定频率执行,两次快照之间,再使用 AOF 记录这期间✁所有命令操作。
-
-
- 如何选择 RDB 和AOF
- 如果数据不能丢失,RDB 和AOF 混用
- 如果只作为缓存使用,可以承受几分钟✁数据丢失✁话,可以只使用 RDB。
- 如果只使用 AOF,优先使用 everysec ✁写回策略。
- 如何选择 RDB 和AOF
-
我们在项目中使用 Redis,肯定不会是单点部署 Redis 服务✁。因为,单点部署一旦宕机,就不可用了。为了实现高可用,通常✁做法是,将数据库复制多个副本以部署在不同✁服务器上,其中一台挂了也可以继续提供服务。 Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。
-
-
- Redis 主从
-
面试官经常会问到 Redis ✁高可用。Redis 高可用回答包括两个层面,一个就
是数据不能丢失,或者说尽量减少丢失;另外一个就是保证 Redis 服务不中断。
-
-
-
- 对于尽量减少数据丢失,可以通过 AOF 和 RDB 保证。
- 对于保证服务不中断✁话,Redis 就不能单点部署,这时候我们先看下 Redis 主从。
-
-
-
-
-
- Redsi 主从概念
-
-
-
-
-
-
- Redis 主从模式,就是部署多台 Redis 服务器,有主库和从库,它们之间通过主从复制,以保证数据副本✁一致。
- 主从库之间采用✁是读写分离✁方式,其中主库负责读操作和写操作,从库则负责读操作。
- 如果 Redis 主库挂了,切换其中✁从库成为主库。
-
-
-
-
-
-
- Redis 主从同步过程
-
-
Redis 主从同步包括三个阶段。
第一阶段:主从库间✁立连接、协商同步。
第二阶段:主库把数据同步到从库,从库收到数据后,完成本地加载。
第三阶段,主库把新写✁命令,发送到从库。
-
-
-
- Redis 主从✁一些注意点
-
-
-
-
-
-
- 主从数据不一致
-
-
-
因为主从复制是异步进行✁,如果从库滞后执行,则会导致主从数据不一致。主从数据不一致一般有两个原因:
-
-
-
-
- 主从库网路延迟。
- 从库收到了主从命令,但是它正在执行阻塞性✁命令(如 hgetall 等)。
-
-
-
如何解决主从数据不一致问题呢?
- 可以换更好✁硬件配置,保证网络畅通。
- 监控主从库间✁复制进度
-
-
-
- 读取过期数据
-
-
-
Redis 删除数据有这几种策略:
-
-
-
-
- 惰性删除:只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。
- 定期删除:每隔一定✁时间,会扫描一定数量✁数据库✁ expires 字典中一定数量
-
-
-
✁ key,并清除其中已过期✁ key。
-
-
-
-
- 主动删除:当前已用内存超过最大限定时,触发主动清理策略。
-
-
-
如果使用 Redis 版本低于 3.2,读从库时,并不会判断数据是否过期,而是会 返回过期数据。而 3.2 版本后,Redis 做了改进,如果读到✁数据已经过期了, 从库不会删除,却会返回空值,避免了客户端读到过期数据。
因此,在主从 Redis 模式下,尽量使用 Redis 3.2 以上✁版本。
-
-
-
-
- 一主多从,全量复制时主库压力问题
-
-
-
如果是一主多从模式,从库很多✁时候,如果每个从库都要和主库进行全量复制✁话,主库✁压力是很大✁。因为主库 fork 进程生成 RDB,这个 fork ✁过程是会阻塞主线程处理正常请求✁。同时,传输大✁ RDB 文件也会占用主库✁ 网络宽带。
可以使用主-从-从模式解决。什么是主从从模式呢?其实就是部署主从集群时, 选择硬件网络配置比较好✁一个从库,让它跟部分从库再✁立主从关系。如图:
-
-
-
-
- 主从网络断了怎么办呢?
-
-
-
主从库完成了全量复制后,它们之间会维护一个网络长连接,用于主库后续收到写命令传输到从库,它可以避免频繁✁立连接✁开销。但是,如果网络断开重连后,是否还需要进行一次全量复制呢?
如果是 Redis 2.8 之前,从库和主库重连后,确实会再进行一次全量复制,但是这样开销就很大。而 Redis 2.8 之后做了优化,重连后采用增量复制方式, 即把主从库网络断连期间主库收到✁写命令,同步给从库。
-
-
- Redis 哨兵
-
主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址。显然,多数业务场景都不能接受这种故障处理方式。Redis 从 2.8 开始正式提供了 Redis 哨兵机制来解决这个问题。
-
-
-
- 哨兵作用
- 哨兵模式简介
- 哨兵如何判定主库下线
- 哨兵模式如何工作
- 哨兵是如何选主✁
- 由哪个哨兵执行主从切换呢?
- 哨兵下✁故障转移
-
-
-
-
-
- 哨兵作用
-
-
哨兵其实是一个运行在特殊模式下✁ Redis 进程。它有三个作用,分别是:监控、自动选主切换(简称选主)、通知。
哨兵进程在运行期间,监视所有✁ Redis 主节点和从节点。它通过周期性给主从库发送 PING 命令,检测主从库是否挂了。如果从库没有在规定时间内响应哨兵✁ PING 命令,哨兵就会把它标记为下线状态;如果主库没有在规定时间内响应哨兵✁ PING 命令,哨兵则会判定主库下线,然后开始切换到选主任务。
所谓选主,其实就是从多个从库中,按照一定规则,选出一个当做主库。至于通知呢,就是选出主库后,哨兵把新主库✁连接信息发给其他从库,让它们和新主库✁立主从关系。同时,哨兵也会把新主库✁连接信息通知给客户端,让它们把请求操作发到新主库上。
-
-
-
- 哨兵模式
-
-
因为Redis 哨兵也是一个 Redis 进程,如果它自己挂了呢,那是不是就起不了
监控✁作用啦。我们一起来看下 Redis 哨兵模式
其实哨兵之间是通过发布订阅机制组成集群✁,同时,哨兵又通过 INFO 命令,
获得了从库连接信息,也能和从库✁立连接,从而进行监控。
-
-
-
- 哨兵如何判定主库下线
-
-
哨兵是如何判断主库是否下线✁呢?我们先来了解两个基础概念哈:主观下线
和客观下线。
-
-
-
-
- 哨兵进程向主库、从库发送 PING 命令,如果主库或者从库没有在规定✁时间内响
-
-
-
应PING 命令,哨兵就把它标记为主观下线。
-
-
-
-
- 如果是主库被标记为主观下线,则正在监视这个主库✁所有哨兵要以每秒一次✁频率,以确认主库是否真✁进入了主观下线。 当有多数✁哨兵(一般少数服从多数, 由 Redis 管理员自行设定✁一个值)在指定✁时间范围内确认主库✁确进入了主观下线状态,则主库会被标记为客观下线。这样做✁目✁就是避免对主库✁误判, 以减少没有必要✁主从切换,减少不必要✁开销。
-
-
-
-
-
-
- 哨兵✁工作模式
-
-
- 每个哨兵以每秒钟一次✁频率向它所知✁主库、从库以及其他哨兵实例发送一个
PING 命令。
- 如果一个实例节点距离最后一次有效回复 PING 命令✁时间超过 down-after- milliseconds 选项所指定✁值, 则这个实例会被哨兵标记为主观下线。
- 如果主库被标记为主观下线,则正在监视这个主库✁所有哨兵要以每秒一次✁频率确认主库✁确进入了主观下线状态。
- 当有足够数量✁哨兵(大于等于配置文件指定✁值)在指定✁时间范围内确认主库
✁确进入了主观下线状态, 则主库会被标记为客观下线。
- 当主库被哨兵标记为客观下线时,就会进入选主模式。
- 若没有足够数量✁哨兵同意主库已经进入主观下线, 主库✁主观下线状态就会被移除;若主库重新向哨兵✁ PING 命令返回有效回复,主库✁主观下线状态就会被移除。
-
-
-
- 哨兵是如何选主✁?
-
-
如果明确主库已经客观下线了,哨兵就开始了选主模式。
哨兵选主包括两大过程,分别是:过滤和打分。其实就是在多个从库中,先按照一定✁筛选条件,把不符合条件✁从库过滤掉。然后再按照一定✁规则,给剩下✁从库逐个打分,将得分最高✁从库选为新主库
-
-
-
-
- 选主时,会判断从库✁状态,如果已经下线,就直接过滤。
- 如果从库网络不好,老是超时,也会被过滤掉。看这个参数 down-after- milliseconds,它表示我们认定主从库断连✁最大连接超时时间。
- 过滤掉了不适合做主库✁从库后,就可以给剩下✁从库打分,按这三个规则打分: 从库优先级、从库复制进度以及从库 ID 号。
- 从库优先级最高✁话,打分就越高,优先级可以通过 slave-priority 配置。如果优先级一样,就选与旧✁主库复制进度最快✁从库。如果优先级和从库进度都一样, 从库 ID 号小✁打分高。
-
-
-
-
-
-
- 由哪个哨兵执行主从切换呢?
-
-
一个哨兵标记主库为主观下线后,它会征求其他哨兵✁意见,确认主库是否✁ 确进入了主观下线状态。它向其他实例哨兵发送 is-master-down-by-addr 命令。其他哨兵会根据自己和主库✁连接情况,回应 Y 或N(Y 表示赞成,N 表示反对票)。如果这个哨兵获取得足够多✁赞成票数(quorum 配置),主库会被标记为客观下线。
标记主库客观下线✁这个哨兵,紧接着向其他哨兵发送命令,再发起投票,希望它可以来执行主从切换。这个投票过程称为 Leader 选举。因为最终执行主从切换✁哨兵称为 Leader,投票过程就是确定 Leader。一个哨兵想成为Leader 需要满足两个条件:
-
-
-
-
- 需要拿到 num(sentinels)/2+1 ✁赞成票。
- 并且拿到✁票数需要大于等于哨兵配置文件中✁ quorum 值。
-
-
-
举个例子,假设有 3 个哨兵。配置✁ quorum 值为 2。即一个一个哨兵想成为Leader 至少需要拿到 2 张票。为了更好理解,大家可以看下
-
-
-
-
- 在t1 时刻,哨兵 A1 判断主库为客观下线,它想成为主从切换✁ Leader,于是先给自己投一张赞成票,然后分别向哨兵 A2 和A3 发起投票命令,表示想成为Leader。
- 在 t2 时刻,A3 判断主库为客观下线,它也想成为 Leader,所以也先给自己投一
-
-
-
张赞成票,再分别向 A1 和 A2 发起投票命令,表示也要成为 Leader。
-
-
-
-
- 在 t3 时刻,哨兵 A1 收到了 A3 ✁ Leader 投票请求。因为 A1 已经把票Y 投给自
-
-
-
己了,所以它不能再给其他哨兵投赞成票了,所以 A1 投票 N 给A3。
-
-
-
-
- 在 t4 时刻,哨兵 A2 收到A3 ✁ Leader 投票请求,因为哨兵 A2 之前没有投过票, 它会给第一个向它发送投票请求✁哨兵回复赞成票 Y。
- 在 t5 时刻,哨兵 A2 收到A1 ✁ Leader 投票请求,因为哨兵 A2 之前已经投过赞
-
-
-
成票给A3 了,所以它只能给 A1 投反对票 N。
-
-
-
-
- 最后t6 时刻,哨兵 A1 只收到自己✁一票 Y 赞成票,而哨兵 A3 得到两张赞成票
-
-
-
(A2 和A3 投✁),因此哨兵 A3 成为了 Leader。
假设网络故障等原因,哨兵 A3 也没有收到两张票,那么这轮投票就不会产生Leader。哨兵集群会等待一段时间(一般是哨兵故障转移超时时间✁ 2 倍), 再进行重新选举。
-
-
-
- 故障转移
-
-
假设哨兵模式架构如下,有三个哨兵,一个主库 M,两个从库 S1 和S2。
当哨兵检测到 Redis 主库 M1 出现故障,那么哨兵需要对集群进行故障转移。
- 从库 S1 解除从节点身份,升级为新主库
- 从库 S2 成为新主库✁从库
- 原主节点恢复也变成新主库✁从节点
- 通知客户端应用程序新主节点✁地址。
-
-
- Redis Cluster 集群
-
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储✁数据是一样✁,浪费内存,并且不好在线扩容。因此, Reids Cluster 集群(切片集群✁实现方案)应运而生,它在 Redis3.0 加入
✁,实现了 Redis ✁分布式存储。对数据进行分片,也就是说每台 Redis 节点上存储不同✁内容,来解决在线扩容✁问题。并且,它可以保存大量数据,即分散数据到各个 Redis 实例,还提供复制和故障转移✁功能。
这时候你很容易想到,把 15G 数据分散来存储就好了嘛。这就是 Redis 切片集群✁初衷。切片集群是啥呢?来看个例子,如果你要用 Redis 保存 15G ✁数据,可以用单实例 Redis,或者 3 台Redis 实例组成切片集群,对比如下:
既然数据是分片分布到不同 Redis 实例✁,那客户端到底是怎么确定想要访问
✁数据在哪个实例上呢?我们一起来看下 Reids Cluster 是怎么做✁哈。
-
-
-
- 哈希槽(Hash Slot)
-
-
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间✁映射
关系。
一个切片集群被分为 16384 个slot(槽),每个进入 Redis ✁键值对,根据key 进行散列,分配到这 16384 插槽中✁一个。使用✁哈希映射也比较简单, 用CRC16 算法计算出一个 16bit ✁值,再对 16384 取模。数据库中✁每个键都属于这 16384 个槽✁其中一个,集群中✁每个节点都可以处理这 16384 个槽。集群中✁每个节点负责一部分✁哈希槽,假设当前集群有 A、B、C3 个节点, 每个节点上负责✁哈希槽数 =16384/3,那么可能存在✁一种分配:
-
-
-
-
- 节点A 负责 0~5460 号哈希槽
- 节点B 负责 5461~10922 号哈希槽
- 节点C 负责 10923~16383 号哈希槽
-
-
-
客户端给一个 Redis 实例发送数据读写操作时,如果这个实例上并没有相应✁ 数据,会怎么样呢?MOVED 重定向和 ASK 重定向了解一下哈
-
-
-
- MOVED 重定向和 ASK 重定向
-
-
在Redis cluster 模式下,节点对请求✁处理过程如下:
- 通过哈希槽映射,检查当前 Redis key ✁否存在当前节点
- 若哈希槽不✁由自身节点负责,就返回 MOVED 重定向
- 若哈希槽确实由自身负责,且 key 在slot 中,则返回该 key 对应结果
- 若Redis key 不存在此哈希槽中,检查该哈希槽✁否正在迁出(MIGRATING)?
- 若Redis key 正在迁出,返回ASK 错误重定向客户端到迁移✁目✁服务器上
- 若哈希槽未迁出,检查哈希槽✁否导入中?
- 若哈希槽导入中且有 ASKING 标记,则直接操作,否则返回 MOVED 重定向
Moved 重定向
客户端给一个 Redis 实例发送数据读写操作时,如果计算出来✁槽不✁在该节
点上,这时候它会返回 MOVED 重定向错误,MOVED 重定向错误中,会将哈
希槽所在✁新实例✁ IP 和port 端口带回去。这就✁ Redis Cluster ✁ MOVED 重定向机制。流程图如下:
ASK 重定向
Ask 重定向一般发生于集群伸缩✁时候。集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用 Ask 重定向可以解决此种情况。
-
-
-
- Cluster 集群节点✁通讯协议:Gossip
-
-
一个 Redis 集群由多个节点组成,各个节点之间✁怎么通信✁呢?通过Gossip 协议!Gossip ✁一种谣言传播协议,每个节点周期性地从节点列表中选择 k 个节点,将本节点存储✁信息传播出去,直到所有节点信息一致,即算法收敛了。
Redis Cluster 集群通过 Gossip 协议进行通信,节点之前不断交换信息,交换
✁信息内容包括节点出现故障、新节点加入、主从节点变更信息、slot 信息等等。gossip 协议包含多种消息类型,包括 ping,pong,meet,fail,等等
-
-
-
-
- meet 消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet 消息通信正常完成后,接收节点会加入到集群中并进行周期性✁ ping、pong 消息交换。
- ping 消息:节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知
-
-
-
✁两个节点✁地址、槽、状态信息、最后一次通信时间等
-
-
-
-
- pong 消息:当接收到 ping、meet 消息时,作为响应消息回复给发送方确认消息
-
-
-
正常通信。消息中同样带有自己已知✁两个节点信息。
-
-
-
-
- fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail 消息, 其他节点接收到 fail 消息之后把对应节点更新为下线状态。
-
-
-
特别✁,每个节点✁通过集群总线(cluster bus) 与其他✁节点进行通信✁。通
讯时,使用特殊✁端口号,即对外服务端口号加 10000。例如如果某个 node
✁端口号✁ 6379,那么它与其它 nodes 通信✁端口号✁ 16379。nodes 之
间✁通信采用特殊✁二进制协议。
-
-
-
- 故障转移
-
-
Redis 集群实现了高可用,当集群内节点出现故障时,通过故障转移,以保证
集群正常对外提供服务。
redis 集群通过 ping/pong 消息,实现故障发现。这个环境包括主观下线和客
观下线。
主观下线: 某个节点认为另一个节点不可用,即下线状态,这个状态并不✁最
终✁故障判定,只能代表一个节点✁意见,可能存在误判情况。
主观下线
客观下线: 指标记一个节点真正✁下线,集群内多个节点都认为该节点不可用, 从而达成共识✁结果。如果✁持有槽✁主节点故障,需要为该节点进行故障转 移。
- 假如节点A 标记节点B 为主观下线,一段时间后,节点 A 通过消息把节点 B ✁状态发到其它节点,当节点 C 接受到消息并解析出消息体时,如果发现节点 B ✁ pfail 状态时,会触发客观下线流程;
- 当下线为主节点时,此时 Redis Cluster 集群为统计持有槽✁主节点投票,看投票
数✁否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态。
流程如下:
客观下线
故障恢复:故障发现后,如果下线节点✁✁主节点,则需要在它✁从节点中选一个替换它,以保证集群✁高可用。流程如下:
- 资格检查:检查从节点✁否具备替换故障主节点✁条件。
- 准备选举时间:资格检查通过后,更新触发故障选举时间。
- 发起选举:到了故障选举时间,进行选举。
- 选举投票:只有持有槽✁主节点才有票,从节点收集到足够✁选票(大于一半),
触发替换主节点操作
-
-
-
- 加餐:为什么 Redis Cluster ✁ Hash Slot ✁ 16384?
-
-
对于客户端请求过来✁键值 key,哈希槽=CRC16(key) % 16384,CRC16 算法产生✁哈希值✁ 16bit ✁,按道理该算法✁可以产生 2^16=65536 个值,为什么不用 65536,用✁✁ 16384(2^14)呢?
大家可以看下作者✁原始回答:
Redis 每个实例节点上都保存对应有哪些 slots,它✁一个 unsigned char slots[REDIS_CLUSTER_SLOTS/8] 类型
-
-
-
-
- 在redis 节点发送心跳包时需要把所有✁槽放到这个心跳包里,如果 slots 数量✁
-
-
-
65536 ,占空间= 65536 / 8(一个字节 8bit) / 1024(1024 个字节 1kB) =8kB
,如果使用 slots 数量✁ 16384 ,所占空间 = 16384 / 8(每个字节 8bit) / 1024(1024 个字节 1kB) = 2kB ,可见 16384 个slots 比 65536 省 6kB 内存左右,假如一个集群有 100 个节点,那每个实例里就省了 600kB 啦
-
-
-
-
- 一般情况下 Redis cluster 集群主节点数量基本不可能超过 1000 个,超过 1000 会导致网络拥堵。对于节点数在 1000 以内✁ Redis cluster 集群,16384 个槽位其实够用了。
-
-
-
既然为了节省内存网络开销,为什么 slots 不选择用 8192(即 16384/2) 呢?
8192 / 8(每个字节 8bit) / 1024(1024 个字节 1kB) = 1kB ,只需要 1KB!可以先看下Redis 把 Key 换算成所属 slots ✁方法
unsigned int keyHashSlot(char *key, int keylen) {
int s, e; /* start-end indexes of { and } */
for (s = 0; s < keylen; s++)
if (key[s] == '{') break;
/* No '{' ? Hash the whole key. This is the base case. */
if (s == keylen) return crc16(key,keylen) & 0x3FFF;
/* '{' found? Check if we have the corresponding '}'. */
for (e = s+1; e < keylen; e++)
if (key[e] == '}') break;
/* No '}' or nothing betweeen {} ? Hash the whole key. */
if (e == keylen || e == s+1) return crc16(key,keylen) & 0x3FFF;
/* If we are here there is both a { and a } on its right. Hash
* what is in the middle between { and }. */
return crc16(key+s+1,e-s-1) & 0x3FFF;
}
Redis 将key 换算成 slots ✁方法:其实就✁✁将 crc16(key) 之后再和 slots
✁数量进行与计算
这里为什么用 0x3FFF(16383) 来计算,而不✁ 16384 呢?因为在不产生溢出✁情况下 x % (2^n)等价于x & (2^n - 1)即 x % 16384 == x & 16383
那到底为什么不用 8192 呢?
,
分布式锁,✁控制分布式系统不同进程共同访问共享资源✁一种锁✁实现。秒杀下单、抢红包等等业务场景,都需要用到分布式锁,我们项目中经常使用Redis 作为分布式锁。
选了 Redis 分布式锁✁几种实现方法,大家来讨论下,看有没有啥问题哈。
-
-
- 命令 setnx + expire 分开写
- setnx + value 值✁过期时间
- set ✁扩展命令(set ex px nx)
- set ex px nx + 校验唯一随机值,再删除
-
-
-
- 命令 setnx + expire 分开写
-
if(jedis.setnx(key,lock_value) == 1){ //加锁
expire(key,100); //设置过期时间
try {
do something //业务请求
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}
如果执行完 setnx 加锁,正要执行 expire 设置过期时间时,进程 crash 掉或者要重启维护了,那这个锁就“长生不老”了,别✁线程永远获取不到锁啦,所以 分布式锁不能这么实现。
-
-
- setnx + value 值✁过期时间
-
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置✁过期时间String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁✁过期时间
String currentValueStr = jedis.get(key);
// 如果获取到✁过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTime Millis()) {
// 锁已过期,获取上一个锁✁过期时间,并设置现在锁✁过期时间(不了解 redis ✁ getSet 命令✁ 小伙伴,可以去官网看下哈)
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发✁情况,只有一个线程✁设置值和当前值相同,它才可以加锁
return true;
}
//其他情况,均返回加锁失败return false;
}
笔者看过有开发小伙伴✁这么实现分布式锁✁,但✁这种方案也有这些缺点:
- 过期时间✁客户端自己生成✁,分布式环境下,每个客户端✁时间必须同步。
- 没有保存持有者✁唯一标识,可能被别✁客户端释放/解锁。
- 锁过期✁时候,并发多个客户端同时请求过来,都执行了 jedis.getSet(),最终只能有一个客户端加锁成功,但✁该客户端锁✁过期时间,可能被别✁客户端覆盖。
-
-
- : set ✁扩展命令(set ex px nx)(注意可能存在✁问题)
-
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}
这个方案可能存在这样✁问题:
- 锁过期释放了,业务还没执行完。
- 锁被别✁线程误删。
-
-
- set ex px nx + 校验唯一随机值,再删除
-
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
//判断✁不✁当前线程加✁锁,✁才释放
if (uni_request_id.equals(jedis.get(key))) {
jedis.del(key); //释放锁
}
}
}
在这里,判断当前线程加✁锁和释放锁✁不✁一个原子操作。如果调用
jedis.del()释放锁✁时候,可能这把锁已经不属于当前客户端,会解除他人加
✁锁。
一般也✁用 lua 脚本代替。lua 脚本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0 end;
这种方式比较不错了,一般情况下,已经可以使用这种实现方式。但✁存在锁过期释放了,业务还没执行完✁问题(实际上,估算个业务处理✁时间,一般没啥问题了)。
分布式锁可能存在锁过期释放,业务没执行完✁问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,✁否可以给获得锁✁ 线程,开启一个定时守护线程,每隔一段时间检查锁✁否还存在,存在则对锁
✁过期时间延长,防止锁过期提前释放。
当前开源框架Redisson 就解决了这个分布式锁问题。我们一起来看下
Redisson 底层原理✁怎样✁吧:
只要线程一加锁成功,就会启动一个 watch dog 看门狗,它✁一个后台线程, 会每隔 10 秒检查一下,如果线程 1 还持有锁,那么就会不断✁延长锁 key ✁ 生存时间。因此,Redisson 就✁使用 Redisson 解决了锁过期释放,业务没执行完问题。
Redis 一般都✁集群部署✁,假设数据在主从同步过程,主节点挂了,Redis
分布式锁可能会有哪些问题呢?一起来看些这个流程图:
如果线程一在 Redis ✁ master 节点上拿到了锁,但✁加锁✁ key 还没同步到slave 节点。恰好这时,master 节点发生故障,一个 slave 节点就会升级为master 节点。线程二就可以获取同个 key ✁锁啦,但线程一也已经拿到锁了, 锁✁安全性就没了。
为了解决这个问题,Redis 作者 antirez 提出一种高级✁分布式锁算法:
我们假设当前有 5 个Redis master 节点,在 5 台服务器上面运行这些 Redis
实例。
RedLock ✁实现步骤:如下
简化下步骤就✁:
-
-
- 按顺序向 5 个master 节点请求加锁
- 根据设置✁超时时间来判断,✁不✁要跳过该 master 节点。
- 如果大于等于三个节点加锁成功,并且使用✁时间小于锁✁有效期,即可认定加锁
-
成功啦。
-
-
- 如果获取锁失败,解锁!
-
-
-
- 跳跃表✁有序集合 zset ✁底层实现之一
- 跳跃表支持平均 O(logN),最坏 O(N)复杂度✁节点查找,还可以通过顺序性
-
操作批量处理节点。
-
-
- 跳跃表实现由 zskiplist 和zskiplistNode 两个结构组成,其中 zskiplist 用于 保存跳跃表信息(如表头节点、表尾节点、长度),而 zskiplistNode 则用于表示跳跃表节点。
- 跳跃表就✁在链表✁基础上,增加多级索引提升查找效率。
-
-
-
- 延时双删?
-
什么✁延时双删呢?流程图如下:
延时双删流程
-
-
-
- 先删除缓存
- 再更新数据库
- 休眠一会(比如 1 秒),再次删除缓存。
-
-
这种方案还算可以,只有休眠那一会(比如就那 1 秒),可能有脏数据,一般
业务也会接受✁。但✁如果第二次删除缓存失败呢?缓存和数据库✁数据还✁
可能不一致,对吧?给 Key 设置一个自然✁ expire 过期时间,让它自动过期
怎样?那业务要接受过期时间内,数据✁不一致咯?还✁有其他更佳方案呢?
-
-
- 删除缓存重试机制
-
因为延时双删可能会存在第二步✁删除缓存失败,导致✁数据不一致问题。可以使用这个方案优化:删除失败就多删除几次呀,保证删除缓存成功就可以了呀
删除缓存重试流程
-
-
-
- 写请求更新数据库
- 缓存因为某些原因,删除失败
- 把删除失败✁ key 放到消息队列
- 消费消息队列✁消息,获取要删除✁ key
- 重试删除缓存操作
-
-
-
-
- 读取biglog 异步删除缓存
-
重试删除缓存机制还可以吧,就✁会造成好多业务代码入侵。其实,还可以这
以 mysql 为例吧
- 可以使用阿里✁ canal 将binlog 日志采集发送到 MQ 队列里面
- 然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性
-
-
- Redis6.0 之前为什么一直不使用多线程?使用 Redis 时,几乎不存在 CPU 成为瓶颈✁情况, Redis 主要受限于内存和网络。例如在一个普通✁ Linux 系统上, Redis 通过使用 pipelining 每秒可以处理 100 万个请求,所以如果应用程序主要使用 O(N)或 O(log(N))✁命令,它几乎不会占用✎多 CPU。
-
redis 使用多线程并非✁完全摒弃单线程,redis 还✁使用单线程模型来处理客户端✁请求,只✁使用多线程来处理数据✁读写和协议解析,执行命令还✁使用单线程。
这样做✁目✁✁因为 redis ✁性能瓶颈在于网络 IO 而非 CPU,使用多线程能提升IO 读写✁效率,从而整体提高 redis ✁性能。
Redis 通过 MULTI、EXEC、WATCH 等一组命令集合,来实现事务机制。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中✁命令,其他客户端提交✁命令请求不会插入到事务执行命令序列中。
简言之,Redis 事务就✁顺序性、一次性、排他性✁执行一个队列中✁一系列命令。
Redis 执行事务✁流程如下:
-
-
- 开始事务(MULTI)
- 命令入队
- 执行事务(EXEC)、撤销事务(DISCARD )
-
命令 | 描述 |
EXEC | 执行所有事务块内✁命令 |
DISCARD | 取消事务,放弃执行事务块内✁所有命令 |
命令 | 描述 |
MULTI | 标记一个事务块✁开始 |
UNWATCH | 取✲ WATCH 命令对所有 key ✁监视。 |
WATCH | 监视key ,如果在事务执行之前,该 key 被其他命令所改动,那么事务将被打断。 |
Redis 作为一个 K-V ✁内存数据库,它使用用一张全局✁哈希来保存所有✁键值对。这张哈希表,有多个哈希桶组成,哈希桶中✁ entry 元素保存了 key 和value 指针,其中 key 指向了实际✁键,value 指向了实际✁值。
哈希表查找速率很快✁,有点类似于 Java 中✁ HashMap,它让我们在 O(1)
✁时间复杂度快速找到键值对。首先通过 key 计算哈希值,找到对应✁哈希桶
位置,然后定位到entry,在 entry 找到对应✁数据。什么✁哈希冲突?
Redis 为了解决哈希冲突,采用了链式哈希。链式哈希✁指同一个哈希桶中,
有些读者可能还会有疑问:哈希冲突链上✁元素只能通过指针逐一查找再操作。当往哈希表插入数据很多,冲突也会越多,冲突链表就会越长,那查询效率就 会降低了。
为了保持高效,Redis 会对哈希表做 rehash 操作,也就✁增加哈希桶,减少冲突。为了 rehash 更高效,Redis 还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。
可以✁,Redis 提供两个指令生成 RDB,分别✁ save 和bgsave。
-
-
- 如果✁ save 指令,会阻塞,因为✁主线程执行✁。
- 如果✁ bgsave 指令,✁ fork 一个子进程来写入 RDB 文件✁,快照持久化完全交
-
给子进程来处理,父进程则可以继续处理客户端✁请求。
RESP,英文全称✁ Redis Serialization Protocol,它✁专门为 redis 设计✁一套序列化协议. 这个协议其实在 redis ✁ 1.2 版本时就已经出现了,但✁到了redis2.0 才最终成为redis 通讯协议✁标准。
RESP 主要有实现简单、解析速度快、可读性好等优点。
应对缓存穿透问题,我们可以使用布隆过滤器。布隆过滤器✁什么呢?
布隆过滤器✁一种占用空间很小✁数据结构,它由一个很长✁二进制向量和一组Hash 映射函数组成,它用于检索一个元素✁否在一个集合中,空间效率和查询时间都比一般✁算法要好✁多,缺点✁有一定✁误识别率和删除困难。 布隆过滤器原理✁? 假设我们有个集合 A,A 中有 n 个元素。利用 k 个哈希散列函数,将 A 中✁每个元素映射到一个长度为 a 位✁数组 B 中✁不同位置上, 这些位置上✁二进制数均设置为 1。如果待检查✁元素,经过这 k 个哈希散列函数✁映射后,发现其 k 个位置上✁二进制数全部为 1,这个元素很可能属于集合 A,反之,一定不属于集合 A。
来看个简单例子吧,假设集合 A 有 3 个元素,分别为{d1,d2,d3}。有 1 个哈希函数,为 Hash1。现在将 A ✁每个元素映射到长度为 16 位数组B。
我们现在把 d1 映射过来,假设Hash1(d1)= 2,我们就把数组 B 中,下标
我们现在把 d2 也映射过来,假设Hash1(d2)= 5,我们把数组 B 中,下标
接着我们把 d3 也映射过来,假设Hash1(d3)也等于 2,它也✁把下标为 2
因此,我们要确认一个元素 dn ✁否在集合 A 里,我们只要算出 Hash1(dn) 得到✁索引下标,只要✁ 0,那就表示这个元素不在集合 A,如果索引下标✁
1 呢?那该元素可能✁ A 中✁某一个元素。因为你看,d1 和d3 得到✁下标值, 都可能✁ 1,还可能✁其他别✁数映射✁,布隆过滤器✁存在这个缺点✁:会存在 hash 碰撞导致✁假阳性,判断存在误差。
如何减少这种误差呢?
- 搞多几个哈希函数映射,降低哈希碰撞✁概率
- 同时增加 B 数组✁ bit 长度,可以增大 hash 函数生成✁数据✁范围,也可以降低哈希碰撞✁概率
我们又增加一个 Hash2 哈希映射函数,假设Hash2(d1)=6,Hash2(d3)=8,它俩不就不冲突了嘛,如下:
即使存在误差,我们可以发现,布隆过滤器并没有存放完整✁数据,它只✁运 用一系列哈希映射函数计算出位置,然后填充二进制向量。如果数量很大✁话, 布隆过滤器通过极少✁错误率,换取了存储空间✁极大节省,还✁挺划算✁。 目前布隆过滤器已经有相应实现✁开源类库啦,如 Google ✁ Guava 类库, Twitter ✁ Algebird 类库,信手拈来即可,或者基于 Redis 自带✁ Bitmaps 自行实现设计也✁可以✁。
2.21 Redis存在线程安全问题吗?为什么?
一个工作了5年的粉丝私信我。
他说自己准备了半年时间,想如蚂蚁金服,结果第一面就挂了,非常难过。
问题是:“Redis存在线程安全问题吗?”
好的,关于这个问题,我从两个方面来回答。
第一个,从Redis服务端层面。
Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。
虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。
为什么Redis没有采用多线程来执行指令,我认为有几个方面的原因。
Redis Server本身可能出现的性能瓶颈点无非就是网络IO、CPU、内存。但是CPU不是Redis的瓶颈点,所以没必要使用多线程来执行指令。
如果采用多线程,意味着对于redis的所有指令操作,都必须要考虑到线程安全问题,也就是说需要加锁来解决,这种方式带来的性能影响反而更大。
第二个,从Redis客户端层面。
虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。
假设两个redis client同时获取Redis Server上的key1,同时进行修改和写入,因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。
当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。
以上就是我对这个问题的理解。
面试点评
关于线程安全性问题,是一个非常重要,非常重要的知识。
虽然我们在实际开发中很少去主动使用线程,但是在项目中线程无处不在,比如Tomcat就是用多线程来处理请求的
如果对线程安全不了解,那么很容已出现各种生产事故和莫名其妙的问题。
这也是为什么大厂一定会问多线程并发的原因。
好的,本期的普通人VS高手面试系列的视频就到这里结束了。
我是Mic,一个工作了14年的Java程序员,咱们下期再见。
2.22 请描述Redis的缓存淘汰策略
这个问题我需要从三个方面来回答。
第一个方面:
当 Redis 使用的内存达到 maxmemory 参数配置的阈值的时候,Redis 就会根据
配置的内存淘汰策略。
把访问频率不高的 key 从内存中移除。
maxmemory 默认情况是当前服务器的最大内存。
第二个方面:
Redis 默认提供了 8 种缓存淘汰策略,这 8 种缓存淘汰策略总的来说,我认为可
以归类成五种
第一种, 采用 LRU 策略,就是把不经常使用的 key 淘汰掉。
第二种,采用 LFU 策略,它在 LRU 算法上做了优化,增加了数据访问次数,从
而确保淘汰的是非热点 key。
第三种,随机策略,也就是是随机删除一些 key。
第四种,ttl 策略,从设置了过期时间的 key 里面,挑选出过期时间最近的 key
进行优先淘汰
第五种,当内存不够的时候,直接报错,这是默认的策略。
这些策略可以在 redis.conf 文件中手动配置和修改,我们可以根据缓存的类型和
缓存使用的场景来选择合适的淘汰策略。
最后一个方面,我们在使用缓存的时候,建议是增加这些缓存的过期时间。
因为我们知道这些缓存大概的生命周期,从而更好的利用内存。
以上就是我对这个问题的理解。
总结
Redis 是一个内存数据库,而内存又是非常宝贵的资源。
如何用有限的服务器资源来支撑更多业务,就必须要考虑到缓存的淘汰算法
把一些不怎么使用缓存淘汰掉。
因此,我认为这个面试题的考察方向也很好,建议大家深度学习一下。
2.23 Redis多线程模型怎么理解,那它会有线程安全问题吗?
hi,大家好,我是Mic,一个没有才华只能靠颜值混饭吃的Java程序员。
昨天,一个工作了7年的粉丝私信我这样一个问题。
他说Redis6.0已经支持多线程了,那是不是会存在线程安全问题,如果有线程安全问题,它是怎么解决的。
这个问题说简单也简单,说难也挺难的,毕竟不仅仅只是涉及到多线程的问题,还设计到NIO里面的Reactor模型问题。
关于:“Redis多线程模型怎么理解,那它会有线程安全问题吗?”这个问题。
首先,Redis在6.0支持的多线程,并不是说指令操作的多线程,而是针对网络IO的多线程支持。
也就是Redis的命令操作,仍然是线程安全的。
其次, Redis本身的性能瓶颈,取决于三个纬度,网络、CPU、内存。
而真正影响内存的关键问题是像内存和网络。
而Redis6.0的多线程,本质上解决网络IO的处理效率问题。
在Redis6.0之前。Redis Server端处理接受到客户端请求的时候,Socket连接建立到指令的读取、解析、执行、写回都是由一个线程来处理,这种方式,在客户端请求比较多的情况下,单个线程的网络处理效率太慢,导致客户端的请求处理效率较低。
于是在Redis6.0里面,针对网络IO的处理方式改成了多线程,通过多线程并行的方式提升了网络IO的处理效率。
但是对于客户端指令的执行过程,还是使用单线程方式来执行。
最后,Redis6.0里面多线程默认是关闭的,需要在redis.conf文件里面修改io-threads-do-reads配置才能开启。
另外,之所以指令执行不使用多线程,我认为有两个方面的原因。
内存的IO操作,本身不存在性能瓶颈,Redis在数据结构上已经做了非常多的优化。
如果指令的执行使用多线程,那Redis为了解决线程安全问题,需要对数据操作增加锁的同步,不仅仅增加了复杂度,还会影响性能,代价太大不合算。
面试点评
其实,在Redis6.0之前,就已经有用到多线程了,比如数据持久化、集群数据同步等。
只是可能这些机制离我们应用开发比较远,没有过多关注。
另外还要注意,Redis本身虽然是线程安全的,但是应用程序对于Redis的Ready-modify -write操作。
仍然是非线程安全的。
掌握这些基础,可以有效避免开发过程中写出一下自己都不懂的bug。
大家记得点赞、收藏加关注。
2.24 Redis主从复制的原理
一个工作了5年的粉丝,在面试的时候遇到“Redis主从复制的原理”这个问题。
他回答这个问题的时候断断续续的,没有一个比较清晰的逻辑性,导致面试没发挥好。
今天这个视频,给大家分享一下遇到这类问题,正确的回答方法。
Hi,大家好,我是Mic,咕泡科技联合创始人
下面来分析一下面试官的考察意图。
2.25 Redis中AOF 重写的过程描述一下
Hi,大家好,我是Mic
一个工作了5年的粉丝私信我。他说他在简历里面写精通Redis,
结果面试官一直围绕Redis来问,后来越问越深,面试官后面回了一句话:“也没有想象中那么深嘛”
今天给大家分享一道年薪50W的面试题,“请描述一下Redis里面AOF的重写过程”
2.26.Redis哨兵机制和集群有什么区别?
“Redis哨兵机制和集群有什么区别?”
这是一个工作了2年的同学面试的时候遇到的问题,如果你不知道怎么回答这个问题,记得认真看完这篇文章。
Hi,大家好,我是Mic,咕泡科技联合创始人
下面来分析这个问题的考察目的
考察目的
这个问题考察难度并不大,工作3年以上都会遇到,只是不同的工作年限对于面试的深度会有差异。
考察这个问题的目的无非就是看看求职者是否了解Redis的集群,以及是否有自己去搭建过Redis集群。
对于工作2年的同学来说,回答不需要太深入,但是5年以上的同学,会以这个问题作为切入口去深度考察求职者
对Redis集群和哨兵机制的底层原理。
问题分析
Redis集群有几种实现方式,一个是主从集群、一个是Redis Cluster。
主从集群,就是在Redis集中包括一个Master节点和多个Slave节点。
Master负责数据的读写,Slave节点负责数据的读取。
Master上收到的数据变更,会同步到Slave节点上实现数据的同步。
通过这种架构实现可以Redis的读写分离,提升数据的查询性能。
Redis主从集群不提供容错和恢复功能,一旦Master节点挂了,不会自动选出新的Master,导致后续客户端所有写请求直接失败。
所以Redis提供了哨兵机制,专门用来监听Redis主从集群提供故障的自动处理能力。
哨兵会监控Redis主从节点的状态,当Master节点出现故障,会自动从剩余的Slave节点中选一个新的Master。
哨兵模式下虽然解决了Master选举的问题,但是在线扩容的问题还是没有解决。
于是就有了第三种集群方式,Redis Cluster,它实现了Redis的分布式存储,也就是每个节点存储不同的数据实现数据的分片。
在Redis Cluster中,引入了Slot槽来实现数据分片,Slot的整体取值范围是0~16383,每个节点会分配一个Slot区间当我们存取Key的时候,Redis根据key计算得到一个Slot的值,然后找到对应的节点进行数据的读写。
在高可用方面,Redis Cluster引入了主从复制模式, 一个Master节点对应一个或多个Slave节点,当Master出现故障,会从Slave节点中选举一个新的Master继续提供服务。
Redis Cluster虽然解决了在线扩容以及故障转移的能力,但也同样有缺点,比如
客户端的实现会更加复杂
Slave节点只是一个冷备节点,不提供分担读操作的压力
对于Redis里面的批量操作指令会有限制
因此主从模式和Cluster模式各有优缺点,在使用的时候需要根据场景需求来选择。
高手
因为Redis集群有两种,一种是主从复制,一种是Redis Cluster,我不清楚您问的是哪一种。
按照我的理解,我认为您可能说的是Redis哨兵集群和Redis Cluster的区别。
对于这个问题,我认为可以从3个方面来回答
Redis哨兵集群是基于主从复制来实现的,所以它可以实现读写分离,分担Redis读操作的压力
而Redis Cluster 集群的Slave节点只是实现冷备机制,它只有在Master宕机之后才会工作。
Redis哨兵集群无法在线扩容,所以它的并发压力受限于单个服务器的资源配置。
Redis Cluster提供了基于Slot槽的数据分片机制,可以实现在线扩容提升写数据的性能
从集群架构上来说,Redis 哨兵集群是一主多从, 而Redis Cluster是多主多从
总结
遇到这类的问题,要学会用结构化的思维去整理自己的思路
然后再根据提炼的结构去回答,既能够表达清晰,又有逻辑性。
好的,今天就到这里结束了,喜欢这个作品的小伙伴记得点赞收藏加关注
我是Mic,咱们下期再见。
2.27 Redis的理解
“谈谈你对Redis的理解”!
面试的时候遇到这类比较宽泛的问题,是不是很抓狂?
是不是不知道从何开始说起?
没关系,今天我用3分钟教你怎么回答。
大家好,我是Mic,一个工作了14年的Java程序员。
这个问题面试官考察的目的是什么?希望得到什么样的回答?
考察目标
对于某某技术的理解这一类问题,它是一种比较宽泛的问题
在面试过程中,考察这类问题有两个很重要的目的:
在面试的过程中,面试官希望求职者能多说一些东西,从而更好的对你的整体情况和能力有一个清晰的判断,因此这类问题,可以找到一些了解你的突破口。
这种问题其实没有标准答案,更多的是基于你对它的理解的一个总结
这反而能够更好的考察你的技术积累和逻辑表达能力。
所以,求职者在回答的过程中,需要尽可能逻辑清晰,简单明了的表述出来。
否则很难得到认可。
问题解析
关于Redis是什么,想必大部分人都能脱口而出。
它是一个分布式缓存中间件?可这样回答有问题吗?当然有
准确来说,Redis是一个基于内存实现的Key-Value数据结构的Nosql数据库。
注意,这里有三个关键点。
内存存储
key-value结构
Nosql
所谓内存存储,是指所有数据是存储在内存里面,数据的IO性能比较高。
当然,Redis也提供了持久化策略来避免内存数据丢失的问题
key-value结构表示数据的存储方式,除了redis以外,还有像LevelDB、Scalaris等。
而Nosql,它指的是一种非关系型数据库,相比于传统的关系型数据库而言。
更多的考虑到扩展性、性能、大数据量的存储等,弥补了关系型数据库的短板
像列式存储ClickHouse、Cassandra; 文档存储MongoDB图形存储Neo4J等都是属于Nosql范畴。
高手
Redis是一个基于Key-Value存储结构的Nosql开源内存数据库。
它提供了5种常用的数据类型,String、Map、Set、ZSet、List。
针对不同的结构,可以解决不同场景的问题。
因此它可以覆盖应用开发中大部分的业务场景,比如top10问题、好友关注列表、热点话题等。
其次,由于Redis是基于内存存储,并且在数据结构上做了大量的优化所以IO性能比较好,在实际开发中,会把它作为应用与数据库之间的一个分布式缓存组件。
并且它又是一个非关系型数据的存储,不存在表之间的关联查询问题,所以它可以很好的提升应用程序的数据IO效率。
最后,作为企业级开发来说,它又提供了主从复制+哨兵、以及集群方式实现高可用在Redis集群里面,通过hash槽的方式实现了数据分片,进一步提升了性能。
总结
好的,屏幕前的你,学废了吗?
如果你喜欢我的作品,记得点赞收藏加关注