0%

[toc]


redisObject对象#

redis每一个key(键) ,本质上都对应了一个redisObject对象。
但是键对应的数据结构有很多,做的操作也不同,所以要先以redisObject作为入口和关联点,再去做操作
3f2f395c3f0f7e0adc26c461f1b47846b61abf52


a374de100b37f8fd036ff7dfae7da6a4ff2e5fc7
对象类型、编码类型的对应关系在前面那张图里有解释。


Q: redis从命令调用到触发执行,经历了什么?#

A:
803dc234f7de4819af505a6a705429dd34e8da3a
以lpop为例
3c79d88d49559edc82bd3abe1d2d65bacdfb6c3f


Q: redis的共享机制有了解吗?#

A:
有一些类似常量、或者通用的值,会放到共享对象中。
比如OK、ERROR、 状态码等,即每次不会重新生成这些对象。
只支持字符串对象(整数也属于字符串)


Q: 为什么共享对象不支持字符串以外的那些列表、哈希对象等?#

A:
对复杂度较高的对象创建共享对象, 需要消耗很多的CPU
而为了验证内容是否改变或者匹配, 消耗的量也很大,不如就不共享。


Q: redis对象什么时候被回收?#

A:
redisObject有个引用计数。 但删除了某个key, 则这个key对应redis对象的引用计数-1。
当通过删除操作减为0的时候, 释放这个键的内存空间。


SDS 动态字符串(simple dynmic string)#

实际结构如下
fd702c8bb3c049afdacc62293d6f49921a486bec
注意,len并不是包含头部+buf+\0的。
他只是记录用了多少字符。
而实际上的buf可能比你用的要多。


Q:经典,SDS相比直接用C语言的字符串char*有什么优势?#

A:

  • 可以根据len马上获取字符串长度。而C语言需要遍历直到遇到\0才能得到长度
  • 杜绝缓冲区溢出。当涉及字符串拼接式,可以提前计算长度是否够用,再扩容
  • 可以进行预分配,当字符串要扩容时,提前分配一倍空间,避免频繁扩容
  • 二进制安全,即不用担心字符串中间自带的\0问题(图片流、序列化流之类的可能会有)

Q: 既然有长度len了,为什么末尾还要保留\0?#

A:
这样可以直接重用C语言库<string.h>中的一些函数


Q: 怎么计算未使用的空间?#

A:
alloc - len, 就是未使用的buf空间


Q: 什么时候, buf空间会比实际用的多?#

A:
只有当字符串真正需要增长的时候,且空间不足时,才会增加一倍。
这个就是空间预分配。
注意刚开始建立一个字符串时,是不会腾多余空间的。


Q:这个增长后多出来的预分配空间后面会被优化掉?#

A:
不会被释放,除非:

  1. 键被删除
  2. redis重启,通过持久化载入字符串不会做预分配。

如果真的频繁增长,则需要修改redis服务代码,增加定时释放机制。


Q: 字符串如果做了缩短,多出来的空间也不会释放吗?#

A:
会释放,但是需要执行一些api例如sdsfree进行手动释放。
适用于你这个redis会频繁做字符串缩短的场景。


ziplist 压缩列表#

  • 一句话解释: ziplist这个列表中,每个元素的大小不是固定的, 每个元素都有个头部,确认这个元素的大小,因此这个ziplist非常紧致。

    存储结构如下:

    3c72452741a16801ceb312d2e8ff95fd14d3e462


Q: ziplist的最后有个zlend终止符0xff, entry中的数据有没有可能是0xff,导致冲突?#

A:
ziplist保证任何情况下, 一个entry的首字节(entry头字节)都不会是255。

存储int时:
|11000000| encoding为3个字节,后2个字节表示一个int16;
|11010000| encoding为5个字节,后4个字节表示一个int32;
|11100000| encoding 为9个字节,后8字节表示一个int64;
|11110000| encoding为4个字节,后3个字节表示一个有符号整型;
|11111110| encoding为2字节,后1个字节表示一个有符号整型;
|1111xxxx| encoding长度就只有1个字节,xxxx表示一个0 - 12的整数值;
|11111111| 还记得zlend么?


Q: 为什么ziplist这样设计很省内存?#

A:
用了encoding来细化存储大小, 这样可以避免很多的冗余空间。


Q: ziplist做新增时,会像arryalist那样预留空间吗?#

A:
为了保持紧致,不会预留,每次写都是做分配内存的操作
删除的话也是立刻缩容。


Q: 什么是ziplist的链式反应?#

A:
头部加了一个超级大的节点
后面节点的prelen字段变长, 于是整个entry变长
于是再导致后面节点的prelen变长,以此类推。
影响就是时间会比较慢,总是要依次分配。
但是触发概率比较低, 不会那么巧全部都在prelen增长的边缘


quicklist快表#

也就是 很多个ziplist组成的list
fbc43b472ab1370f35f023cce85dba8e9a6c1490
原先ziplist里,每个entry只存存数。
而现在的entry变成了一个个可以动态扩展的ziplist。

Q: 快表和压缩表的区别?#

A:

  • 压缩链表不适合中间写, 因此每次都需要重新分配内存。而快表宏观上是一个双向链表,进行插入时很方便。
  • 快表可以用二分查找定位数据

quicklist源码解读


字典/哈希表 - Dict#

就是简单的哈希表 + 链表解决冲突的实现结构。
f09e457c6a9d4ccab072c4958e0b6aff88b55004
扩容是就是rehash重新哈希进行扩展或者收缩。
扩容和收缩:当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:
如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used*2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。
相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。


Q: 如果哈希表的数据量很大, 扩容时会怎么做?#

