当前版本5.0 稳定版,项目使用 5.0 C语言写的
问题 | 答案 |
---|---|
Redis 基本数据结构
参考: https://mp.weixin.qq.com/s/gRtiSNDCuS0c8nF_Q8Tv9A https://mp.weixin.qq.com/s/TR8oe7c1SlOrk78untXdOA |
string :动态字符串,是可以修改的字符串,类似于 Java 的 ArrayList,分配冗余空间的方式来减少内存的频繁分配,当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
长度大于32字节,对象的编码设置为 raw,动态字符串,两次内存分配 长度小于32字节,对象将使用embstr 编码,只读不能修改,一次内存分配 都使用 redisObject 结构和 sdshdr 结构来保存字符串 |
list | list : 相当于 Java 语言里面的 LinkedList,注意它是双向链表而不是数组。插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。Redis 的列表结构常用来做异步队列使用。 列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist(快速列表)是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
列表对象保存的所有字符串元素的长度都小于 64 字节;列表对象保存的元素数量数量小于 512 个;使用 ziplist 编码。 |
hash | hash : 相当于 Java 语言里面的 HashMap,它是无序字典。Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。Redis 的字典默认的 hash 函数是 siphash。
扩容条件:正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容。
渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当搬迁完成了,就会使用新的hash结构取而代之。当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。
缩容 Redis的hash结构不但有扩容还有缩容,从这一点出发,它要比Java的HashMap要厉害一些,Java的HashMap只有扩容。缩容的原理和扩容是一致的,只不过新的数组大小要比旧数组小一倍。缩容的条件是元素个数低于数组长度的 10%。缩容不会考虑 Redis 是否正在做 bgsave。
保存的所有键值对的键和值的字符串长度都小于64字节;保存的键值对数量小于512个;使用 ziplist 编码。 |
set | set : 相当于 Java 语言里面的 HashSet,字典中所有的 value 都是一个值NULL,有去重功能 |
zset | zset (有序列表): 它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫做「跳跃列表」的数据结构。 |
位图、HyperLogLog、布隆过滤器 | 位图: 普通的字符串,也就是 byte 数组,setbit getbit bitcount bitpos
HyperLogLog : 用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存, 就可以计算接近 2^64 个不同元素的基数。 应用:统计网站每个网页每天的 UV 数据 相关命令:pfadd 和 pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfmerge,用于将多个 pf 计数值累加在一起形成一个新的 pf 值。 问题:pf 的内存占用为什么是 12k? 算法中使用了 1024 个桶进行独立计数,不过在 Redis 的 HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是2^14 * 6 / 8 = 12k字节。
布隆过滤器:大型的位数组和几个不一样的无偏 hash 函数 布隆过滤器可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某个对象是否存在时,它可能会误判。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。 应用:新闻客户端推荐系统实现推送去重 相关命令:bf.add 添加元素,bf.exists 查询元素是否存在。如果想要一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需要用到 bf.mexists 指令。 |
如何从海量的 key 中找出满足特定前缀的 key 列表来? | keys:用来列出所有满足特定正则字符串规则的 key 缺点: 没有 offset、limit 参数;遍历算法复杂度O(n) 导致Redis服务卡顿
scan:复杂度O(n),通过游标分步进行,不会阻塞线程;返回游标为零即为结束;返回结果可能会有重复,limit 只是说扫描的行数非返回结果的行数 |
redis 与 memcache 选择 | 支持数据结构: redis支持复杂数据结构,memcache只支持简单数据结构 支持集群模式:redis天然支持 cluster 模式,memcache依靠客户端实现 持久化:redis支持数据持久化到磁盘,memcache把数据全都存在内存中 线程模型:Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的非阻塞IO复用模型 |
为啥 redis 单线程模型也能效率这么高? |
|
Redis 过期策略 | 过期的 key 集合: redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。 定时扫描策略:Redis 默认会每隔100ms过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。 注:如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。 从库的过期策略:从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。会出现主从数据的不一致。 惰性策略:所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除 |
Redis 内存淘汰机制 问题:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整? |
maxmemory(最大使用内存): 超出即需内存淘汰 maxmemory-policy:内存淘汰策略 noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。 volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。 volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。 volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。 allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。 allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。
LRU 算法 : 双向链表 + 字典 实现 近似 LRU 算法(redis采用):给每个 key 增加了一个额外的小字段,这个字段的长度是 24 个 bit,也就是最后一次被访问的时间戳。它的处理方式只有懒惰处理。当 Redis 执行写操作时,发现内存超出 maxmemory,就会执行一次 LRU 淘汰算法。这个算法也很简单,就是随机采样出 5(可以配置) 个 key,然后淘汰掉最旧的 key,如果淘汰后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。maxmemory_samples :每次采样的个数,默认是5. |
Redis 持久化有哪几种方式 | RDB: 一次全量备份,快照是内存数据的二进制序列化形式,在存储上非常紧凑,调用 glibc 的函数fork产生一个子进程,采用COW(Copy On Write) 机制来实现快照持久化,可以让 redis 保持高性能。 开启命令 :save m n (表示m秒内数据集存在n次修改时) 手动触发: save 阻塞当前Redis服务器 bgsave 异步进行快照操作
AOF: 内存数据修改的指令记录文本,快照文件更大。收到客户端修改指令后,先存到AOF日志(磁盘),然后再执行指令。每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。 AOF 重写(fork子进程)条件: 调用bgRewriteAof 指令、大于最小的重写值及大于指定增量 通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行 |
Redis 主从架构,主从复制(异步)
主负责写,从节点负责读 主从架构 -> 读写分离 -> 水平扩容支撑读高并发 心跳 slave node 每隔 1秒 发送一个 heartbeat 参考: https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-master-slave.md |
主从复制原理: 全量复制: master生成RDB文件发送给slave(默认60s完成),之后新增数据写命令缓存在内存中(默认大小256M) slave清空旧数据加载RDB
增量复制: master 直接从自己的 backlog 中获取部分丢失的数据,发送给 slave node,默认 backlog 就是 1MB slave 发送的 psync 中的 offset 来从 backlog 中获取数据的 |
Redis 哨兵实现高可用 Redis Sentinel 集群看成是一个 ZooKeeper 集群 |
主要功能:主节点存活检测、主从运行情况检测、自动故障转移、主从切换。 主观下线:就一个哨兵如果自己觉得一个 master 宕机了,那么就是主观宕机 客观下线:如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就是客观宕机 Sentinel通过 cmd 连接给 Redis 发送命令,通过 pub/sub 连接到 Redis 实例上的其他 Sentinel 实例。(每秒一次的频率)
故障转移:选点的依据依次是:网络连接正常->5秒内回复过INFO命令->10*down-after-milliseconds内与主连接过的->从服务器优先级->复制偏移量->运行id较小的 哨兵机制是有缺点的: 1.主从服务器的数据要经常进行主从复制,这样造成性能下降。 2.主从切换过程,服务不可用 |
Redis Cluster (稳定版本5.0.4 3.0之后支持集群) 遇到 单机内存、并发、流量 等瓶颈时,可以采用 Cluster 架构方案达到 负载均衡 的目的。 端口号:6379 节点间通信端口号:16379 |
Redis Cluster 是去中心化的,采用 gossip 协议维护集群元数据。gossip 协议包含多种消息,包含 ping,pong,meet,fail 等等。
定槽位:集群有固定的16384 (2^14)的 slots,slot位置 CRC16(KEY)%16384 ,或者key中嵌入tag指定slot。客户端连接集群时会得到一份集群slot信息,当客户端要查找某个 key 时,可以直接定位到目标节点。
槽位迁移:Redis迁移的单位是槽,Redis 一个槽一个槽进行迁移。槽在原节点的状态为migrating,在目标节点的状态为importing。从源节点获取内容 => 存到目标节点 => 从源节点删除内容。迁移过程是同步的,节点的主线程会处于阻塞状态,直到key被成功删除。
槽位感知: moved 是用来纠正槽位的(客户端访问了错误的节点,刷新槽位关系表);asking 用来临时纠正槽位(客户端访问了正在迁移的节点,不刷新槽位关系表)
redis cluster 高可用:判断节点宕机(主观宕机、客观宕机)、从节点过滤、从节点选举 功能限制:key分布在不同节点时,批量操作及事务不支持;不能将一个很大的键值对映射到不同的节点; |
Redis 实现分布式锁 分布式锁在分布式环境下,锁定全局唯一公共资源,表现为:
分布式锁的目的如下:
1. 防止用户重复下单 共享资源进行上锁的对象 : 【用户id】 Redlock: https://blog.csdn.net/chen_kkw/article/details/81276068 |
实现1:占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。 问题:如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
实现2:我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。 问题:setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。
实现3:Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象。 问题:超时问题。Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。 方案:set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key
Redlock:顺序地在 所有实例上申请锁,使用相同的 key 和 random value,一半以上获取到锁 申请成功;申请失败释放锁; 问题:其中一个redis重启(同步持久化或者启动后的Redis一段时间不可用待锁过期) |
分布式锁问题 1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; 参考: https://blog.csdn.net/wuzhiwei549/article/details/80692278 |
基于数据库实现排他锁: 数据库表字段: 自增id、锁方法名method_nam(唯一键)、锁状态state(1分配 2未分配)、更新时间update_time、版本号version 获取锁:插入一行 成功 释放锁:删除一行 问题及解决方案: 性能问题数据库单点(主从);解锁失败无失效时间(定时任务清理);插入失败即报错非阻塞(加个while循环);非重入(表中加机器及线程信息)
基于redis实现: SET resource_name my_random_value NX PX 30000 问题及解决方案:锁时间不可控超时(释放时检测唯一key;锁续租,循环延长到期时间);主从同步不及时问题,redis AP模型(Redlock 算法来保证)
基于 Zookeeper: ZK 和客户端的心跳进行维持 创建临时有序节点,开启事件监听 问题及解决方案:客户端挂了(直接释放锁);业务线程死循环一直持有锁不放(业务方处理或者监控锁异常);业务方GC导致心跳断开(GC完之后检查所状态,重试) |
Redis 实现消息队列 和 延时队列 | 消息队列 :list(列表) 数据结构常用来作为异步消息队列使用,用blpop/brpop替代前面的lpop/rpop,阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。
延时队列:可以通过 Redis 的 zset(有序列表) 来实现。我们将消息序列化成一个字符串作为 zset 的value,这个消息的到期处理时间作为score,然后用多个线程轮询 zset 获取到期的任务进行处理 |
Redis 管道、事务 | 管道:客户端通过改变了读写的顺序带来的性能的巨大提升,
事务:multi(开始)/exec(提交)/discard(丢弃) redis 不支持事务回滚 所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为 Redis 的单线程特性,它不用担心自己在执行队列的时候被其它指令打搅,可以保证他们能得到的「原子性」执行。 discard 指令,用于丢弃事务缓存队列中的所有指令,在 exec 执行之前。 注:事务在遇到指令执行失败后,后面的指令还继续执行。
watch机制:它就是一种乐观锁,watch 会在事务开始之前盯住 1 个或多个关键变量,当事务执行时,也就是服务器收到了 exec 指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后,是否被修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec 指令就会返回 null 回复告知客户端事务执行失败,这个时候客户端一般会选择重试。 |
guava cache | 加载:LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。
回收: 基于容量的回收(超过设定值,尝试回收最近没有使用或总体上很少使用的缓存项); 定时回收:
基于引用的回收
显式清除:个别清除、批量清除、清除所有缓存项 清理什么时候发生? 不会自动清理,访问的时候判断;人工清理 刷新:异步刷新,提供读 |
Redis & Tair
Tair: https://www.oschina.net/p/tair |
Redis 适用 需要使用复杂数据结构(map, set),map/set中元素很多(1000以上) 延迟敏感服务 不适用 数据量超过600GB(数据太多,全内存太浪费资源) 需要多语言客户端支持
Tair 适用 不能容忍数据丢失 ,数据量大,内存放不下的服务 不适用 使用复杂数据结构(map/set),map/set中元素很多(1000以上) |