8. Interview-Redis
1 Redis数据结构及适用场景
- String
- SDS
- 缓存用户信息,json字符串(一次性全部序列化)
- List
- ziplist,quicklist
- LinkedList,异步队列,确保顺序
- 利用lrange命令做基于Redis的分页功能
- Hash
- HashMap,数组+链表
- 缓存用户信息,不用一次性全部序列化,可以对每个字段单独存储,部分获取节省网络流量
- Set
- HashSet,无序唯一,可用于去重
- 缓存中奖的用户ID,因为有去重功能,可以保证同一用户不会中奖两次
- 去重
- Zset
- HashMap+SortedSet,跳跃列表skiplist
- 排行榜
- top k
- 缓存粉丝列表,value是粉丝的用户ID,score是关注时间,可以按关注时间排序
- 缓存学生成绩,value是学生的ID,score是他的成绩,可以按成绩排名
- 延时任务
- 范围查找
Redis-StringSDS,Simple Dynamic String,简单动态字符串。
Redis-链表双向链表
Redis-字典字典/符号表/关联数组/map
Redis-整数集合整数集合intset,保证集合中不出现重复元素,节省内存
Redis-压缩列表压缩列表ziplist
快速列表quicklist=ziplist+linkedlist
- 链表的前驱和后继一共要占据16字节,可以继续优化
- linkedlist分段,每段用ziplist,多哥ziplist之间使用双向指针串起来
Redis-跳跃表跳跃表skiplist-zset
- 二分查找,O(n)降到O(lg(n))
紧凑列表listpack
- Redis5.0引入的listpack,设计目的是替代ziplist
- 尾部存放当前元素的长度,最后一个元素的位置可以通过总字节数和本身长度算出来,比ziplist节省一个标记最后一个元素位置的字段zltail_offset
基数树,有序字典树,rax
- zset是按照score排序,rax是按照key排序
- rax被用在Redis Stream里面存储消息队列,快速定位消息ID
2 Redis到底是单线程还是多线程?Redis单线程模型?
- Redis6.0以前是单线程,像node.js、Nginx等都是单线程模型,但是都是服务器高性能的典范,使用Redis指令要特别注意时间复杂度为O(n)的指令,一不小心就会导致Redis卡顿;
- Redis6.0开始引入了多线程
- Redis的性能瓶颈是网络I/O操作,redis多线程部分是处理网络读写及协议解析;
- Redis用del指令删除key,如果key很大,就需要多线程异步非阻塞的删除,释放内存减少对主线程阻塞的时间,提高执行效率。
3 Redis有多快?
C语言编写,官方QPS,每秒读11w,写8w
4 Redis为什么快?底层怎么实现的?
- 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内存,6位系统不限制内存。
- 如果设置了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实现发号器
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过期了,大量请求查询DB,给DB造成巨大压力。
- 设置热点数据永远不过期。
- 互斥锁(Mutex)。
17.3 缓存雪崩
大量的原有缓存失效的请求都去查询DB,短时间内给DB造成了巨大的压力,造成数据库宕机,进而引发一系列连锁反应,造成整个系统崩溃。缓存击穿针对的是单个key,缓存雪崩针对的是多个key。
- 为key设置不同的缓存失效时间,随机值加盐。
- 设置热点数据永远不过期。
- 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
- 加锁排队:加锁或者队列或者分布式锁,并没有提高并发,其他线程阻塞,适合并发量不是很大场景
- 限流模式:请求超过阈值,后续直接返回
- 隔离模式:尽可能把雪崩控制在尽可能小的范围
- 二级缓存:一级缓存过期时间短,二级缓存过期时间长或者不过期,一级缓存失效后访问二级缓存,同时刷新一级缓存和二级缓存。抢到锁的线程才查询DB,否则查询二级缓存。
18
19 缓存预热
系统上线前将相关的缓存数据直接加载到缓存系统的过程就是缓存预热。
- 哪些数据需要预热(其实就是找出热点key)
- 已知的热点key
- Redis监控工具
- 客户端上报,nginx+lua上报到Kafka,然后统计出热点key
- 怎么预热
- 找出热点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实现消息队列
- PubSub
- 不支持消息持久化
- 不支持消息多播机制
- Disque
- Redis作者单独开启的一个做多播消息队列的项目,一直还没发布
- Stream
- 2018.6,Redis5.0新增了Stream数据结构,实现了支持多播的可持久化消息队列。Redis Stream极大的借鉴了kafka的设计。
- 消息ID的形式是"整数-整数",timestampInMills-sequence,每个消息都有一个唯一的ID,后面加入的消息ID必须大于前面的消息ID
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,可以产生216=65536个值,为什么取模不选择65536,要选择16384=214呢?
- 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算法 CRC算法2- 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使用场景
-
数据库
-
缓存
缓存热点数据,结合过期时间expire,然后进行缓存更新的操作。用户登录注册时候验证码、用户名存储。 -
限时业务
限时优惠活动、手机验证码等 -
计数器相关应用
redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。 -
排行榜问题
关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。
在奶茶活动中,我们需要展示各个部门的点赞排行榜, 所以我针对每个部门做了一个SortedSet,然后以用户的openid作为上面的username,以用户的点赞数作为上面的score, 然后针对每个用户做一个hash,通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息,这个当时在实际运用中性能体验也蛮不错的。 -
5、分布式锁
这个主要利用redis的setnx命令进行,setnx:"set if not exists"就是如果不存在则成功设置缓存同时返回1,否则返回0 ,这个特性在俞你奔远方的后台中有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock,如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。 当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间 就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。
当然我们可以将这个特性运用于其他需要分布式锁的场景中,结合过期时间主要是防止死锁的出现。
6、延时操作
这个目前我做过相关测试,但是还没有运用到我们的实际项目中,下面我举个该特性的应用场景。 比如在订单生产后我们占用了库存,10分钟后去检验用户是够真正购买,如果没有购买将该单据设置无效,同时还原库存。 由于redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。 所以我们对于上面的需求就可以用以下解决方案,我们在订单生产时,设置一个key,同时设置10分钟后过期, 我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。 当然我们也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。
7、分页、模糊搜索
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查询更利于系统的性能保障。
8、点赞、好友等相互关系的存储
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。 又或者在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
这个在奶茶活动中有运用,就是利用set存储用户之间的点赞关联的,另外在点赞前判断是否点赞过就利用了sismember方法,当时这个接口的响应时间控制在10毫秒内,十分高效。
9、队列/消息队列
由于redis有list push和list pop这样的命令,所以能够很方便的执行队列操作。
lpush/rpop,rpush/lpop
-
过滤器
scrapy-redis作为分布式的爬虫框架,便是使用了 redis 的 Set 这个数据结构来对将要爬取的 url 进行去重处理。 -
六、频率控制
项目的社区功能里,不可避免的总是会遇到垃圾内容,一觉醒来你会发现首页突然会被某些恶意的帖子和广告刷屏了,如果不采取适当的机制来控制就会导致用户体验受到严重的影响
控制广告垃圾贴的策略很多,高级一点的可以通过AI,最简单的方式是通过关键词扫描,还有比较常用的一种方式是频率控制,限制单个用户内容的生产速度,不通等级的用户会有不同的频率控制参数
频率控制就可以使用redis来实现,我们将用户的行为理解为一个时间序列,我们要保证在一定的时间内限制单个用户的时间序列的长度,超过这个长度就禁止用户的行为,它可以是用redis的zset(有序集合,zset详解)来实现
图中绿色的部门就是我们要保留的一个时间段的时间序列信息,灰色的段会被砍掉。统计绿色段中时间序列记录的个数就知道是否超过了频率的阈值。
- 位图
项目里需要做一个球队成员的签到系统,当用户量比较少的时候,设计上比较简单,就是将用户的签到状态用redis的hash结构来存储,签到一次就再hash结构里记录一条,签到有三种状态:未签到,已签到和部签到,分别是0,1,2三个整数值
hset sign:(user_id) 2019-08-12 0
hset sign:$(user_id) 2019-08-14 2
1
2
3
4
这个其实非常浪费用户空间,后来想做全部用户的签到,技术leader指出,这时候的再用hash就有问题了,他讲到当用户过千万的时候,内存可能会飚到 30G+,我们线上实例通常过了 20G 就开始报警,30G 已经属于严重超标了。
这时候我们就开始着手解决这个问题,去优化存储。我们选择使用位图来记录签到信息,一个签到状态需要两个位来记录,一个月的存储空间只需要 8 个字节。这样就可以使用一个很短的字符串来存储用户一个月的签到记录。
但是位图也有一个缺点,它的底层是字符串,字符串是连续存储空间,位图会自动扩展,比如一个很大的位图 8m 个位,只有最后一个位是 1,其它位都是零,这也会占用1m 的存储空间,这样的浪费非常严重。
所以呢就有了咆哮位图这个数据结构,它对大位图进行了分段存储,全位零的段可以不用存。
另外还对每个段设计了稀疏存储结构,如果这个段上置 1 的位不多,可以只存储它们的偏移量整数。这样位图的存储空间就得到了非常显著的压缩。
-
模糊计数
上面提到的签到系统,如果产品经理需要知道这个签到的日活月活怎么办呢?
通常我们会直接甩锅——请找数据部门。
但是数据部门的数据往往不是很实时,经常前一天的数据需要第二天才能跑出来,离线计算是通常是定时的一天一次。那如何实现一个实时的活跃计数?
最简单的方案就是在 Redis 里面维护一个 set 集合,来一个用户,就 sadd 一下,最终集合的大小就是我们需要的 UV 数字。
但是这个空间浪费严重怎么办?这时候就需要使用redis提供的HyperLogLog模糊计数功能,它是一种概率计数,有一定的误差,大约是0.81%。
但是空间占用很小,其底层是一个位图,它最多只会占用12k的存储空间,而且在计数值比较小的时候,位图使用稀疏存储,空间占用就更小了。 -
布隆过滤器
-
排行榜:得分最高的人排名第一,例如高点击率,活跃度,最高销售数量,投票数最高的前10名,等等。
-
使用Redis进行会话缓存是很常见的情况。使用Redis在其他存储上缓存会话的优点是Redis提供了持久性。目前,很多解决方案都采用Redis作为会话存储解决方案。