8.|8. Interview-Redis
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源码看过吗?
【8.|8. Interview-Redis】Content1 Redis适用场景?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是他的成绩,可以按成绩排名
- 延时任务
- 范围查找
- 优先队列,给每条消息加一个唯一的序号
- Redis6.0以前是单线程,像node.js、Nginx等都是单线程模型,但是都是服务器高性能的典范,使用Redis指令要特别注意时间复杂度为O(n)的指令,一不小心就会导致Redis卡顿;
- Redis6.0开始引入了多线程
- Redis的性能瓶颈是网络I/O操作,redis多线程部分是处理网络读写及协议解析;
- Redis用del指令删除key,如果key很大,就需要多线程异步非阻塞的删除,释放内存减少对主线程阻塞的时间,提高执行效率。
SDS,Simple Dynamic String,简单动态字符串。
文章图片
Redis-String
双向链表
文章图片
Redis-链表
字典/符号表/关联数组/map
文章图片
Redis-字典
整数集合intset,保证集合中不出现重复元素,节省内存
文章图片
Redis-整数集合
压缩列表ziplist
文章图片
Redis-压缩列表
快速列表quicklist=ziplist+linkedlist
- 链表的前驱和后继一共要占据16字节,可以继续优化
- linkedlist分段,每段用ziplist,多个ziplist之间使用双向指针串起来
跳跃表skiplist-zset
文章图片
Redis-跳跃表
- 二分查找,O(n)降到O(lg(n))
紧凑列表listpack
- Redis5.0引入的listpack,设计目的是替代ziplist
- 尾部存放当前元素的长度,最后一个元素的位置可以通过总字节数和本身长度算出来,比ziplist节省一个标记最后一个元素位置的字段zltail_offset
基数树,有序字典树,rax
- zset是按照score排序,rax是按照key排序
- rax被用在Redis Stream里面存储消息队列,快速定位消息ID
- 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参数。
文章图片
IO多路复用-select
文章图片
IO多路复用-epoll 5 讲一下Redis的I/O多路复用中的Reactor设计模式
- 在高性能的I/O设计中,有两个著名的模式
- Reactor模式,用于同步I/O操作;
- Proactor模式,用于异步I/O操作。
- Reactor设计模式又叫做响应器模式,通常用于NIO非阻塞IO的通信框架中,比如说Netty。
- Reactor是事件驱动机制,应用程序需要提供相应的接口并注册到Reactor上,如果有相应事件发生,Reactor将主动调用回调函数。
- 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可以配置。
- 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之前盯住关键变量,否则会出错。
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
实现方案
- 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实例,这些实例之间相互独立,没有主从关系
文章图片
Redisson分布式锁原理
常见问题及解决方案
- 死锁
目的要让setnx和setex两个命令成为原子性操作- Lua脚本
- Redis2.8实现了setnx和setex原子性操作,比如RedisTemplate
- 业务时间大于锁超时时间
- Lua脚本
- 可重入锁
- A加锁后服务宕机,恢复后可重复获得锁,验证当前锁的内容是否匹配即可。
分布式锁三种实现方案
- 数据库锁
- 乐观锁
- 主键ID,简单但是问题多
- 版本号,侵入性强,每个表都要加version字段
- 悲观锁,for update
- 乐观锁
- redis实现分布式锁
- setnx(key,value)+expire()
- Redission
- Redlock
- ZooKeeper实现分布式锁
- 有序临时节点,最小的有序节点获得锁,使用完就消失,通过watch监控节点变化,继续找出最小的有序节点获得锁,依次继续。
分布式锁三种实现方式特点
- 基于数据库实现
- 优点:实现方式简单易懂
- 缺点:性能差,有锁表风险,占用CPU资源多
- 基于Redis实现
- 优点:基于缓存的实现性能高
- 缺点:依赖Redis中间件HA,过期时间不好控制,牺牲了一定的可靠性
- 基于zookeeper实现
- 优点:不需要控制过期时间,可靠性高
- 缺点:依赖zookeeper中间件,leader选举过程中服务不可用,性能较Redis实现低
分布式锁三种实现比较
- 难易:Zookeeper >= 缓存 > 数据库
- 性能:缓存 > Zookeeper >= 数据库
- 可靠性:Zookeeper > 缓存 > 数据库
- 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
- Replication
- Sentinel
- Codis
- slot=1024,crc32(key) % 1024
- Twemproxy
- MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
- Cluster
- slot=16384,crc16(key) % 16384
- redis.conf中maxmemory参数设置最大内存,单位是bytes,一般设置为机器物理内存的四分之三。
- 达到最大内存redis首先会清除已到期或即将到期的key,清理后仍然达到最大内存,将无法写入,写入会报错,OOM command not allowed when used memory > 'maxmemory',但是可以读操作。
- 如果maxmemory没有设置或者设置为0,32位系统最多使用3GB内存,64位系统不限制内存。
- 如果设置了maxmemory,最好要设置过期策略,比如说maxmemory-policy volatile-lru
- 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.
/**
* 1.id 总长度 52bit,为了兼容 js,php,flex 等语言 long 类型最长只能 52 bit
* 2.最高 28 bit 为秒级时间戳,因为位数限制,不能从 1970.1.1 开始,-1420041600 表示从 2015.1.1
* 开始,大约可以使用10年(3106天)
* 3.接下来4个bit为 project id,客户端传入,区分业务
* 4.再接下来4个bit为 instance id,HA 用的,支持最多16个instance。
* 如果业务只需要“秒级粗略有序”,则可以多个 instance 同时使用, 不需要做任何特殊处理;
* 如果业务需要“因果有序”,比如某个user短期内快速做的多个操作必须有因果顺序(程序化交易,必须先卖再买,几个毫秒内完成),
* 那么就需要做一些特殊处理,要么用 uid 做一致性hash,或者像一段时间内固定使用一个 instance
* 5.最低16个bit是 sequence id,每个 instance 支持每秒 65535 个 id。bit数再大,redis 该成为瓶颈
* 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时间
- 服务端接口层增加校验,如用户鉴权校验,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,否则查询二级缓存。
哪些数据需要预热?怎么找出热点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 6379keys '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
- 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是否过期,过期再去底层系统拿到新值更新缓存,缺陷是逻辑较第一种复杂
- 自动降级
系统根据一些关键数据指标,达到就自动降级 - 手动降级
配置热开关手动降级
- LRU
- 栈,大量内存拷贝,性能低,一般不适用
- 单链表,O(n),表头存最近最少使用的
- 哈希表+双向链表,O(1),利用前驱节点直接删除表头元素
- LRF
- LFU
- FIFO
- Random
- LRU采用的是近似LRU,严格LRU需要额外存储前驱和后继节点信息,需要消耗更大内存;
- 近似LRU对每个key额外增加一个小字段,字段长度是24bit,存储最近一次LRU访问时间戳,通过随机采样懒惰删除方式,maxmemory-samples,默认是5,取样值越大近似LRU就越趋近严格LRU,但是性能损耗也会升高;
- Redis3.0后为了提升性能新增了淘汰池,淘汰池是一个数组,大小是maxmemory_samples,每一次淘汰循环,新的随机得出的key会跟淘汰池的key进行融合,淘汰掉最旧的一个key,保留剩余的key放入淘汰池留待下一个循环。
- 使用CONFIG SET maxmemory-samples
命令在生产环境上试验各种不同的采样大小值是很简单的。
文章图片
写缓存
文章图片
读缓存 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
- 用于处理比较耗时的请求,例如批量发送邮件,如果直接在网页触发执行发送,程序会出现超时
- 高并发场景,当某个时刻请求瞬间增加时,可以把请求写入到队列,后台在去处理这些请求
- 抢购场景,先入先出的模式
- redis崩溃的时候队列功能失效
- 如果入队端一直在塞数据,而出队端没有消费数据,或者是入队的频率大而多,出队端的消费频率慢会导致内存暴涨
- Redis的队列也可以像rabbitmq那样,即可以做消息的持久化,也可以不做消息的持久化。当做持久话的时候,需要启动redis的dump数据的功能,暂时不建议开启持久化。Redis其实只适合作为缓存,而不是数据库或是存储。它的持久化方式适用于救急,不太适合当作一个普通功能来用。因为dump时候,会影响性能,数据量小的时候还看不出来,当数据量达到百万级别,内存10g左右的时候,非常影响性能。
- 假如有多个消费者同时监听一个队列,其中一个出队了一个元素,另一个则获取不到该元素。
- Redis的队列应用场景是一对多或者一对一的关系,即有多个入队端,但是只有一个消费端(出队)。
- 死循环方式读取:易实现,故障时无法及时回复(适用于秒杀这种短时间的)
- 定时任务:压力均分,有处理上限(无论你队列前的系统,峰值多么不稳定,队列后的系统依然会定时执行)
案例:订单系统,下单后将订单信息写入队列后,立刻返回下单成功。配货系统每隔几分钟定时读取队列,对订单进行汇总处理 - 守护进程:类似于php-fpm和cgi,需要shell基础(用这个进程来检测,队列中是否有内容,有内容的话,启动出队系统进行处理)
- 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
- Redis并不是将空闲内存立即归还给操作系统,因为操作系统是以页为单位回收内存,只要页上面有一个key在使用,就不会被回收。
- 执行flushdb,内存立即被回收了,因为所有可以被删除了。
- Redis为了保持自身结构的简单性,将内存分配的细节交给第三方内存分配库去实现。
- Redis默认使用Facebook的jemalloc库管理内存,也可以切换到Google的tcmalloc库,但是jemalloc性能比tcmalloc要稍好一些。
- 通过info memory指令可以看到Redis的mem_allocator使用了jemalloc
- 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
- 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
- 危险指令
- keys
- flushdb
- flushall
- 对危险指令改键
- 在redis.conf的security模块加入rename-command keys [自定义键值]
- 完全封杀一条指令:rename成为空串,比如rename-command flushall “”
- 密码控制
- 客户端使用auth指令
- 从节点使用masterauth
- Redis以普通用户身份启动,即使有恶意攻击,也拿不到root权限
- Redis SSL代理官方推荐用spiped工具,spiped进程需要成对出现,相互之间需要使用相同的共享密钥来加密消息。
时间复杂度为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卡顿。
- 客户端分区方案
- Redis Sharding,Jedis支持
- 代理分区方案
- Twemproxy
- Codis
- 查询路由方案
- Redis Cluster
- 顺序分片 / 连续分片
- Bigtable
- HBase
- Hypertable
- 随机分片 / 哈希分片
- 哈希取余分片
- 一致性哈希分片
- 虚拟槽分片:Redis Cluster,Dynamo
- 如果槽位是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进行压缩。槽位越小,节点少的情况下,压缩率高。
文章图片
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。
- key最大可以存储512M
- String类型的value最大可以存储512M
- List、Hash、Set、Zset的value最大存储元素个数是2^32-1
大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
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机制
- 拆分。
- 减少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即可。
40 Redis源码看过吗?
推荐阅读
- springboot使用redis缓存
- (1)redis集群原理及搭建与使用(1)
- springboot结合redis实现搜索栏热搜功能及文字过滤
- Redis——发布订阅/消息队列
- redis|redis 常见问题一
- 实操Redission|实操Redission 分布式服务
- 二、Redis的五种常用数据类型
- 深入理解redis——布隆过滤器BloomFilter
- redis哨兵模式
- 关于分布式锁|关于分布式锁 Redis 与 Zookeeper 的原理,它们如何实现分布式锁()