修订记录 | 时间 |
---|---|
首次发布 | 2023.06 |
一、基础
1.1 数据结构
1. 常见Redis数据结构
String、Hash、List、Set、SortedSet(ZSet)。
2. String
底层实现:SDS。编码方式可能是RAW(大多数)、EMBSTR(长度小于44字节)和INT(整数值)。
使用场景:单值缓存、对象缓存、分布式锁、计数器(文章阅读量)。
3. Hash
底层实现:默认ZipList,以节省内存,当数据量大时会变成Dict(HT)。
使用场景:电商购物车。
问:知道渐进式rehash吗?
4. List
List结构类似于一个双端链表,可以从首、尾操作元素。
底层实现:3.2版本之后是QuickList。
使用场景:栈、消息队列。
5. Set
Set是Redis中的集合,元素唯一。
底层实现:Dict字典,采用HT编码,(即哈希表)。Dict中key存储元素,value为null。当存储的都是整数切不超过某一限值时采用IntSet编码,以节省内存。
使用场景:抽奖、朋友圈点赞、微博关注模型(共同关注、可能认识)。
6. SortedSet
SortedSet是一个可排序的Set集合,每个元素带有一个score属性,可以基于score进行排序。
特性:可排序、元素不重复、查询速度快。
底层实现:跳表SkipList + Dict(HT编码),即跳表+哈希表。
使用场景:排行榜。
1.2 缓存
1. 缓存击穿及解决方法
缓存击穿
给某一个key设置了过期时间,当key过期时恰好有对这个key的大量并发请求过来,这些请求都会直接查数据库,把数据库击垮。
解决方案
- 互斥锁
发现缓存未命中之后,获取互斥锁(比如redis中的setnx),开始查询数据库并写入缓存,释放互斥锁。此时如果有另外一个请求查询缓存,则获取不到锁,定时重试get缓存的方法。这个方案可以保证数据的强一致性,但性能较差。 - 逻辑过期
热点key不设置过期时间,但在存储内容中增加逻辑过期时间字段。查询缓存时,先校验逻辑过期时间,如果过期则获取互斥锁,返回过期数据。同时开启新线程查询数据库更新缓存,更新完后释放互斥锁。这个方案高可用,性能更好,但可能数据不一致。
2. 缓存穿透及解决方法
缓存穿透
查询一个不存在的数据,mysql中也查询不到,也不会写入缓存,导致每次请求都会查数据库。这种情况大概率是遭到了外部攻击。
解决方案
- 缓存空数据,查询返回为空,缓存空结果。
优点:简单。
缺点:消耗内存,可能后续会发生不一致的问题。 - 使用布隆过滤器
查询布隆过滤器中不存在则直接返回,缓存预热时需要预热布隆过滤器。
布隆过滤器使用bitmap位图,存储数据时会根据多个哈希函数计算哈希值,将bitmap上对应的位置改为1。查询数据时使用相同的多个哈希函数计算哈希值,去bitmap对应位置查看是否都为1,若不是则数据不存在。
如果有哈希冲突,则可能存在误判。bitmap越小误判率越大,Redisson和Guava中都可以设置误判率,一般设置为5%。
优点:内存占用较少,没有多余key。
缺点:实现复杂,存在误判。
3. 缓存雪崩及解决方法
缓存雪崩
在同一段时间大量的缓存key同时过期或Redis服务宕机,大量的请求到达数据库,给数据库带来很大的压力。
解决方案
- 给不同key的过期时间上增加随机值
添加1-5分钟随机值,避免同时过期。 - 利用Redis集群提高服务的可用性
哨兵模式、集群模式 - 给缓存业务添加降级限流策略
Nginx或Spring Cloud Gateway - 给业务添加多级缓存
Guava或Caffeine
1.3 持久化
RDB(Redis数据备份文件)、AOF。
1. RDB
RDB全称Redis Database Backup File,数据备份文件,也叫做数据快照。就是把内存中的所有数据记录到磁盘中,当实例故障重启后,从磁盘中读取快照文件,恢复数据。默认开启。
1.1 bgsave流程:
- 开始时会fork主进程,得到一个子进程,子进程共享主进程的内存数据。fork时会阻塞主进程。
- 完成fork之后子进程读取内存数据并写入新的RDB文件。
- 用新的RDB文件替换旧的RDB文件。
1.2 RDB在什么时候执行?
- 默认服务停止时执行一次save,即主进程进行持久化。
- 可以在redis.conf中配置save seconds changes参数,来修改RDB触发的频率。达到save条件时会自动触发bgsave后台进行异步持久化。例如save 60 1000是说在60s内至少执行1000次修改则会触发一次RDB。
1.3 RDB的缺点
- RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险。
- fork子进程、压缩、写出RDB文件都比较耗时,可能十几秒或者几十秒。
2. AOF
AOF全称是Append Only File,追加文件。Redis处理的每一个写命令都会记录在AOF文件中,可以看成命令日志文件。默认关闭AOF,需要修改redis.conf中的appendonly来开启。还可以配置记录频率。
因为是记录命令,AOF文件会比RDB文件大很多,可能会记录对一个key的多次操作,但只有最后一次写操作才有意义。可以通过bBGREWRITEAOF命令来重写合并AOF文件。
RDB和AOF对比
1.4 内存淘汰
1. 数据过期策略
问:Redis的key过期之后,会立即删除吗?
不会立即删除,但数据过期以后需要从内存中删除,删除策略有惰性删除和定期删除两种。Redis是惰性删除和定期删除配合使用的。
惰性删除
设置key过期之后,不去管这个key,再次获取时如果发现过期,则删掉这个key。
对CPU友好,不需要额外检查key,但对内存不友好,过期key如果不被访问到就会一直存在在内存中。
定期删除
定期随机对一些key进行检查,删除里面过期的key。有SLOW和FAST两种模式,SLOW模式默认100ms执行一次,每次不超过25ms;FAST模式执行频率不固定,耗时不超过1ms。
减轻内存的压力。
2. 内存淘汰策略 maxmemory-policy
问:内存被占满了怎么办?
Redis支持8种不同的策略来删除key。
- noeviction:默认这种策略,不淘汰任何key,内存满后不允许新key写入。
- volatile-ttl
- allkeys-random
- volatile-random
- allkeys-lru
- volatile-lru
- allkeys-lfu
- volatile-lfu
LRU最少最近使用,LFU最少频率使用。
问:数据库中有1000w数据,Redis中只能存20w数据,如何保证Redis中的数据都是热点数据?
答:可以把淘汰策略设置为allkeys-lru,那么留下来的就是热点数据。
1.5 双写一致性
问:MySQL数据如何与Redis进行同步?
先介绍业务背景:是一致性要求高,还是允许一定的延迟。
读操作:缓存命中,直接返回;缓存未命中则查询数据库,写入缓存,并设置过期时间。
方案一:延迟双删
写操作:采用延迟双删策略,先删除缓存,修改数据库,延时等待一段时间后再删除缓存。延时双删只能降低脏数据的风险,不能完全解决脏数据的问题。
- 为什么要删除两次缓存?
因为无论先删缓存还是先修改数据库,都有可能存在脏数据,所以要删除两次。 - 为什么要延时删除?
因为数据库一般是主从模式,读写分离的,需要延时让主节点把数据同步给从节点。
方案二:加分布式锁
性能会有损耗。
二、原理
2.1 高效
问:为什么Redis是单线程的,还那么快?
- 完全基于内存,采用C语言编写,更高效。
- 采用单线程,避免不必要的上下文切换和竞争条件。
- 使用了I/O多路复用模型,是非阻塞I/O。
Redis的网络模型就是使用I/O多路复用结合事件处理器来处理多个socket请求,包括连接应答处理器、命令回复处理器、命令请求处理器。Redis的性能瓶颈主要在网络请求上,在Redis 6.0之后,采用了多线程来处理网络I/O,比如多线程转换命令、回复事件,但执行命令仍是单线程。
问:解释一下I/O多路复用模型。
I/O多路复用指的是利用单线程来监听多个socket,并在某个socket可读或可写时得到通知,避免了无效的等待,充分利用了CPU的资源。Redis中的I/O多路复用使用的是epoll模式,会在通知用户进程socket就绪的时候,将已就绪的socket写入用户空间。
2.2 底层数据结构
动态字符串SDS、IntSet、Dict、ZipList、QuickList、SkipList
三、应用 / 设计
3.1 分布式锁
场景:集群情况下的定时任务、抢单、幂等性场景。
1. 基础实现
- SET lock NX EX time
- 有一个单独的看门狗线程对锁进行续期
- 释放锁时判断是否为本线程的锁(lua脚本)
锁的名字可以是UUID+线程名,加UUID是为了区别多个jvm运行应用的情况。
存在一些极低概率的问题:
- 不可重入:同一个线程无法多次获取同一把锁。
- 不可重试:需业务代码额外编写重试策略。
- 主从一致性:如果Redis使用集群,主从同步之间存在延迟,极端情况下从主节点获取了锁,主宕机后从节点中锁是没有被获取的。
2. Redisson
Redisson是一个在Redis基础上实现的分布式工具的集合。
可重入:利用hash结构记录线程id和重入次数。
可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制。
超时续约:利用watchDog看门狗,每个一段时间重置超时时间。
3.2 Redis集群
主从复制、哨兵模式、分片集群
1. 主从复制
单机Redis并发8-10w,想进一步提升,需要构建集群。主从集群是一主多从,可以实现读写分离,写操作主节点,读操作从节点。
主从数据同步流程是什么?
- 全量同步(首次或offset相差过大)
- 从节点发送同步请求(含replication id和offset)。
- 主节点对比replid不一致,则需要全量同步。返回master的eplication id、offset给从节点保存。
- 主节点进行bgsave,生成一份rdb,发送给从节点。
- 从节点收到rdb,将自己数据清空后,读取rdb。
- 主节点生成rdb过程中的写操作会记录在rep_baklog中,作为增量数据发送给从节点。
- 增量同步(后续)
- 从节点发送同步请求(含eplication id和offset)。
- 主节点对比eplication id一致,只需要增量同步。
- 主节点发送offset到最新操作的rep_baklog给从节点。
- 一般是节点重启服务后。
2. 哨兵模式
主从模式下,如果主节点宕机,则会出现无法写入的情况。Redis提供了哨兵机制来实现主从集群的自动故障恢复。
功能
- 监控:Sentinel每秒会发ping指令来检查主从节点是否正常工作。
- 自动故障恢复:如果主节点客观下线,Sentinel就会从从节点中选出新的主节点。
- 客观下线:当多个Sentinel都认为某个节点下线了,则这个节点为客观下线。如果只是少数Sentinel认为则是主观下线。
- 选出新的主节点:
- 首先判断这些从节点和主节点的断开时间,超过一定值则排除。
- 按照slave_priority排序,越小优先级越高。
- slave_priority一样的情况下,offset值大的优先,表明更近更新过,丢失数据更少。
- 按照slave节点的运行id排序,越小优先级越高。
- 通知变更:出现新的主节点后,Sentinel会将最新的信息推送给Redis客户端。
脑裂问题
当Sentinel认为主节点客观下线了,就会选出新的主节点,但原来的主节点恢复工作了,就存在了两个主节点。部分客户端往原来的主节点中写入了一部分数据,但原来的主节点会降级为从节点,将同步新的主节点的数据,所以会丢失部分数据。
- 出现原因:节点故障,或者Sentinel和主节点在不同的网络分区。
- 解决方法:
- min-slaves-to-write 1 设置连接到master的最少slave数量(判断有多少个从服务器,达到了要求才发送信息,避免了主服务器失连后依旧写入数据)
- min-slaves-max-lag 10 设置slave连接到master的最大延迟时间(减少同步间隔时间,在失连前同步完成)
问:怎么保证Redis的高并发高可用?
高并发:主从架构 / 分片集群
高可用:哨兵模式
3. 分片集群
- 集群中有多个master,每个master保存不同的数据。
- 每个master都可以有多个slave节点。
- master之间通过ping监测彼此健康状态。
- 客户端请求可以访问集群任意节点,最终都会转发到正确节点。
3.3 Redis限流
1. 固定窗口
2. 滑动窗口
3. 令牌桶
3.2 BigKey
1. 什么是BigKey
某一个key对应的value占用空间很大,那么就称为BigKey。
String类型下超过1M,其他类型下含有成员数上万个。
2. 为什么会产生BigKey?
业务分析不准确,实际业务中value值过大。List、Set中的无效数据没有及时删除。
3. BigKey的影响
占用内存增大、网络阻塞延迟变大、集群中迁移困难。
4. 怎么解决BigKey
- 根据业务将key拆分为多个key。
- 及时清理list、set中的无效数据。
3.3 Redis场景
- 查询数据缓存
- 分布式锁
- 延迟队列
商品到时下架。