A:
使用渐进rehash的方式。
扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的

  • 字典的删除查找更新等操作可能会在两个哈希表上进行

  • 第一个哈希表没有找到,就会去第二个哈希表上进行查找。

  • 但是进行 增加操作,一定是在新的哈希表上进行的

    目的是防止扩容过程阻塞哈希的响应,因此扩容是异步完成的机制

intSet整数集合#

intSet是set的底层实现。
当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。使用遍历实现集合的操作。
实现很简单
编码(决定整数是16/32/64位)——个数——存放整数
659d4ccfbc6169c3d352904f4fe911deda8dfd03
为了方便二分查找,各个项在数组中的值得大小从小到大有序排,且不包含任何重复项


Q: 原先是16位的编码, 如果此时插入了1个64位的整数怎么办?#

A:

  1. 根据新元素的类型(比如int32),扩展整数集合底层数组的空间大小,并为新元素分配空间。
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上
  3. 在放置元素的过程中, 需要继续维持底层数组的有序性质不变
  4. 最后改变encoding的值,length+1。

ZSikpList跳表#

skiplist实现原理

  • 插入时,随机一个层数,然后找到那一层的对应位置插入,并更新前后节点

  • get时,先从最高层找,比要找的大时,再去下一层。

  • 随机时不是完全随机,会根据当前分布情况,修改概率p,并设置某一层的最大限制,防止比别的层过多。

    b148147baa7475ed77e0525aa6fb624c7f822861


Q: 跳表和平衡树的比较?#

A:

  • 范围查找时,跳表比平衡树简单,找到最小点时直接遍历即可找到最大点的位置即可。 平衡树可能还要重新搜。
  • 增删节点时, 平衡树还要调整树,但是跳表只需要修改前后两个节点。
  • 跳表平均每个节点存1.33个指针, 而平衡树要一定存2个指针。
  • 时间复杂度, 跳表和平衡树没有区别,都是O(logn).

底层结构和redis数据结构对象关系#

字符串string底层结构#

f1bc579ea85ccb7493f162e9acc242906b3d551e
可以看到字符串有三种编码:int、 embstr(小于44的短字符串)、raw(大于44的长字符串)


Q: embstr和raw的底层区别是什么?#

A:

  • embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。
  • 因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。
  • embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间
  • 因此redis中的embstr实现为只读。

Q: 如果int的大小溢出long了,怎么办?#

A:
会自动转换编码为raw。


列表list底层结构#

7f13eca2de5a7046c82c9c25b53235fec4da5d3f
可以看到新版本的redis, 列表只有一种实现方式——quicklist。
以前有OBJ_ENCODING_ZIPMAP与OBJ_ENCODING_LINKEDLIST两种编码,现在都被废弃了。


哈希表hash底层结构#

164eb020bd8c70f6cb695f479067260c951ee4c2
可以hash表的底层是 ziplist或者dict
dict好理解, 为什么ziplist可以作为haxi表呢?
因为ziplist被hash表限制在了数量比较少的场景, 这种场景下直接遍历是不会耗时很多的。
aeabbe3b03d258faa3891cd1519b890e831fbbd8
使用ziplist作为底层hash结构的条件:

  1. 列表保存元素个数小于512个
  2. 每个元素长度小于64字节

Q: ziplist怎么解决哈希冲突?#

A: 元素太少了, 不用解决, 没有哈希过程, 直接遍历。 如果有相同的key,直接替换他的下一个entry即可。

集合对象底层结构#

ef7b67ef44114b3ca380b467840de99a21fce23f
集合是无序且不支持重复的。
使用dict和intset两种编码。
使用dict时, 只有dict的键部分, 值部分的链表不会允许展开,因为不允许重复。

intset的条件:#

  1. 都是整数
  2. 数量小于512

有序集合 sortedSet底层结构#

056c5fd86e054f81735172f916350716fe684dc9
使用ziplist或者(skiplist+dict)实现。
ziplist仍然是数量很少的时候使用,时间消耗没那么高
skip+dict比较复杂,如下图所示:
4e33d050961d676cb2fdb6ea2a2bea5a9cf8eac5


Q: 为什么不能单独使用skiplist或者单独使用dict实现?#

A:

  • 假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;
  • 假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。

[toc]


Q: redis事务有哪些命令?#

A:

  • MULTI: 开启事务。后面执行的操作都会放进队列里而不执行。
  • EXEC: 执行事务中的所有操作命令
  • DISCARD: 取消事务,放弃事务队列里的所有命令
  • WATCH:监视一个或多个key,如果事务在“MULTI后, EXEC前”,被修改了,则事务中断

Q: redis事务出现错误时,会怎么处理?回滚还是抛弃?#

A:
有2种情况

  1. 语法错误, 例如MULTI之后, 输入了一个不存在的命令"sets", 则事务终止, 后面再EXEC将不会有变化。
  2. 运行期错误。语法正确,但类型错误,类型错误需要等真正运行时才知道。
    这时候事务不会回滚,而是跳过错误命令,继续执行

Q: redis怎么实现CAS乐观锁?#

A:
使用watch命令。
就是MULTI之后, 针对某个键执行WATCH, 当后面真正需要EXEC时,如果key变了,就会失败。
此时的redis程序需要重复MULTI WATCH EXEC的操作,直到真正执行成功


Q: 为什么redis不支持回滚?为什么mysql这类关系型数据库需要回滚?#

A:

  • 失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

对于关系型数据库,他们是日志先行,可靠性大于效率的。
只有先做到了日志线性,才能保证正常回滚,而这肯定是有性能损耗,且实现复杂度非常高的。


Q: redis满足事务ACID的哪几个属性?#

