Catalog
- 1 Redis适用场景?Redis底层数据结构使用场景?
- 2 Redis到底是单线程还是多线程?Redis单线程模型?
- 3 Redis底层数据结构
- 4 Redis有多快?Redis为什么快?底层怎么实现的?
- 5 讲一下Redis的I/O多路复用中的Reactor设计模式
- 6 Redis持久化机制
- 7 Redis的watch机制
- 8 Redis网络通信协议
- 9 Redis实现分布式锁
- 10 分布式锁实现方案及优缺点比较
- 11 BSD协议是什么?
- 12 Redis HA
- 13 Redis怎么设置最大内存?超过最大内存会怎样?
- 14 Redis虚拟内存机制
- 15 Redis实现发号器
- 16 Redis管道技术pipeline
- 17 缓存穿透&缓存击穿&缓存雪崩
- 18 二级缓存怎么建设,第二级缓存为什么用本地缓存、两层都用分布式缓存不行吗?了解过业内哪些成熟框架?
- 19 缓存预热
- 20 缓存更新
- 21 缓存降级
- 22 缓存淘汰算法
- 23 LRU怎么实现?Redis是怎么实现的?
- 24 缓存一致性解决方案(最终一致性)
- 25 Redis实现消息队列
- 26 Redis小对象压缩存储
- 27 Redis内存回收机制
- 28 Redis内存分配算法
- 29 Redis info指令
- 30 Redis过期策略
- 31 Redis懒惰删除
- 32 Redis安全机制
- 33 Redis有哪些时间复杂度O(n)的指令,应该怎么处理这些指令,有什么改进措施?
- 34 Redis集群方案
- 35 数据分片方式
- 36 Redis Cluster中的槽位数为什么是16384?
- 37 CRC算法(CRC8&CRC16&CRC32&CRC64)
- 38 Redis中key和value的存储大小限制?
- 39 Redis大key优化方案
- 40 Redis源码看过吗?
Content
1 Redis适用场景?Redis底层数据结构使用场景?
Redis使用场景?
缓存
缓存热点数据,比如用户登录注册时候验证码、用户名、token等。发号器/分布式唯一ID
利用incr/incrby指令构建发号器计数器/限速器
限流算法中用做计数器,统计播放数据/浏览量/在线人数,限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等。限时业务
限时优惠活动、手机验证码等分布式锁
分布式锁三种实现方式数据库锁、Redis、zookeeper。Redis实现分布锁主要有setnx ex命令、Redission、RedLock。队列/消息队列(发布订阅/阻塞队列)
利用Redis的list数据结构,能够很方便的执行队列操作。lpush/rpop, rpush/lpop,阻塞命令brpop/blpop。延迟队列/延时操作
下单后30分钟内支付,利用设置一个key,同时设置30分钟后过期, 我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。 也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。bitmap/BloomFilter
布隆过滤器适用于大数据量情况下某元素是否存在于集合中的判断。时间和空间效率都很高,缺陷是存在误判。比如说爬虫重复URL检测、HBASE写入数据是否存在于磁盘文件、检查单词拼写是否正确、CDN(squid)代理缓存技术等,利用的是Redis的Set数据结构去重功能。Session服务器
使用Redis进行会话缓存,也有一些现成的jar包,tomcat-redis-session-manager.jar等。排行榜问题
关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。在奶茶活动中,我们需要展示各个部门的点赞排行榜, 所以我针对每个部门做了一个SortedSet,然后以用户的openid作为上面的username,以用户的点赞数作为上面的score, 然后针对每个用户做一个hash,通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息,这个当时在实际运用中性能体验也蛮不错的。分页、模糊搜索
redis的set集合中提供了一个zrangebylex方法,语法如下:ZRANGEBYLEX key min max [LIMIT offset count]
通过ZRANGEBYLEX zset - + LIMIT 0 10 可以进行分页数据查询,其中- +表示获取全部数据zrangebylex key min max 这个就可以返回字典区间的数据,利用这个特性可以进行模糊查询功能,这个也是目前我在redis中发现的唯一一个支持对存储内容进行模糊查询的特性。前几天我通过这个特性,对学校数据进行了模拟测试,学校数据60万左右,响应时间在700ms左右,比mysql的like查询稍微快一点,但是由于它可以避免大量的数据库io操作,所以总体还是比直接mysql查询更利于系统的性能保障。点赞、好友等相互关系的存储
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。 又或者在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
这个在奶茶活动中有运用,就是利用set存储用户之间的点赞关联的,另外在点赞前判断是否点赞过就利用了sismember方法,当时这个接口的响应时间控制在10毫秒内,十分高效。
Redis底层数据结构适用场景?
- String
- SDS,simple dynamic string,简单动态字符串
- 缓存用户信息,json字符串(一次性全部序列化)
- List
- ziplist,quicklist
- LinkedList,异步队列,确保顺序
- 利用lrange命令做基于Redis的分页功能
- 消息队列
- Hash
- HashMap,数组+链表
- 缓存用户信息,不用一次性全部序列化,可以对每个字段单独存储,部分获取节省网络流量
- Set
- HashSet,无序唯一,可用于去重
- 缓存中奖的用户ID,因为有去重功能,可以保证同一用户不会中奖两次
- 去重
- Zset
- HashMap+SortedSet,跳跃列表skiplist
- 排行榜
- top k
- 缓存粉丝列表,value是粉丝的用户ID,score是关注时间,可以按关注时间排序
- 缓存学生成绩,value是学生的ID,score是他的成绩,可以按成绩排名
- 延时任务
- 范围查找
- 优先队列,给每条消息加一个唯一的序号
2 Redis到底是单线程还是多线程?Redis单线程模型?
- Redis6.0以前是单线程,像node.js、Nginx等都是单线程模型,但是都是服务器高性能的典范,使用Redis指令要特别注意时间复杂度为O(n)的指令,一不小心就会导致Redis卡顿;
- Redis6.0开始引入了多线程
- Redis的性能瓶颈是网络I/O操作,redis多线程部分是处理网络读写及协议解析;
- Redis用del指令删除key,如果key很大,就需要多线程异步非阻塞的删除,释放内存减少对主线程阻塞的时间,提高执行效率。
3 Redis底层数据结构
SDS,Simple Dynamic String,简单动态字符串。
双向链表
字典/符号表/关联数组/map
整数集合intset,保证集合中不出现重复元素,节省内存
压缩列表ziplist
快速列表quicklist=ziplist+linkedlist
- 链表的前驱和后继一共要占据16字节,可以继续优化
- linkedlist分段,每段用ziplist,多个ziplist之间使用双向指针串起来
跳跃表skiplist-zset
- 二分查找,O(n)降到O(lg(n))
紧凑列表listpack
- Redis5.0引入的listpack,设计目的是替代ziplist
- 尾部存放当前元素的长度,最后一个元素的位置可以通过总字节数和本身长度算出来,比ziplist节省一个标记最后一个元素位置的字段zltail_offset
基数树,有序字典树,rax
- zset是按照score排序,rax是按照key排序
- rax被用在Redis Stream里面存储消息队列,快速定位消息ID
4 Redis有多快?Redis为什么快?底层怎么实现的?
C语言编写,官方QPS,每秒读11w,写8w。
- Redis基于内存操作,内存读写速度快
- Redis单线程,省去了多线程上下文切换的时间开销
- Redis使用IO多路复用技术,可以处理并发的连接,事件轮询API就是Java里面的NIO技术
- select(1983年在BSD里实现)
- 会修改传入的参数
- 轮询sock查找数据,数据量大开销很大
- 线程非安全
- 只能监视1024个连接
- poll(1997年实现)
- 线程非安全
- epoll(2002年实现,Linux系统是epoll,FreeBSD和macosx用的是kqueue)
- 不用轮询sock查找数据就能知道哪个sock有数据
- 线程安全
- select函数是阻塞的,可以注册自己感兴趣的IO请求,等到数据来时再处理,可以提高CPU利用率,epoll利用Reactor设计模式实现了这一机制。
- 只支持Linux系统
- select(1983年在BSD里实现)
- Redis定时任务会记录在一个被称为"最小堆"的数据结构中,在这个堆中最快要执行的任务排在堆的最上方,在每个循环周期里,Redis会对最小堆里面已经到点的任务进行处理,处理完毕将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。
5 讲一下Redis的I/O多路复用中的Reactor设计模式
- 在高性能的I/O设计中,有两个著名的模式
- Reactor模式,用于同步I/O操作;
- Proactor模式,用于异步I/O操作。
- Reactor设计模式又叫做响应器模式,通常用于NIO非阻塞IO的通信框架中,比如说Netty。
- Reactor是事件驱动机制,应用程序需要提供相应的接口并注册到Reactor上,如果有相应事件发生,Reactor将主动调用回调函数。
6 Redis持久化机制
-
RDB
- 默认持久化方式,将内存中数据以快照方式全量备份保存到二进制文件dump.rdb,RDB效率高但是最后一次持久化后的数据可能丢失;
- Redis使用操作系统的多进程COW(Copy On Write)机制实现快照持久化,主进程会调用glibc的函数fork一个子进程进行持久化,子进程和父进程共享内存里面的代码段和数据段,这是Linux操作系统的机制,目的是为了节约内存资源。
- COW机制,Copy On Write机制是Linux操作系统优化策略,数据段由很多操作系统的页面组成,每个页面的大小只有4KB,一个Redis实例里面一般都会有成千上万个页面。当父进程要对其中一个页面的数据进行修改时,会将页面复制一份分离出来,然后对这个复制的页面进行修改,子进程对应的页面没有变化。
-
AOF
- Append of File,AOF日志存储的是Redis对内存进行写操作的顺序指令序列,连续的增量备份;
- Redis提供了bgrewriteaof指令用于对AOF日志进行重写,原理就是开辟一个子进程对内存进行遍历,转换成一系列的Redis操作指令,序列化到一个新的AOF日志文件中,序列化完毕后将操作期间发生的增量AOF日志追加到这个新的AOF日志文件中,追加完毕后替代旧的AOF日志文件。
- Linux的glibc提供了fsync(int fd)函数将指定文件的内容强制从内存缓存刷到磁盘。fsync是一个很耗时的磁盘IO操作,会降低Redis性能同时增加系统IO负担,所以对fsync调用策略:
- Redis永不调用fsync,交给操作系统决定定时同步磁盘,很不安全,生产环境不适用
- Redis来一个指令就调用fsync一次,非常慢性能损耗严重,生产环境不适用
- Redis每隔1s执行一次fsync操作,1s可以配置。
7 Redis的watch机制
- watch是乐观锁,在exec执行前监视任意数量的key,在exec提交时检查是否有一个key被修改了,如果是则拒绝提交事务并返回客户端nil
- 每个Redis数据库都保存了watched_keys的字典,字典的key是所有被watched的key,字典的value是一个链表,记录了所有watched_keys对应的客户端;
- 所有对key的修改都会去watched_keys字典检查,发现有watch就会打开对应的redis_dirty_cas标识,表示该事务安全性被破坏,如果事务提交时检测到该标识已打开就会拒绝提交事务,以保证事务安全性。
- Redis禁止在multi和exec之间执行watch指令,而必须在multi之前盯住关键变量,否则会出错。
8 Redis网络通信协议
RESP
Redis客户端和服务端通信使用RESP(REdis Serialization Protocol)协议,RESP是一种直观的文本协议,主要优势是实现简单、解析性能好,Redis作为认为数据库系统的瓶颈一般不在于网络流量,而在于数据库自身内部逻辑处理上,所以Redis使用了浪费流量的文本协议,依然取得极高的访问性能。
RESP 协议在Redis1.2被引入,直到Redis2.0才成为和Redis服务器通信的标准。这个协议需要在你的Redis客户端实现。
虽然RESP在技术上不特定于TCP,但是在Redis的上下文中,该协议仅用于TCP连接(或类似的面向流的连接,如unix套接字)。
RESP 是二进制安全的,并且不需要处理从一个进程发到另外一个进程的批量数据,因为它使用前缀长度来传输批量数据。 注意:这里概述的协议仅用于客户机-服务器通信。Redis集群使用不同的二进制协议在节点之间交换消息。
-
在 RESP 中,Redis协议将传输的结构数据分为5种最小单元类型,单元结束时统一加上回车换行符号\r\n,数据的类型依赖于首字节:
- 单行字符串(Simple Strings): 响应的首字节是 “+”
- 多行字符串(Bulk Strings): 响应的首字节是“$”
- NULL,用多行字符串表示,长度为-1
- 空串,用多行字符串表示,长度为0
- 整型(Integers): 响应的首字节是 “:”
- 数组(Arrays): 响应的首字节是 “*“
- 错误(Errors): 响应的首字节是 “-“
gossip
- redis cluster节点间采用gossip协议进行通信,各节点都有一份元数据,变更发送到各个节点,改善了集中式变更元数据的压力。
- 集群的周期性函数clusterCron()执行周期是100ms,为了保证传播效率,每10个周期,也就是1s,每个节点都会随机选择5个其它节点,并从中选择一个最久没有通信的节点发送ping消息。
- 对哪些长时间没有 “被” 随机到的节点进行特殊照顾:每个周期(100ms)扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新。
客户端 ——> 服务器
- 客户端向服务器发送的指令只有一种格式:多行字符串数组
服务器 ——> 客户端
- 服务器向客户端回复的响应要支持多种数据结构,就是以上5种基本类型的组合。
- 单行字符串,没有双引号括起来,比如set key value返回OK
- 多行字符串,使用双引号括起来,比如get key返回"value"
- 整数响应,比如incr返回1
- 数组响应,比如hset设值后hgetall返回
- 错误响应,-ERR
9 Redis实现分布式锁
实现方案
-
redis单节点实现
- Redis2.8支持setnx和expire指令可以原子执行,给key设置一个唯一值,即value要是唯一的,set key value nx ex 5000
- nx,key不存在才设置值,key存在不做任何操作
- ex 5000,设置key的过期时间是5000毫秒
- 超时问题
- Lua脚本可以保证多个指令的原子性执行
- 可重入性问题
- Redis分布式锁如果要支持可重入,需要对客户端的set方法进行包装,使用线程的ThreadLocal变量存储当前持有锁的计数,需要考虑内存锁计数的过期时间问题
- Redis2.8支持setnx和expire指令可以原子执行,给key设置一个唯一值,即value要是唯一的,set key value nx ex 5000
-
Redisson框架实现分布式锁
- 加锁成功,根据hash算法选择一个节点执行Lua脚本写入redis;
- 加锁失败,循环重试至加锁成功;
- watch dog自动延期机制,后台线程不断延迟锁的生存时间
- 缺陷是主从或者哨兵模式下,master持有了锁然后宕机,锁信息还未来得及同步到slave,slave重新选举为master也获得锁,导致多个客户端同时加锁。
- 联锁,RedissionMultiLock,所有对象实例都加上锁才算成功加锁。
- 红锁,RedissionRedLock,大部分节点加锁成功就算成功加锁。
-
RedLock实现分布式锁
- RedLock需要提供多个Redis实例,这些实例之间相互独立,没有主从关系
- 加锁时,向过半节点发送set(key,value,nx=true,ex=***)指令,过半节点set成功就算加锁成功
- 解锁时,向所有节点发送del指令
- n/2+1节点加锁成功,并且使用锁时间小于锁失效时间才叫加锁成功。
- RedLock算法还需要考虑出错重试、时钟漂移等细节问题
- RedLock需要提供多个Redis实例,这些实例之间相互独立,没有主从关系
常见问题及解决方案
- 死锁
目的要让setnx和setex两个命令成为原子性操作- Lua脚本
- Redis2.8实现了setnx和setex原子性操作,比如RedisTemplate
- 业务时间大于锁超时时间
- Lua脚本
- 可重入锁
- A加锁后服务宕机,恢复后可重复获得锁,验证当前锁的内容是否匹配即可。
10 分布式锁实现方案及优缺点比较
分布式锁三种实现方案
- 数据库锁
- 乐观锁
- 主键ID,简单但是问题多
- 版本号,侵入性强,每个表都要加version字段
- 悲观锁,for update
- 乐观锁
- redis实现分布式锁
- setnx(key,value)+expire()
- Redission
- Redlock
- ZooKeeper实现分布式锁
- 有序临时节点,最小的有序节点获得锁,使用完就消失,通过watch监控节点变化,继续找出最小的有序节点获得锁,依次继续。
分布式锁三种实现方式特点
- 基于数据库实现
- 优点:实现方式简单易懂
- 缺点:性能差,有锁表风险,占用CPU资源多
- 基于Redis实现
- 优点:基于缓存的实现性能高
- 缺点:依赖Redis中间件HA,过期时间不好控制,牺牲了一定的可靠性
- 基于zookeeper实现
- 优点:不需要控制过期时间,可靠性高
- 缺点:依赖zookeeper中间件,leader选举过程中服务不可用,性能较Redis实现低
分布式锁三种实现比较
- 难易:Zookeeper >= 缓存 > 数据库
- 性能:缓存 > Zookeeper >= 数据库
- 可靠性:Zookeeper > 缓存 > 数据库
11 BSD协议是什么?
目前经过Open Source Initiative组织通过批准的开源协议目前有58种(http://www.opensource.org/licenses /alphabetical)。我们在常见的开源协议如BSD,GPL,LGPL,MIT等都是OSI批准的协议。如果要开源自己的代码,最好也是选择这些被批准的开源协议。
- BSD,Berkeley Software Distribution
- Apache License 2.0
- GPL,Gun General Public License
- LGPL,GNU Lesser General Public License
- CPL,Common Public Liecense
- MIT,Massachusetts Institute of Technology
12 Redis HA
- Replication
- Sentinel
- Codis
- slot=1024,crc32(key) % 1024
- Twemproxy
- MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
- Cluster
- slot=16384,crc16(key) % 16384
13 Redis怎么设置最大内存?超过最大内存会怎样?
- redis.conf中maxmemory参数设置最大内存,单位是bytes,一般设置为机器物理内存的四分之三。
- 达到最大内存redis首先会清除已到期或即将到期的key,清理后仍然达到最大内存,将无法写入,写入会报错,OOM command not allowed when used memory > 'maxmemory',但是可以读操作。
- 如果maxmemory没有设置或者设置为0,32位系统最多使用3GB内存,64位系统不限制内存。
- 如果设置了maxmemory,最好要设置过期策略,比如说maxmemory-policy volatile-lru
14 Redis虚拟内存机制
- redis新的VM机制会把数据分页存放,redis会将访问量较少的页即冷数据swap到磁盘,访问多的页即热点数据由磁盘自动置换到内存中。
- VM机制默认是关闭的,可以开启:vm-enabled no
- 虚拟内存文件路径默认是/tmp/redis.swap,不可多个redis实例共享。vm-swap-file /tmp/redis.swap
- vm-max-memory无论设置多小,默认是0,key都是放到内存,value放到磁盘。
- 一个对象可以保存在多个page上,但是一个page上不能被多个对象共享,vm-page-size根据存储数据大小设定的,如果存储小对象,vm-page-size可以设置为32或64bytes,如果存储很大对象,可以设置更大,如果不确定,就用默认值32
- vm-pages设置swap文件中的page数量,在磁盘上每8个pages将消耗1byte的内存。
- wm-max-threads设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,表示访问都是单线程操作,可能会造成较长时间的延迟,默认值为4.
15 Redis实现发号器
/**
* 1.id 总长度 52bit,为了兼容 js,php,flex 等语言 long 类型最长只能 52 bit <br>
* 2.最高 28 bit 为秒级时间戳,因为位数限制,不能从 1970.1.1 开始,-1420041600 表示从 2015.1.1
* 开始,大约可以使用10年(3106天) <br>
* 3.接下来4个bit为 project id,客户端传入,区分业务 <br>
* 4.再接下来4个bit为 instance id,HA 用的,支持最多16个instance。<br>
* 如果业务只需要“秒级粗略有序”,则可以多个 instance 同时使用, 不需要做任何特殊处理;<br>
* 如果业务需要“因果有序”,比如某个user短期内快速做的多个操作必须有因果顺序(程序化交易,必须先卖再买,几个毫秒内完成),
* 那么就需要做一些特殊处理,要么用 uid 做一致性hash,或者像一段时间内固定使用一个 instance <br>
* 5.最低16个bit是 sequence id,每个 instance 支持每秒 65535 个 id。bit数再大,redis 该成为瓶颈<br>
* 6.宕机不能自动立即重启,必须间隔1秒以上,避免 sequence id 重复导致 id 重复;迁移时必须先 kill 老的
* instance,再启动新的,也是为了避免 sequence id 重复。
*
* @param key
* 如果是全局ID,则共用Key,否则用业务本身的key,如获取用户的ID,则key为User
* @param pid
* 可以细分业务,如4bit 1-16
* @param instanceid,实例id,可以针对多个Redis,
* 4bit 1-16
* @return
*/
public Long getID(String key, int pid, int instanceid) {
Long id = 0L;
try {
Long idnum = redisDao.incr(key) % 65536;
Long sec = System.currentTimeMillis() - 1420041600;
id = sec * 16777216 + pid * 1048576 + instanceid * 65536 + idnum;
} catch (Throwable e) {
logger.error("Redis创建ID发生异常,将使用本地创建程序",e);
CrtIdGenerator crtIdGenerator = new CrtIdGenerator(0, 0);
id = crtIdGenerator.nextId();
}
return id;
}
public Long getID() {
Long id = 0L;
try {
Long idnum = redisDao.incr(DEFAULT_GLOBAL_KEY) % 65536;
Long sec = System.currentTimeMillis() - 1420041600;
id = sec * 16777216 + 1 * 1048576 + 1 * 65536 + idnum;
} catch (Exception e) {
logger.error("Redis创建ID发生异常,将使用本地创建程序",e);
CrtIdGenerator crtIdGenerator = new CrtIdGenerator(0, 0);
id = crtIdGenerator.nextId();
}
return id;
}
16 Redis管道技术pipeline
- Redis pipeline本质上是由客户端提供的技术,跟Redis服务器没直接联系。
- 通过调整传统C/S架构请求顺序,合并读写请求,大幅节省IO时间
17 缓存穿透&缓存击穿&缓存雪崩
17.1 缓存穿透
查询缓存和DB都未命中,黑客可以利用不存在的key攻击。
- 服务端接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 缓存空值,设置较短过期时间,最长不超过五分钟。
- 布隆过滤器(Bloom Filter)。将所有可能存在的数据存到bitmap中,一定不存在数据直接会被拦截,避免对底层存储系统的查询压力。
- 位数组bitmap + 无偏哈希函数,元素只有0和1,每个元素占1bit
- 添加key,k个哈希函数得到不同哈希值,将这些位置设为1,其他为0
- 布隆过滤器误判:只要有一个0就说明key不存在,如果都是1,也不一定说明key存在(不存在一定不存在,存在不一定真存在)
- 少许的误判来极大的节省空间
- 误判率计算公式:m位数组大小,k哈希函数个数,n是添加的元素个数
- 误判的优化:二进制数组,长度设置比较大
- 为什么会有误判:两个key经过同一哈希函数得到同样位置,将此位置置为1,但是不能说这两个key都是存在的。
17.2 缓存击穿
缓存未命中,DB命中,一般是key过期失效了,短时间大量请求查询过期失效的热key,进而转到查询DB,给DB造成巨大压力。
- 设置热点数据永远不过期。
- 互斥锁(Mutex),利用Redis的setnx命令。这种现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它;其他的线程走到这一步拿不到锁就等待,等第一个线程查询到了数据,然后更新缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
17.3 缓存雪崩
大量的原有缓存失效的请求都去查询DB,短时间内给DB造成了巨大的压力,造成数据库宕机,进而引发一系列连锁反应,造成整个系统崩溃。缓存击穿针对的是单个key,缓存雪崩针对的是多个key。
- 缓存集群HA:事先做好Redis缓存集群自身的高可用建设。
- 为key设置不同的缓存失效时间,随机值加盐。
- 设置热点数据永远不过期。
- 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
- 加锁排队:加锁或者队列或者分布式锁,并没有提高并发,其他线程阻塞,适合并发量不是很大场景
- 限流模式:请求超过阈值,后续直接返回
- 隔离模式:尽可能把雪崩控制在尽可能小的范围
- 本地缓存或二级缓存:一级缓存过期时间短,二级缓存过期时间长或者不过期,一级缓存失效后访问二级缓存,同时刷新一级缓存和二级缓存。抢到锁的线程才查询DB,否则查询二级缓存。
18 二级缓存怎么建设,第二级缓存为什么用本地缓存、两层都用分布式缓存不行吗?了解过业内哪些成熟框架?
19 缓存预热
系统上线前将相关的缓存数据直接加载到缓存系统的过程就是缓存预热。
哪些数据需要预热?怎么找出热点key?
- 已知的热点key
- Redis监控工具。比如cachecloud等。
- 客户端上报。改动 Redis SDK,记录每个请求,定时把收集到的数据上报,然后由一个统一的服务进行聚合计算。方案直观简单,但没法适应多语言架构,一方面多语言 SDK 对齐是个问题,另外一方面后期 SDK 的维护升级会面临比较大的困难,成本很高。最经典的nginx+lua上报到Kafka,然后统计出热点key。
- 代理层收集上报。如果所有的 Redis 请求都经过代理的话,可以考虑改动 Proxy 代码进行收集,思路与客户端基本类似。该方案对使用方完全透明,能够解决客户端 SDK 的语言异构和版本升级问题,不过开发成本会比客户端高些。
- Redis数据定时扫描。Redis 在 4.0 版本之后添加了 hotkeys 查找特性[1],可以直接利用 redis-cli --hotkeys 获取当前 keyspace 的热点 key,实现上是通过 scan + object freq 完成的。该方案无需二次开发,能够直接利用现成的工具,但由于需要扫描整个 keyspace,实时性上比较差,另外扫描耗时与 key 的数量正相关,如果 key 的数量比较多,耗时可能会非常长。
- Redis节点抓包解析。在可能存在热 key 的节点上(流量倾斜判断),通过 tcpdump 抓取一段时间内的流量并上报,然后由一个外部的程序进行解析、聚合和计算。该方案无需侵入现有的 SDK 或者 Proxy 中间件,开发维护成本可控,但也存在缺点的,具体是热 key 节点的网络流量和系统负载已经比较高了,抓包可能会情况进一步恶化。
通过redis的dump.rdb找出热key步骤
- 将redis的dump.rdb文件下载到本地(一般redis的持久化文件以rdb的方式存储,在redis配置文件可以找到dump.rdb的存储路径)。
- 用rdbtools工具生产内存报告,命令是 rdb -c memory,例子:
sudo rdb -c memory /redisfile/dump.rdb >test.csv
注意:rdb文件越大,生成时间越长。Rdbtools是以python语言开发的。GITHUP地址:https://github.com/sripathikrishnan/redis-rdb-tools/
- 内存报告生成后,结合用linux sort命令排序,根据內存列排序,找出最高的key有哪些。例子:
sudo sort -k4nr -t , test.csv > sort.txt
- 查看前1000个排序最高的数据
awk -F ',' '{print substr($3, 0,18)}' sort.txt | head -1000 | sort -k1 | uniq
- 查看sort.txt的结果,一般能得出类似‘my_rank_top’开头的集合占用最高,排在了前面。若要查看类似‘my_rank_top’开头的key总共占用了多少内存,可以用命令:
sudo cat sort.txt | grep ‘my_rank_top’ | awk -F ',' '{sum += $4};END {print sum}'
- 得知了my_rank_top这样的key占用最多内存,而且很可能是业务已经不再需要,但是长期在内存中没清理的,我们可以删除了这些集合。可以用模糊匹配key来删除,命令如下:
redis-cli -h 127.0.0.1 -p 6379 keys 'my_ranking_list*' | xargs redis-cli -h 127.0.0.1 -p 6379 del
另附:在本地启动redis加载dump.rdb文件时,一直load失败。搞了很长时间,终于找到原因:redis配置文件里databases要修改为256,本地默认是16,而产生原始dump.rdb的redis的databases就是25。
怎么预热
- 找出热点key --- 在DB查询出数据 --- 加载到Redis
20 缓存更新
- Redis自带缓存失效策略,maxmemory-policy
- volatile-lru:使用LRU算法移除key,只对设置了过期时间的键
- allkeys-lru:使用LRU算法移除key
- volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
- allkeys-random:移除随机的key
- volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
- noeviction:不进行移除。针对写操作,只是返回错误信息,del可以继续操作,读请求可以继续操作,Redis默认的淘汰策略
- 自定义缓存淘汰策略
- 定期清理过期的缓存,缺陷是需要维护大量的key
- 当请求过来先判断key是否过期,过期再去底层系统拿到新值更新缓存,缺陷是逻辑较第一种复杂
21 缓存降级
系统流量太大,保证核心服务可用,非核心服务可考虑降级。一些诸如加入购物车、支付等是不能降级的。
- 自动降级
系统根据一些关键数据指标,达到就自动降级 - 手动降级
配置热开关手动降级
22 缓存淘汰算法
- LRU
- 栈,大量内存拷贝,性能低,一般不适用
- 单链表,O(n),表头存最近最少使用的
- 哈希表+双向链表,O(1),利用前驱节点直接删除表头元素
- LRF
- LFU
- FIFO
- Random
23 LRU怎么实现?Redis是怎么实现的?
LRU常用哈希表+双向链表实现,表头是刚刚使用过得元素,表尾是不常用的元素会被踢掉,get/set在不冲突情况下可保证O(1)复杂度 也可通过双向链表保证LRU的删除/更新O(1)复杂度,过期时间可以维护一个线程,懒惰删除。但是redis不是这么实现的。
- LRU采用的是近似LRU,严格LRU需要额外存储前驱和后继节点信息,需要消耗更大内存;
- 近似LRU对每个key额外增加一个小字段,字段长度是24bit,存储最近一次LRU访问时间戳,通过随机采样懒惰删除方式,maxmemory-samples,默认是5,取样值越大近似LRU就越趋近严格LRU,但是性能损耗也会升高;
- Redis3.0后为了提升性能新增了淘汰池,淘汰池是一个数组,大小是maxmemory_samples,每一次淘汰循环,新的随机得出的key会跟淘汰池的key进行融合,淘汰掉最旧的一个key,保留剩余的key放入淘汰池留待下一个循环。
- 使用CONFIG SET maxmemory-samples <count>命令在生产环境上试验各种不同的采样大小值是很简单的。
24 缓存一致性解决方案(最终一致性)
24.1 重量级客户端
24.2 客户端数据库与缓存解耦
25 Redis实现消息队列
- 队列模式(list)
- lpush+rpop / rpush+lpop:非阻塞式
- lpush+brpop / rpush+blpop:阻塞式,推荐
- PubSub
- 不支持消息持久化
- 不支持消息多播机制
- Disque
- Redis作者单独开启的一个做多播消息队列的项目,一直还没发布
- Stream
- 2018.6,Redis5.0新增了Stream数据结构,实现了支持多播的可持久化消息队列。Redis Stream极大的借鉴了kafka的设计。
- 消息ID的形式是"整数-整数",timestampInMills-sequence,每个消息都有一个唯一的ID,后面加入的消息ID必须大于前面的消息ID
25.1 Redis队列使用场景
- 用于处理比较耗时的请求,例如批量发送邮件,如果直接在网页触发执行发送,程序会出现超时
- 高并发场景,当某个时刻请求瞬间增加时,可以把请求写入到队列,后台在去处理这些请求
- 抢购场景,先入先出的模式
25.2 redis的list作为队列存在的问题
- redis崩溃的时候队列功能失效
- 如果入队端一直在塞数据,而出队端没有消费数据,或者是入队的频率大而多,出队端的消费频率慢会导致内存暴涨
- Redis的队列也可以像rabbitmq那样,即可以做消息的持久化,也可以不做消息的持久化。当做持久话的时候,需要启动redis的dump数据的功能,暂时不建议开启持久化。Redis其实只适合作为缓存,而不是数据库或是存储。它的持久化方式适用于救急,不太适合当作一个普通功能来用。因为dump时候,会影响性能,数据量小的时候还看不出来,当数据量达到百万级别,内存10g左右的时候,非常影响性能。
- 假如有多个消费者同时监听一个队列,其中一个出队了一个元素,另一个则获取不到该元素。
- Redis的队列应用场景是一对多或者一对一的关系,即有多个入队端,但是只有一个消费端(出队)。
25.3 消息处理触发机制
- 死循环方式读取:易实现,故障时无法及时回复(适用于秒杀这种短时间的)
- 定时任务:压力均分,有处理上限(无论你队列前的系统,峰值多么不稳定,队列后的系统依然会定时执行)
案例:订单系统,下单后将订单信息写入队列后,立刻返回下单成功。配货系统每隔几分钟定时读取队列,对订单进行汇总处理 - 守护进程:类似于php-fpm和cgi,需要shell基础(用这个进程来检测,队列中是否有内容,有内容的话,启动出队系统进行处理)
26 Redis小对象压缩存储
- Redis如果使用32bit进行编译,内部所有数据结构所使用的的指针空间占用会少一半,可以节约大量内存。如果Redis使用内存不超过4GB,可以考虑使用32bit进行编译。
- 如果Redis内部管理的集合数据结构很小,会使用紧凑存储形式压缩存储。
- ziplist是一个紧凑的字节数组结构。
- intset是一个紧凑的整数数组结构,用于存放元素都是整数且元素个数较少的set集合。
- Redis支持set集合动态从unit16升级到unit32,再升级到unit64
- 如果set存储的是字符串,那么sadd立即升级为hashtable结构
- Redis规定的小对象存储结构限制条件,超过了就必须用标准结构存储:
- hash的元素个数超过512就必须用标准结构存储
- hash的任意元素长度超过64
- list元素个数超过512
- list任意元素长度超过64
- set的整数元素个数超过512
- zset的元素个数超过128
- zset任意元素长度超过64
27 Redis内存回收机制
- Redis并不是将空闲内存立即归还给操作系统,因为操作系统是以页为单位回收内存,只要页上面有一个key在使用,就不会被回收。
- 执行flushdb,内存立即被回收了,因为所有可以被删除了。
28 Redis内存分配算法
- Redis为了保持自身结构的简单性,将内存分配的细节交给第三方内存分配库去实现。
- Redis默认使用Facebook的jemalloc库管理内存,也可以切换到Google的tcmalloc库,但是jemalloc性能比tcmalloc要稍好一些。
- 通过info memory指令可以看到Redis的mem_allocator使用了jemalloc
29 Redis info指令
- info指令分为9大块,可以一次性获取所有信息,也可以分模块获取信息
- info,获取所有信息
- info memory,获取内存相关信息
Server:服务器运行的环境参数
Clients:客户端相关信息
- 客户端连接数:info clients
Memory:服务器运行内存统计数据
- 查看内存占用:info memory | grep used | grep human
Persistence:持久化信息
Stats:通用统计数据
- Redis每秒执行多少次指令:info stats | grep ops
- 超出最大连接数限制而被拒绝的客户端连接次数:info stats |grep reject
- 主从半同步复制失败次数:info stats | grep sync
Replication:主从复制相关信息
- 查看主从复制信息:info replication
- 查看复制积压缓冲区大小:info replication | grep backlog
CPU:CPU使用情况
Cluster:集群信息
KeySpace:键值对统计数量信息
30 Redis过期策略
- 定期删除 + 懒惰删除
- 过期的key放入独立的字典中,定时遍历字典删除过期的key
- 懒惰删除就是客户端访问这个key时候检查是否过期,过期立即删除
- 定期扫描策略
- Redis默认每秒进行10次过期扫描,扫描时间上线是25ms,不会遍历过期字典所有的key,而是采用贪心策略
- 从过期字典随机选出20个key
- 删除这20个key中已经过期的key
- 如果过期的key的比例超过了1/4,那就重复继续随机选出20个key
- 如果大批量key同时过期会出现卡顿现象,因为内存管理器需要频繁回收内存页,会产生一定的CPU消耗
- Redis默认每秒进行10次过期扫描,扫描时间上线是25ms,不会遍历过期字典所有的key,而是采用贪心策略
- 从节点的过期策略
- 从节点不会进行过期扫描,从节点对过期的处理的被动的,主节点在key到期时会在AOF文件写入一条del指令,同步到所有从节点,从节点收到后执行del指令删除过期的key
31 Redis懒惰删除
- Redis的del指令很快,如果key很大,del操作就会导致单线程卡顿,Redis 4.0引入了unlink指令,对del操作懒处理,交给后台线程异步回收内存。
- Redis提供的flushdb和flushall指令,用来清空数据库,也是极其缓慢的操作,Redis 4.0在指令后增加了async参数,清空操作交给后台线程慢慢处理。
- flushdb async
- flushall async
- AOF sync也很慢,也是通过异步线程才操作,跟懒惰删除线程不是同一个,有一个专属于自己的任务队列,队列里面只放AOF sync任务
- Redis 4.0提供了很多异步删除机制:
- slave-lazy-flush:从节点接收完rdb文件后的flush操作
- lazyfree-lazy-eviction:内存达到maxmemory进行淘汰
- lazyfree-lazy-expire key:过期删除
- lazyfree-lazy-server-del rename:指令删除的destKey
32 Redis安全机制
- 危险指令
- keys
- flushdb
- flushall
- 对危险指令改键
- 在redis.conf的security模块加入rename-command keys [自定义键值]
- 完全封杀一条指令:rename成为空串,比如rename-command flushall “”
- 密码控制
- 客户端使用auth指令
- 从节点使用masterauth
- Redis以普通用户身份启动,即使有恶意攻击,也拿不到root权限
- Redis SSL代理官方推荐用spiped工具,spiped进程需要成对出现,相互之间需要使用相同的共享密钥来加密消息。
33 Redis有哪些时间复杂度O(n)的指令,应该怎么处理这些指令,有什么改进措施?
时间复杂度为O(n)的指令
- keys *
- flushdb
- flushall
- config set,动态调整Redis服务器配置,无需重启
- mget
- lrange
- hgetall
- smembers
- zrange
怎么处理这些指令
- 改键
redis.conf中security模块执行rename-command keys abcd - 禁用
redis.conf中security模块执行rename-command keys ""
改进优化措施
- Redis2.8推出了scan指令,分批次扫描Redis记录,会导致查询耗时加大,但是不会引起Redis卡顿。
34 Redis集群方案
- 客户端分区方案
- Redis Sharding,Jedis支持
- 代理分区方案
- Twemproxy
- Codis
- 查询路由方案
- Redis Cluster
35 数据分片方式
- 顺序分片 / 连续分片
- Bigtable
- HBase
- Hypertable
- 随机分片 / 哈希分片
- 哈希取余分片
- 一致性哈希分片
- 虚拟槽分片:Redis Cluster,Dynamo
36 Redis Cluster中的槽位数为什么是16384?
- 如果槽位是65536,发送心跳包的消息头达8KB,发送的心跳包过于庞大
- CRC16算法产生的哈希值有16bit,可以产生2^16 =65536个值,为什么取模不选择65536,要选择16384=2^14呢?
- Redis节点每秒都在发送ping消息作为心跳包,如果槽位是65536,消息头的大小为65536/8/1024=8KB,ping消息消息头过大,浪费带宽。如果槽位是16384,消息头大小是2KB。
- Redis集群主节点数量基本上不会超过1000个
- Redis作者建议Redis Cluster节点数最好不要超过1000个,会导致网络拥堵。
- 槽位越小,节点少的情况下,bitmap压缩率高
- 哈希槽是通过一张bitmap来保存的,在传输过程中,会对bitmap进行压缩。槽位越小,节点少的情况下,压缩率高。
37 CRC算法(CRC8&CRC16&CRC32&CRC64)
- CRC,Cyclic Redundancy Check,循环冗余校验码,是数据通信领域最常用的一种查错校验码,对网络传输数据进行多项式计算,保证数据的正确性和完整性。
- CRC8,CRC16....,8,16等代表多项式最高此项的幂,最高次幂相同,不同的标准有不同的CRC算法
- Redis使用的是CRC-16-CCITT标准,XMODEM协议使用的CRC标准。
- CRC算法要校验的数据位是8位,CRC算法的参数只有256种可能,事先可以把这256种计算出来放到数组,查询时候时间复杂度为O(n)。
- CRC16每个元素都是uint16_t类型(unsigned short int),CRC64每个元素都是unit64_t类型(unsigned long int)
- MySQL中的crc32()返回值范围是0~2^32-1,生成整型数据用bigint存储
- crc32()缺陷是容易发生碰撞(相比MD5),crc64()对其进行了优化,crc64()底层机制也是MD5。
38 Redis中key和value的存储大小限制?
- key最大可以存储512M
- String类型的value最大可以存储512M
- List、Hash、Set、Zset的value最大存储元素个数是2^32-1
39 Redis大key优化方案
39.1 Redis大key发生背景
大key发生场景
- 热门话题下评论、答案排序场景。
- 大V的粉丝列表。
- 使用不恰当,或者对业务预估不准确、不及时进行处理垃圾数据等。
大key导致的问题
由于Redis主线程为单线程模型,大key也会带来一些问题,如:
- 集群模式在slot分片均匀情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS高。
- 大key相关的删除或者自动过期时,会出现qps突降或者突升的情况,极端情况下,会造成主从复制异常,Redis服务阻塞无法响应请求。大key的体积与删除耗时可参考下表:
key类型 field数量 耗时
Hash 100万 1000ms
List 100万 1000ms
Set 100万 1000ms
Sorted Set 100万 1000ms
39.2 Redis大key发现与删除机制
Redis 4.0之前的大key的发现与删除方法:
- redis-rdb-tools工具。redis实例上执行bgsave,然后对dump出来的rdb文件进行分析,找到其中的大KEY。
- redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。
- 自定义的扫描脚本,以Python脚本居多,方法与redis-cli --bigkeys类似。
- debug object key命令。可以查看某个key序列化后的长度,每次只能查找单个key的信息。官方不推荐。
Redis 4.0之后的大key的发现与删除方法
- memory usage命令
- lazyfree机制
39.3 Redis大key的处理方案
- 拆分。
- 减少key个数或分桶。
- 预防。
拆分key
- 单个key存储大value且每次都是整存整取
这种操作一般都是每次整存整取,这种情况可以尝试将对象拆分成多个key-value,使用multiGet获取值,这样分拆意义在于分拆操作的压力,将操作压力平摊到多个redis实例,降低对于单个redis的io压力。 - 单个key存储大value且每次只存取部分数据
同样可以拆成几个key-value,也可以将这些存储在一个hash中,每个field代表具体属性,使用hget,hmget来获取部分value,使用hset,hmset来更新部分属性。 - hash,set,zset,list中存储过多数据
同样可以将这部分元素拆分,以hash为例,正常的流程是:hget(hashKey, field);hset(hashKey, field, value)。现在可以固定一个桶数量,比如1w,每次存取的时候,先在本地计算field的hash值,对1w取模,确定field落在哪个key上。
set,zset,list做法类似。
减少key个数或分桶
一个集群存储了上亿的key,如果key个数过多会带来更多的内存占用空间:
- key本身占用。
- 集群中,服务端会建立slot2key的映射关系,这种指针占用在key多的情况下存在巨大的空间浪费,在key上亿时,内存消耗十分明显。
减少key个数可以减少对内存的消耗,可以参考hash结构存储,将多个key存储在一个hash结构中。
组合那些key本身强相关性的,比如key代表一个对象,m每个key是对象的一个属性,按照这种方式设置一个新的key-hash的结构,原先的key作为这个新hash field。如果key本身没有相关性,可以预估总量,对一些场景预分固定的桶数量。
比如有2亿key,按照一个hash存储100个field来算,需要200w个桶,这样可以按照200w个固定桶数量做取模,hash(123456) % 200w,比如算出3个key的桶分别是1,2,3。调用存储api hset(key, field, value),读取时用hget(key,field)。建议分桶数量100合适。
Bitmap和Bloom拆分
使用Bloom的场景往往是数据量极大的情况,这种情况下,bitmap和bloom使用空间比较大。
如果bitmap比较大,可以拆分成多个小的bitmap,可以通过结合hash方式,将key路由到hash上对应的bitmap上,将不同的key分配给不同的bitmap,而不是所有小的bitmap当作一个整体,这样每次请求都只要取redis中的一个key即可。