A:

  • 原子性:Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。 不支持原子回滚
  • 一致性: redis不具备 ACID 中一致性的概念
  • 隔离性: 事务之间不影响,因为是单线程
  • 持久性: 通过一定策略可以保证持久性,但不保证

[toc]

Q: redis的并发是怎么样的?#

A:
https://www.cnblogs.com/wanlei/p/10464517.html
redis是单线程进程, 所有操作都在1个线程内执行, 因为大部分是内存k-v操作,所以不需要太多的CPU消耗,无需多线程。
但是支持IO多路复用。epoll。


Q: 详细讲讲redis怎么利用IO多路复用实现百万级并发的?#

A:
每个网络连接本质上都是文件, 涉及IO。
当客户端与redis服务建立连接, 输入到输出都需要时间, 此时如果连接很多,会不会影响其他的连接请求?
因此为了能快速响应redis请求,以及读取数据并返回, 使用了IO多路复用epoll来响应。 即我虽然和你建立了reids连接,但我等待你发数据了我才真正处理,而不是阻塞等待。
关于epoll的原理解析, 见高并发IO之epoll


Q:redis如何处理锁?#

A:
主要使用Redis Setnx 命令
在指定的 key 不存在时,为 key 设置指定的值
设置成功,返回 1 。 设置失败,返回 0
详情


Q: 讲一下redis的主从复制策略?#

A:
redis 主从复制链接
Redis主从同步策略
主从刚刚连接的时候,进行全量同步;
全同步结束后,进行增量同步。
当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。


Q: 讲一下Redis有哪些缓存淘汰策略、更新策略#

A:
缓存有多种策略可以配置选择
经典的有LRU、默认策略、 最短超期等六种
过期: 惰性+定时
惰性:使用的时候都先检查一下是否超期
定时:定期扫描,随机查X个键,如果发现过期比例超过1/4,继续扫+清, 最大占25ms。 他这类似于分多次小批扫描
详情


Q: 讲一下redis的七种集合类型?#

A:
Redis中7种集合类型应用场景

  • Strings——简单的key-value

    Strings 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。使用Strings类型,你可以完全实现目前 Memcached 的功能,并且效率更高。还可以享受Redis的定时持久化,操作日志及 Replication等功能。可提供与 Memcached 一样的get、set、incr、decr 等操作

  • Hashs——用于结构体或者对象

    在Memcached中,我们经常将一些结构化的信息打包成hashmap,在客户端序列化后存储为一个字符串的值,比如用户的昵称、年龄、性别、积分等,这时候在需要修改其中某一项时,通常需要将所有值取出反序列化后,修改某一项的值,再序列化存储回去。这样不仅增大了开销,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。

    每次存入一个hash对象, 接着存入键值对 key-value

    即存入一个

    lishaoxiao

    lishaoxiao下面有很多个键值对

    name lishaoxiao

    age 20

  • Lists

    Lists 就是链表,相信略有数据结构知识的人都应该能理解其结构。使用Lists结构,我们可以轻松地实现最新消息排行等功能。Lists的另一个应用就是消息队列,可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作Lists中某一段的api,你可以直接查询,删除Lists中某一段的元素。

  • Sets

    Sets 就是一个集合,集合的概念就是一堆不重复值的组合。利用Redis提供的Sets数据结构,可以存储一些集合性的数据,比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

  • Sorted Sets

    和Sets相比,Sorted Sets增加了一个权重参数score,使得集合中的元素能够按score进行有序排列,比如一个存储全班同学成绩的Sorted Sets,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

  • Pub/Sub

    Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

  • Transactions

    谁说NoSQL都不支持事务,虽然Redis的Transactions提供的并不是严格的ACID的事务(比如一串用EXEC提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行),但是这个Transactions还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间有会有其它客户端命令插进来执行)。Redis还提供了一个Watch功能,你可以对一个key进行Watch,然后再执行Transactions,在这过程中,如果这个Watched的值进行了修改,那么这个Transactions会发现并拒绝执行。


Q: Redis有哪些数据结构?#

A: 字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。
如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。
如果你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了。

Q: 使用过Redis分布式锁么,它是什么回事?#

A:

  • 先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
  • 这时候对方会告诉你说你回答得不错,然后接着问如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
  • 这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。
  • 紧接着你需要抓一抓自己得脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!对方这时会显露笑容,心里开始默念:摁,这小子还不错。

Q: 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?#

A:使用keys指令可以扫出指定模式的key列表。


Q:对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?#

A: 这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。


Q: 使用过Redis做异步队列么,你是怎么用的?#

A:
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。


Q: 如果对方追问可不可以不用sleep呢?#

A:
list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。


Q:

如果对方追问能不能生产一次消费多次呢?#

A:
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。


Q: 对方追问pub/sub有什么缺点?#

A:在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。


Q: 如果对方追问redis如何实现延时队列?#

A:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。


Q: 如果有大量的key需要设置同一时间过期,一般需要注意什么?#

A: 如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。


Q:Redis如何做持久化的?#

A:
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。


Q:上面持久化的过程如果突然机器掉电会怎样?#

A: 取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。


Q: 对方追问bgsave的原理是什么?#

A:
你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。


Q: Pipeline有什么好处,为什么要用pipeline?#

A:
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。


Q: Redis的同步机制了解么?#

A:
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。


Q:是否使用过Redis集群,集群的原理是什么?#

A:
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。


[toc]

Q: 你怎么确认你的redis变慢了或者有问题呢?#

A:
用”相同规格“(CPU、内存)的其他redis(基准性能机器)做对比
用–latency-history 命令获取最近一段时间内各命令的响应延迟
如果比基准时间大了2倍以上,那就是变慢了。


Q: 使用什么样的redis命令可能导致性能下降?#

A:
O(N) 以上复杂度的命令,例如 SORT、SUNION、ZUNIONSTORE 等需要大量数据做聚合的命令
*注意, 对于数据的聚合操作,放在客户端做


Q: 频繁写入或者删除一个很大的字符串有什么问题?#

A:
如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。
同样的,当删除这个 key 时(过期、淘汰),释放内存也会比较耗时,这种类型的 key 我们一般称之为 bigkey。
还有备份机制中,如果有bigkey,也会导致备份过程的写时复制很慢,导致性能下降。

  • 除了后台释放的方案外, 尽可能不要写bigkey!

Q: 如何找到redis中是否有这种bigkey?#

A:
”–bigkeys -i 频率”命令 可以获取各种类型的key大小分布情况
本质上就是调用了scan,拿到所有key后统计大小和个数。


Q: 有一个和scan类似的命令叫keys命令, 有什么区别?#

A:

  • redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复
  • scan指令可以无阻塞的提取出指定模式的key列表,用的是额外线程。


Q: redis 4.0 lazy-free机制有了解么?#

A:
执行下面的命令,开启lazy-expire模式
#释放过期 key 的内存,放到后台线程执行
lazyfree-lazy-expire yes
涉及删除、过期造成的主线程延迟问题,都可改成lazy-free,在另外的线程中去释放。


Q: 大量key集中过期也会导致某一时刻性能大量下降,除了加随机过期时间外,还能如何避免?#

A:
通过运维手段处理

  • 在 Redis 上执行 INFO 命令就可以拿到这个实例所有的运行状态数据。
  • 重点关注 expired_keys 这一项,它代表整个实例到目前为止,累计删除过期 key 的数量。
  • 当这个指标在很短时间内出现了突增,需要及时报警出来,并进行这类key超时时间的调整和处理

Q: 如何我的redis内存配置的小了,会如何对性能造成影响?#

A:

  • Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。

    这个过程就是数据淘汰过程。 如果过小,频繁淘汰删除,肯定会影响性能。

  • 机器内存太小,导致频繁做系统的swap磁盘-内存页交换。


Q:Redis 实例内存越大越好吗?太大的话可能有什么问题?#

A:

  • 当 Redis 开启了后台 RDB 和 AOF rewrite 后,在执行时,它们都需要主进程fork出一个子进程进行数据的持久化
  • 这个 fork 过程会消耗大量的 CPU 资源,在完成 fork 之前,整个 Redis 实例会被阻塞住,无法处理任何客户端请求。
  • 执行 fork 的耗时与redis实例大小有关,实例越大,耗时越久。

注意,fork和部署类型也有关,如果部署在虚拟机上,fork所用的时间会更久。


Q: linux的内存大页有了解么? 开启后对redis的性能是提升还是下降?#

A:

  • Linux 内核从 2.6.38 开始,支持了内存大页机制,该机制允许应用程序以 2MB 大小为单位,向操作系统申请内存。
  • 这会导致申请内存的时间变大。
  • 当进行AOF或者RDB备份时,会进行写时复制,如果申请页过大,会导致及时只改20B的数据,也会用2MB单位申请,减慢主线程的请求响应速度。

建议关闭redis部署机器上的内存大页。


Q: AOF需要主线程做同步写, 我配置成 appendfsync everysec (写内存,每秒写磁盘), 是不是就不会阻塞主线程,造成性能下降了?为什么?#

A:
rewrite重写过慢
或者其他应用占用磁盘IO,导致写增量AOF文件过慢
这会导致主线吃还没同步写完,就收到了新的写请求,这时候新的写请求就会阻塞。


Q: 如何解决AOF的写磁盘阻塞问题?#

A:
如果是rewrite, 可以调整配置,让rewrite的时候不要做appendfsync写磁盘。

1
2
3
# AOF rewrite 期间,AOF 后台子线程不进行刷盘操作
# 相当于在这期间,临时把 appendfsync 设置为了 none
no-appendfsync-on-rewrite yes

提升磁盘性能,更换为SSD固态硬盘。


Q: redis大量的内存碎片也会导致性能下降,如何优化redis的碎片?#

A:
redis具有开启碎片整理的功能。
相关配置参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 开启自动内存碎片整理(总开关)
activedefrag yes

# 内存使用 100MB 以下,不进行碎片整理
active-defrag-ignore-bytes 100mb

# 内存碎片率超过 10%,开始碎片整理
active-defrag-threshold-lower 10
# 内存碎片率超过 100%,尽最大努力碎片整理
active-defrag-threshold-upper 100

# 内存碎片整理占用 CPU 资源最小百分比
active-defrag-cycle-min 1
# 内存碎片整理占用 CPU 资源最大百分比
active-defrag-cycle-max 25

# 碎片整理期间,对于 List/Set/Hash/ZSet 类型元素一次 Scan 的数量
active-defrag-max-scan-fields 1000

[toc]


发布/订阅, 就是指客户端用subscribe订阅某个频道或者模式(模式就是带通配符的频道集合), 当服务端redis发布消息的时候, 发消息给订阅了的客户端。

  • 执行subscribe命令客户端会进入订阅状态,处于此状态下客户端不能使用除subscribe、unsubscribe、psubscribe和punsubscribe这四个属于"发布/订阅"之外的命令,否则会报错。

  • 模式订阅是psubscribe 命令,可以同时psubscribe 多个模式
    7c4ba17345ace77b60d7a177936b7545e4854f9a

模式订阅的示意图:
00e18168099bcb14a3411be7b039b67e713d0489

Q: 发布的消息会持久化嘛?
A: 不会,因此可靠性不佳。 而且客户端订阅时,不会收到之前发布过的消息


Q: 频道订阅的原理是怎么样的?底层结构是什么?#

A:
底层结构是字典+链表
ebd8d1addb675d6acffcb6925667757c36de7323

客户端订阅后, redis会把客户端的连接信息放到这个频道的链表上。
redis发布时,根据字典找到链表,然后逐个发送。


Q: 模式订阅呢?又是怎么做的#

A:客户端做模式订阅时,会放到另一个链表上pubsubPattern,就不是字典了
681657383aed243f3bc7d6238bd8c772dbeca81a

链表里每个节点是 “1个客户端+1个模式”
当发布一个消息时, 除了频道字典里的发布, 还会去这个pubsubPattern里遍历,找到所有匹配的模式的客户端,然后发送。

注意模式是不存在字典的,所以如果真的订阅了很多相同的毛模式,是会重复发送的。

[toc]


redis5.0增加了一个结构 stream
是具备一些可靠性机制的消息队列实现

Q: stream中具有哪些结构?#

A:

  1. 消费组——Consumergroup
    1个消费组可以有多个消费者,消费者在一个消费组里抢消息。
  2. 游标id——last_deliverer_id
    记录当前这个消费组被读取到哪了。
    每有一个消费者读取了一条消息, 组的这个游标就+1
  3. 未接收响应的消息里pending_ids
    记录了已经被消费,但是还没调用XACK进行响应的消息。
    是相比list消息队列的重要可靠性提升成员。
  4. 消息id——每个消息独一无的身份
    时间戳+序号
  5. 消息内容
    8efce808fdd905c339f52f6afa33abba45d7a400

stream简单命令#

详细:
Redis Stream

  • XADD 添加消息
  • XDEL 直接删除
  • XREAD 消费,支持阻塞,且消费后会进入pending
  • XACK
  • XGROUP CREATE - 创建消费者组
  • XREADGROUP GROUP - 读取消费者组中的消息

Q: stream生成的消息id是依赖时间戳的,如果机器因为时间发现不同步,决定回退时钟,可能导致出现一样的消息id,怎么办?#

A:
会维护一个latest_generated_id,即上一次生成的消息id。
当她发现这次的时间戳, 比之前的消息id的时间要小, 说明发生了时间回退
这时候他仍会沿用上一次消息id的时间戳,只是在选后上去+1。
db5affdc81c9d08b0dc6b7fb9e6948e5afdaf5a3


Q: 某个消费者消费了消息, 但是消费的过程中挂掉了 ,在重启中,这时候消息会丢失吗?#

A:
stream有个pending列表,存放已经被消费走,但是没收到ACK响应的消息。
pending列表中会记录这个消息被谁消费了。
如果重启的消费者上限后,来取消息时,会先从这个pending里取消息, 这样就可以保证拿到消息不丢了。


Q: 如果消费者不是重启,而是整个机器被剔除了呢?或者长时间挂掉#

A:
XPENDING 命令可以查看pending消息的未被读取时长。
如果超过一定时间, 可以用XCLAIM命令进行转移, 把超过时间的消息转移给另一个消费者的pending列表处理。
这个需要自己写定时线程检查。


Q: 如果这个消息本身就有问题, 无论谁消费都会报错,导致没有ack,怎么办?#

A:
pending消息还有个属性是被读取次数。
如果发现被读取次数超过一定上限, 说明有问题,于是调用XDEL删除问题消息。

[toc]


String字符串#

string 不是简单的字符串的意思
而是key -> value(string)
即value是一个string ,可以用一个key去对应到,key也可以理解成变量名。

  • 这个string可以当成数字使用。
    set count 1
    incr count // count+1
    incrby count 100 // count+100
    但是当你get count的时候,得到的是string的count
    23ec1c8f14dda8d7d967db125bf3b0d3b12c6182

  • 也可以是,jpg图片或者序列化的对象。
    因此string本质上是二进制安全的,不会因为编码不同发生变化


Q: redis-string的key可以是中文吗?#

A:
key可以是中文,不过redis在存储的时候会将key进行序列化,在redis中存储的是字节码。

不推荐使用中文Key,内耗更大并且出现乱码的可能性也是有的。编码字节存储等都是不一样的占用空间。


Q: 那么redis string的value可以存中文吗?什么样子的?要不要指定编码?#

A:

  • 在保存到redis中时以utf8的编码方式保存的,实际上时等价于正常的这种十六进制字符串
  • 默认看到的是乱码\xe4\xb8\xaa\xe4\xba\xba\xe8\xb5\x9b
  • 启动cli时多加一个参数就行了,redis-cli --raw这种方式,就可以查看中文了

Q: redis-string的用途随便说几个#

A:

  • 缓存。把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力
  • 计数器,单节点redis的计数是同步的,不用担心并发问题。调用incr等方法即可
  • session缓存

Q: string的底层实现有了解吗?和C语言的字符串有什么区别?#

A:
redis的字符串类型是由一种叫做简单动态字符串(SDS)的数据类型来实现
其实就是多了当前长度和剩余容量

1
2
3
4
5
struct sdshdr {
int len; //buf中已占用空间的长度
int free; //buf中剩余空间的长度
char buf[]; //数据空间
}

SDC和C语言字符串的区别:

  1. SDS保存了字符串的长度,而C语言不保存,C语言只能遍历找到第一个\0的结束符才能确定字符串的长度
  2. 修改SDS,会检查空间是否足够,不足会先扩展空间,防止缓冲区溢出,C字符串不会检查
  3. SDS的预分配空间机制,可以减少为字符串重新分配空间的次数

备注:重新分配空间方式,小于1M的数据 翻倍+1,例如:13K+13K+1,如果大于1M,每次多分配1M,例如:10M+1M+1,如果字符串变短,并不会立即缩短,而是采用惰性空间释放,有专门的API可以释放多余空间

List列表#

  • list的本质就是一个双端链表!
  • 用于消息排队、最新消息更新、 消息消费队列

Q: 用redis的list可以实现哪些数据结构?怎么实现?#

A:

  • 栈:lpush + lpop (只在一个方向上做push和pop)
  • 队列:lpush + rpop(一个方向入, 一个方向出)
  • 有限集合:lpush + ltrim(trim是截断,这样可以限制长度)
  • 阻塞的消费者消息队列:lpush+brpop(b就是block的意思,没有就阻塞)

Q: 消费者消息队列能设置超时吗?#

A:
可以的
BLPOP key1 [key2 ] timeout


Q: list里的存储结构是什么?#

A:
链表。 链表里存的是string。


Set集合#

  • set本质上就是实现了一个哈希表, 以至于不能在这个set成员中放入相同的key
  • 除了简单的sadd、smember外,还有很多求交集的关键方法。

**

Q: 为什么标签可以用set来做?而不用list#

A:
因为set允许任意的增删, 而list只能对前后操作。 标签可能会被随时增加删除。
因此被某某某点赞/取消的系统也可以用redis的set做缓存


Hash散列#

  • 和set的区别, 就是key-value, 而set里只有key
  • 命令都是hset、hget、hxxx等

Q: hash存储有什么限制?#

A:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
  • 哈希对象保存的键值对数量小于512个

Q: redis如何解决哈希冲突的?#

A:
链表法(挂链法),后入的放到最前面


Q: 散列表容量不足怎么办?#

A:
容量不足时的rehash:
键值数据量变动时,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。

  • 如果是扩充,新数组的空间大小为 大于2*used的2的n次方,比如:used=5,则去大于10的第一个2的n次方,为16
  • 如果是缩小,新数组的空间大小为第一个不大于used的2的n次方,比如:used=5,则新大小为4
    即永远2的n次方
    Q:

Zset有序集合#

  • zset不是java中的treeMap。
    treeMap是根据key进行排序
    而zset是根据scope排序, ke是唯一的, key对应的scope不是唯一的。

  • 关键命令: 可以用ZRANGE拿到前几个。


Q: zset的原理是什么?#

A:
底层分别使用ziplist(压缩链表)和skiplist(跳表)实现。

当zset满足以下两个条件的时候,使用ziplist:

  1. 保存的元素少于128个
  2. 保存的所有元素大小都小于64字节
    不满足这两个条件则使用skiplist。
    (注意:这两个数值是可以通过redis.conf的zset-max-ziplist-entries 和 zset-max-ziplist-value选项 进行修改。)处。

  1. 跳表:
    skiplist实现原理
  • 插入时,随机一个层数,然后找到那一层的对应位置插入,并更新前后节点
  • get时,先从最高层找,比要找的大时,再去下一层。
  • 随机时不是完全随机,会根据当前分布情况,修改概率p,并设置某一层的最大限制,防止比别的层过多。
    b148147baa7475ed77e0525aa6fb624c7f822861

Q: redis 3.0里zset有什么改变?#

A:
zset的底层实现变成了quicklist。

  • quicklist宏观上是一个双向链表,因此,它具有一个双向链表的有点,进行插入或删除操作时非常方便,虽然复杂度为O(n),但是不需要内存的复制,提高了效率,而且访问两端元素复杂度为O(1)。
  • quicklist微观上是一片片entry节点,每一片entry节点内存连续且顺序存储,可以通过二分查找以 log2(n)log2(n) 的复杂度进行定位。
    快速列表(quicklist)源码解读

HyperLogLogs基数统计#

Q: 基数用来解决什么问题?#

A:
解决海量计数问题, 例如每天访问的ip数量, 如果真的存每个ip,会很大。
而基数利用位运算来统计, 限制死了每个键的内存大小, 虽然有一定误差(即位冲突的情况),但是在海量的情况下不影响粗略统计。
4c43aab1de89a220066b3d858353d798100969b3


基数每个键占用的内容都是 12K,理论存储近似接近 2^64 个值, 因为位冲突问题可能会偏少。


BitMap 位存储#

用于统计 ”是“或者”否“的 这种一堆人的状态
比如是否在线的缓存, 可以用bitmap。

如果存储一年的打卡状态需要多少内存呢?
365 天 = 365 bit 1字节 = 8bit 46 个字节左右,非常节省内存


geospatial 地理信息#

求解一些经纬度信息的。略

stream#

是一种更可靠的消息队列实现,redis5.0后实现的。
具体见后面的stream详解

[toc]

Redis是一款内存高速缓存数据库。Redis全称为:Remote Dictionary Server(远程数据服务),使用C语言编写,Redis是一个key-value存储系统(键值存储系统),支持丰富的数据类型


Q: redis有什么好处你知道吗?
A:

  • 读写性能好
  • 支持的几种数据结构很好用,满足大部分使用场景
  • 原子性,不用担心并发请求的问题
  • 特性多,支持订阅/发布,过期等
  • 支持持久化(RDB, AOF)
  • 支持分布式,高可靠

Q: redis读写性能的数据具体有了解过吗?
A:
Redis能读的速度是110000次(即10W+)/s
写的速度是81000次/s (即8W+)


Q: redis的使用场景讲几个吧
A:

  • 热点数据缓存
  • 限时业务(用expir命令设置key超时,key没了商品就没了)
  • 计数器问题(incrby命令可实现原子性递增)
  • 分布式锁(setnx命令)
  • 延时事件(可设置延时+监听,触发超时事件删除一些问题数据)
  • 排行榜问题(SortedSet)
  • 点赞、好友关系存储(集合支持求交集、并集、差集)
  • 简单队列(list push/pop)

Redis官网:http://redis.io/

[toc]


Q: 讲一下你对虚拟化的理解?什么是虚拟化?#

A:
虚拟化可以理解为,将一台机器上的各种资源,进行抽象和隔离,方便在一台机器上运行多个进程或者系统,而且他们互相隔离,各自使用各自的资源,不会在逻辑上互相竞争资源。

虚拟机包含硬件和软件虚拟化。我这里只说软件虚拟化,也就是利用软件技术,在现有的物理平台上实现对物理平台访问的截获和模拟。

经典的有VMWare桌面虚拟、KVM全虚拟化(linux内核模块)、LXC(linux容器,轻量级虚拟化,重点)


Q: 讲一下docker是什么?#

A:
Docker是一个开源的应用容器引擎

  • 它让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到安装了任何 Linux 发行版本的机器上
  • 发布到机器上之后, 会使用沙箱机制完全虚拟出一个完整的内部环境
  • 容器之间不会有任何接口,从而让容器与宿主机之间、容器与容器之间隔离的更加彻底

Q: docker是哪种类型的虚拟化?#

A:
docker属于操作系统层虚拟化(将操作系统内核虚拟化,可以允许使用者空间软件实例被分割成几个独立的单元,并没有模拟硬件), 换句话说,无法改变操作系统的性质。
底层基于LXC(Linux Container,Linux容器)进行轻量化虚拟的实现。


Q: 你们为什么要用docker?有什么好处?#

A:

  1. 如果资源有限,只有1台机器,但是有3个微服务需要部署,每台部署3个服务进程后, 万一一个进程跑多线程时, 线程池打满了, 结果把cpu全占用, 让另外2个服务一起变慢了。 或者内存疯狂涨,把内存打满了搞挂另外2个进程。
    因此引入docker后,每个服务隔离在容器中,使用的线程资源和内存受到独立的限制,就不会互相影响了。

  2. 当需要部署新的微服务时, 如果没有docker,就需要手动部署,按照指导书一点点搭建,配置参数,很容易错,且容易导致开发环境和生产环境参数不一致。
    使用docker后,可以打包docker镜像进行容器部署, 发布的速度也变快了,扩容也变简单了。


Q: Docker和虚拟机Virtual Machine的区别?#

A:

  • Docker容器不是虚拟机, 只是当初被疯狂宣传为“轻量级虚拟机”
  • 虚拟机必须要自己模拟一个虚拟操作系统guestOs才能运行, 但docker不需要,直接使用linux自身支持的namespace和cgroup实现资源隔离。
  • 云服务提供商通常采用虚拟机技术隔离不同的用户/租户。 而Docker通常用于隔离不同的应用,例如前端,后端以及数据库

[toc]



Q: 处理微服务治理时,为什么大多采用rpc而非restful做服务间调用?#

A:

  1. restful大多基于json做序列化, 相比于直接做二进制序列化,性能较差。 而服务间调用频率往往非常高,必须做到低时延。
  2. 常见的rpc框架往往能封装好底层网络通信协议、寻址、序列化问题, 通过一个proto文件即可生成 客户端代码和服务端代码, 大大简化开发工作。

Q: 服务A 调用服务B时, 如果超时了, 应该怎么处理?#

A:
要分情况。

  • 如果是读服务,可以做failover重试, 重试3次才报失败。
  • 如果是写服务, 考虑到幂等性,不应该做重试,直接报错。
  • 如果是耗时较长的接口, 如果超时时间设的太短,则可能因为重试引发雪崩(大量该接口调用都发生连锁超时), 因此不应该采用重试。

常见的超时处理操作和情况:

  • 重试——读操作
  • 失败就立刻报错——非幂等的读操作
  • 出现异常直接忽略——用于审计日志写入和读取等不重要的操作
  • 失败自动恢复,定时重发——用于重要消息通知等操作

Q: 服务发现有哪两种模式?#

A:

  1. 服务端服务发现模式
    提供路由服务route, 由路由服务帮客户端做服务发现和负载均衡调用, 路由服务和注册中心相连接
    缺点: 代理中心额外要引入容错性和伸缩性,成本比较大。 且性能上服务也较差。

  2. 客户端服务发现模式,
    客户端和注册中心相连接, 获取发现服务地址,自己处理多地址的负载均衡等策略。


Q: zookeeper作为注册中心有什么缺点?#

A:

  1. 服务扩容时, 应用启动缓慢
  2. 冗余的服务配置项会增加存储压力, 扩大网络开销
    原因:
    ZK是一个典型的CP系统, 基于ZAB原子广播的强一致性中间件, 写操作存在单点问题,无法通过水平扩容来解决。 当客户端发送写请求时, 集群中的其他节点会优先转发给leader节点, leader节点来做具体的写操作。 只有N/2+1个以上的节点都同步成功, 写才算完成。
    服务扩容的时候, tps越高, 服务注册写入效率月底, 导致上游大量请求排队, 服务启动和配置下载变得缓慢。

Q: 有什么办法解决上面的问题?#

A:

  1. 增加zk的observer节点。 因为observer节点不会增加写入开销,但可以分担带宽压力。
  2. 注册项精简, 只写入关键配置, 其他配置放到另外的元数据中心,实现配置垂直分离。
  3. 去除zk的强一致性, 自研一个最终一致、增量数据返回、去中心化的分布式注册中心。

Q: 服务调用的互相、循环依赖问题如何解决? 即A服务启动依赖B服务某接口, B服务启动A服务某接口的时候#

A:
尽量避免循环依赖, 调用流上要设计成单项依赖
如果确实存在, 要避免开启强制的check服务开关, 不去校验依赖服务是否通,而是启动后定时轮询。


Q: 做一个服务调用链的话, 一次请求需要包含哪些信息?#

A:
traceId, 表示这是哪一次请求的完整跟踪
spanId, 中间某次服务调用的请求/响应过程
parentSpanId, 上一次调用的id, 用于确定请求依赖关系
xxxConetext: 包含更多详细的信息, 例如是否是服务调用/提供方, host、port、接口名,被调用的服务方法名。


Q: 调用链跟踪有哪些时间信息需要统计?#

A:
以一次span为例, 需要记录
Client SendTime 客户端刚发出的时间CS
Server ReciveTime 服务接收请求时间SR
Server Send Time 服务发送响应时间SS
Client Receive Time 客户端接收响应时间CR

  • 服务调用耗时= CR - CS
  • 服务处理耗时= SS - SR
  • 网络开销总耗时= 服务调用耗时 - 服务处理耗时
  • 前置网络耗时 =SR-CS
  • 后置网络耗时 = CR - SS

Q: 怎么在接收和发出时, 能用到同一个traceId?#

A:
使用threadLocal即可, 从该线程中拿到这个traceId变量值,用完销毁。


Q: 所有调用链信息直接写入数据库吗?#

A:
不可以, 容易对数据库造成较大压力, 应该放入消息队列进行消费, 实现削峰的效果


Q: 有哪些实现方式(代码如何实现),对于微服务而言?#

A:
一种看框架本身是否支持相关的filter,继续配置, 例如dubb的filter
或者自己写动态AOP, 例如利用-javaagent 实现动态的jvm增强需求。 实质上利用了jdk1.5引入的Instrumentation接口


Q: 如何实现一个全局服务的定时任务管理?如何设计? 即如果你们希望做定时任务的话,会怎么做?#

A:

  • 方案一:
    可以借助注册中心实现定时任务管理。
  1. 服务自身维护需要的定时任务接口、定时触发条件。 可以自定义注解设置在接口上。
  2. 然后通过注册动作将接口以及接口上的注解发布给注册中心。
  3. 注册中心将定时类接口呈现给 注册中心服务的定时任务管理页。
  4. 当到达时间, 注册中心调用定时任务接口,触发动作。 如果需要关闭,在管理页直接关闭即可。
  5. 当服务的定时任务接口返回了已卸载或者不存在的接口时, 注册中心要删除这个定时任务,不再呈现在页面上。
    一个简单的定时任务调度中心设计方案

当时我自己服务里没有这种功能的注册中心, 实现起来的接口开发量还是比较大的。

  • 方案二:基于数据库锁quartz
    当多个server的定时任务到时间时, 先去抢锁表里该任务的悲观锁,如果抢到了就执行, 如果抢不到就等待。当重新拿到锁后,发现该定时任务已经被设置成“完成”,时间也对的上,于是就不再执行了。

缺点: 集群特性对于高CPU使用率的任务效果很好,但是对于大量的短任务,各个节点都会抢占数据库锁,这样就出现大量的线程等待资源。这种情况随着节点的增加会越来越严重。
另外,quartz的分布式只是解决了高可用的问题,并没有解决任务分片的问题,还是会有单机处理的极限,即某台机器执行过多的定时任务导致负载暴涨,而其他的机器一直凑巧没抢到。

  • 方案三: 引入支持任务分片的分布式任务调度框架
  1. elastic-job (当当)
  2. TBSchedule(淘宝)
  3. Saturn(唯品会)
    默认的分片策略,作业数能被服务器数整除情况下均匀分配:
    4ef34664a006479928c47313a27a0dc4de910f47
    根据哈希的分片策略:

根据作业名的哈希值奇偶数决定采用IP升/降序算法实现分片,作业名的哈希值为奇数则IP升序,作业名的哈希值为偶数则IP降序,通过这种方式用于将不同的作业分片负载均衡至不同的服务器。
如作业名哈希值为偶数,则采用IP降序算法实现分片,这样就避免了采用平均分配算法时IP地址靠后的服务器空闲的问题。

聊聊分布式定时任务elastic-job作业分片策略


Q: 有哪些负载均衡算法?
A:

  • ActiveWeight / LeastActive :低并发度优先, 统计某时刻的被调用 call 数越小优先级越高。即根据短时间内被调用数获取。需要统计的成本,实现难度比较高,存在耦合。

  • Random :随机,按权重设置随机概率。在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。节点少的时候,随机有可能出现连续都随机到一个节点上,导致负载不均衡。

  • RoundRobin :轮循,按公约后的权重设置轮循比率。
    存在慢的提供者累积请求问题,比如:第二台机器性能很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

  • LocalFirst :本地服务优先获取策略。

  • Consistent :一致性 Hash ,相同参数的请求总是发到同一提供者。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。


Q: 负载均衡时,请求失败怎么办?
A:

  • Failover :失败自动切换,当出现失败,重试其它服务器。
    通常用于读操作,但重试会带来更长延迟。

  • Failfast :快速失败,只发起一次调用,失败立即报错。
    通常用于非幂等性的写操作,比如新增记录。

  • Failsafe :失败安全,出现异常时,直接忽略。
    通常用于写入审计日志等操作。

  • Failback :失败自动恢复,后台记录失败请求,定时重发。
    通常用于消息通知操作。

  • Forking :并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。

  • Broadcast :广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。