一.Redis的数据类型有哪些
- 5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
- 3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
redis的5种基础数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
String
虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(Simple Dynamic String,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
List
Redis 中的 List 其实就是链表数据结构的实现。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList
,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
Hash
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。
Set
Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet
。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。
你可以基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
Sorted Set(ZSet)
Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score
,使得集合中的元素能够按 score
进行有序排列,还可以通过 score
的范围来获取元素的列表。有点像是 Java 中 HashMap
和 TreeSet
的结合体。
二.Redis为什么这么快
1.基于内存实现
Redis 将数据存储在内存中,读写操作不会受到磁盘的 IO 速度限制,所以Redis的读写速度会非常的快。
2.使用I/O多路复用模型
传统阻塞 IO ,在执行accept 、recv 等网络操作时,如遇到异常情况会一直处于阻塞状态。多路指的是多个 socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll 是最新的也是目前最好的多路复用技术。
Redis 单线程情况下,内核会一直监听 socket 上的连接请求或者数据请求,一旦有请求到达就交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器。所以 Redis 一直在处理事件,提升了 Redis 的响应性能。
Redis 线程不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis可以同时和多个客户端连接并处理请求,从而提升了并发性。
3.采用单线程模型
Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是多线程执行。
单线程的优势
- 不会因为线程创建导致的性能消耗
- 避免上下文切换引起的 CPU 消耗,没有多线程切换的开销
- 避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁问题
- 代码更清晰,处理逻辑简单
Redis6.0后引入了多线程网络IO来提升连接性能,但是数据读取仍然使用的是单线程。
4.高效的数据结构
为了追求速度,不同数据类型使用不同的数据结构速度才得以提升。每种数据类型都有一种或者多种数据结构来支撑,Redis数据类型和底层数据结构的关系如下图所示。
SDS的特性
SDS是String的底层实现结构。
低时间复杂度,SDS的len保存了已使用空间的长度,获取字符串长度的时间复杂度为O(1)
空间预分配,SDS被修改后,会被分配所需要的必须空间以及额外的未使用空间。
分配规则:如果SDS被修改后,len的长度小于1M,那么SDS将被分配和len相同长度的未使用空间。举个例子,如果 len=10,重新分配后,buf的实际长度会变为10(已使用空间)+10(额外空间)+1(空字符)=21。如果SDS被修改后,len长度大于1M,那么SDS将分配1M的未使用空间。
ZipList的特性
ZipList是List 、hash、sorted Set三种数据类型底层实现之一。
ZipList是由一系列特殊编码的连续内存块组成的顺序型的数据结构,不容易产生内存碎片,内存利用率高。
适用情景:一个列表只有少量数据,并且每个列表项是小整数或短字符串
ZipList的结构。
查找第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,时间复杂度是O(1)。而查找其他元素时,只能逐个查找,此时的时间复杂度是O(N)。
ZipList在表头有三个字段:zlbytes、zltail和zllen,分别表示列表占用字节数、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
ZipList的缺点
插入和删除操作需要频繁的申请和释放内存,同时会发生内存拷贝,数据量大时内存拷贝开销较大。
LinkedList的特性
LinkedList是List的底层实现结构之一。
- 双端带有prev和next指针,定义前后节点的时间复杂度为O(1)。
- 无环表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL 为终点。
- 表头指针和表尾指针通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的时间复杂度为O(1)。
- 链表长度计数器使用list结构的len属性来对list持有的链表节点进行计数,获取链表中节点数量的时间复杂度为O(1)。
- 链表节点使用void*指针来保存节点值,并且可以通过 list 结构的dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
LinkedList的缺点
除保存数据外还需要保存prev、next两个指针,内存利用率低,LinkedList的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ZipList和LinkedList区别
内存使用
- ziplist:通过紧凑存储数据来减少内存使用,适用于元素数量少且元素值小的场景。ziplist不存储指向上一个节点和下一个节点的指针,而是存储上一个节点的长度和当前节点的长度,从而节省内存。
- linkedlist:每个节点都会存储指向上一个节点和指向下一个节点的指针,这会导致大量的内存碎片,因为指针本身也占用内存。
访问速度
- ziplist:由于数据是连续存储的,ziplist在访问时可以利用CPU缓存,提高读取速度。但是,修改中间元素可能需要重构整个列表,这可能会影响性能。
- linkedlist:不支持随机访问,要访问链表中的某个元素,必须从头节点开始遍历到目标节点,这在大型链表中可能会导致较慢的访问速度。
插入和删除操作
- ziplist:插入和删除操作可能会导致整个列表的重新构建,尤其是在列表的中间部分进行插入或删除时。
- linkedlist:插入和删除操作只需要修改相应节点的指针,操作相对简单,但随机访问性能较差。
适用场景
- ziplist:适用于需要存储大量小数据量的场景,如列表、集合和哈希表中的小元素。它节省内存且支持多种数据类型,但不适合较大的数据量。
- linkedlist:适用于需要频繁插入和删除元素的场景,但不适合随机访问
QuickList
QuickList是Redis 3.2版本之后引入的一种新型数据结构,它将多个ZipList串联成一个双向链表。每个QuickList节点中保存一个ZipList,每个ZipList存储一批list中的数据。QuickList的设计目的是在保证高效内存使用的同时,减少内存碎片和操作开销。具体来说:
- 内存使用:QuickList通过将大的ZipList化整为零,避免了大量链表指针带来的内存消耗,同时也减少了内存碎片的产生。
- 操作效率:QuickList结合了ZipList和LinkedList的优点,既保留了ZipList的连续内存布局,又避免了LinkedList的大量指针开销和内存碎片问题。这使得它在插入和删除操作上比纯ZipList更高效,同时查找性能也优于纯LinkedList
SkipList( 跳表)
SkipList是Sorted Set的底层实现结构之一。
SkipList是一种有序数据结构,它通过在每个节点中存储着多个指向其他节点的指针,从而达到快速访问节点的目的。
SkipList支持平均O(logN)、最坏O(N)时间复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
SkipList在LinkedList的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位。
IntSet 整数数组
IntSet是Set的底层实现结构之一。
适用情景:一个集合只包含整数值元素,并且这个集合的元素数量不多。
Inset的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
5.合理的数据编码
由上文可知,不同的数据类型,其底层支持可能有多种数据结构,在不同的时候选择不同的底层数据结构,就涉及到编码转化的问题。
String
底层结构为SDS,若是数字,采用int类型的编码;若是字符,采用raw编码。
List
List 对象的编码包括ZipList和LinkedList(Redis 3.2后改由QuickList实现),若字符串长度 < 64 字节且元素个数 < 512 (默认)则使用ZipList编码,否则使用LinkedList 编码。上述选择ZipList编码的条件为Redis默认值,具体数值可在redis.conf文件中修改。
Hash
Hash对象的编码包括ZipList(Redis 6.0后被ListPack取代)和HashTable,与List类似,若键/值字符串长度 < 64 字节且元素个数 < 512 (默认)则使用 ZipList编码,否则使用HashTable编码。
Soeted Set
Soeted Set对象的编码包括ZipList(Redis 6.0后被listpack取代)和SkipList,与List类似,若字符串长度 < 64 字节且元素个数 < 512 (默认)则使用ZipList编码,否则使用HashTable编码。
Set
Set对象的编码包括IntSet和HashTable,若元素为整数且元素个数小于一定范围使用IntSet编码,否则使用 hashtable 编码。
三.Redis的持久化方式
1.RDB(快照持久化)
RDB持久化是通过将Redis在内存中的数据库记录定时dump到磁盘上的二进制文件中,实现数据的持久化。这个过程可以理解为对Redis内存数据的快照。当Redis需要持久化数据时,它会fork一个子进程,子进程负责将内存中的数据写入到临时文件中,写入成功后,再用这个临时文件替换上次的快照文件。由于这个过程是在子进程中完成的,所以主进程可以继续处理客户端的请求,不会受到持久化操作的影响。
RDB的触发机制
- save命令:这是一个同步操作,会阻塞当前Redis服务器,直到RDB完成为止。因此,线上环境一般禁止使用。
- bgsave命令:这是Redis内部默认的持久化方式,它是一个异步操作。当执行bgsave命令时,Redis会fork一个子进程来完成RDB的过程,主进程可以继续处理客户端请求。
- 自动触发:可以在redis.conf配置文件中设置自动触发的条件,比如“save 900 1”表示在900秒内,如果至少有1个key发生变化,则自动触发bgsave命令。
RDB的优点
- RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
- 对于大规模数据的恢复,且对于数据恢复的完整性不是非常敏感的场景,RDB的恢复速度要比AOF方式更加的高效。
- 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
RDB的缺点
- fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。
- 当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。
- 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。
2.AOF(追加文件持久化)
AOF持久化是通过将Redis执行过的每个写操作以日志的形式记录下来,当服务器重启时会重新执行这些命令来恢复数据。AOF文件以追加的方式写入,即新的写操作会追加到文件的末尾,而不是覆盖之前的内容。这种方式可以确保数据的完整性和一致性。
AOF的触发机制
AOF持久化是异步操作的,Redis会在后台线程中执行fsync操作,将AOF文件的内容同步到磁盘上。用户可以通过配置appendfsync参数来控制fsync操作的频率:
- appendfsync always:每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度。
- appendfsync everysec:每秒钟同步一次,这是AOF的缺省策略,它可以在性能和数据安全性之间取得一个平衡,宕机会丢1秒数据。
- appendfsync no:从不主动同步,而是让操作系统决定何时进行同步,这种方式性能最好,但数据安全性最差。
AOF的优点
- AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
- AOF只是追加写日志文件,对服务器性能影响较小,速度比RDB要快,消耗的内存较少。
- AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
- AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。
AOF的缺点
-
AOF文件会越来越大,需要定期进行AOF重写来压缩文件大小。
-
在数据恢复时,AOF需要执行所有的写操作命令,这可能比RDB的全量加载要慢一些。
3.RDB+AOF混合模式
先RDB备份,后AOF追加。
四.Redis四种部署模式
1.单节点模式
优点
- 配置简单,操作简单
缺点
- 单点的宕机引来的服务的灾难、数据丢失
- 单点服务器内存瓶颈,无法无限纵向扩容
2.主从模式
优点
- 有了主从,提高了Redis整体的可用性,当主节点(master)挂了,可以把从节点(slave)手动升级为主节点继续服务。
缺点
- master挂了整个Redis将失去写操作的能力,仅具备读操作,需要运维半夜爬起来手动升级,中间的请求失败数据丢失无法容忍。
主从复制流程:
- slave第一次连接master,一定会执行一次全量复制
- 全量复制数据量过大,会造成很大的网络开销,消耗CPU/内存/硬盘IO
- 增量复制用于处理在主从复制中因网络等数据丢失的场景,当slave再次连接上master,并且就是原来的master,如果条件允许,master补发数据给slave,补发数据量小,避免全量复制的开销(到底能不能复制还要看offset和buffer的情况)
- 如果slave再次连上的master是新选举的master,那么只能进行全量复制
- 早期的redis只有全量复制,增量复制是对全量复制的重大优化,尽量采用2.8以上版本
全量复制流程:
- slave给master发一个sync同步命令
- master通过bgsave命令fork子进程,持久化生成RDB文件
- master通过网络将RDB文件传给slave
- slave清空老数据,载入新的RDB文件,此时slave阻塞,无法响应客户端,专心复制
增量复制流程:
- 主从节点各自维护自己的复制偏移量offset,主节点写入命令时,offset=offset+命令字节长度;从节点收到主节点命令也会相应增加自己的offset,并同步给主节点。主节点同时维护自己的offset和从节点的offset,以此来判断主从节点数据是否一致。
- 主节点指令记录在本地buffer(缓冲区),异步将buffer同步给从节点
- 若网络不好,同步速度慢了,buffer满了就会从头开始覆盖前面的内容,于是无法增量复制,必须全量复制
3.哨兵模式
哨兵模式的出现用于解决主从模式中无法自动升级主节点的问题,一个哨兵是一个节点,用于监控主从节点的健康,当主节点挂掉的时候,自动选择一个最优从节点升级为主节点。
但哨兵如果挂了怎么办?于是哨兵一般都会是一个集群,是集群高可用的心脏,一般由3-5个节点组成,即使个别节点挂了,集群还可以正常运行。
优点
- 可以在master挂掉后自动选举新的master
缺点
- master挂了,切换新的master会造成未来得及主从同步的数据丢失
- 大数据高并发,单个master内存仍存在上限
- 主从全量同步仍然要耗费大量时间
- 单个Redis只能利用CPU的单个核心,应对海量数据捉襟见肘
哨兵定时监控任务
- 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最近的拓扑结构,可以感知新加入或故障转移的Redis数据节点
- 每隔2秒,每个Sentinel节点会与其他Sentinel节点通信,可以发现新的Sentinel节点,并与其他Sentinel节点交换对主节点的判断信息,方便故障转移及选举
- 每隔1秒,每个Sentinel节点会向主节点、从节点以及其他的Sentinel节点发送ping做一次心跳检测,来确认这些节点是否可达,实现对每个节点的监控
主观下线和客观下线
主观下线:当Sentinel节点ping其他节点,超时未回复,Sentinel节点就会对该节点做失败判定。主观下线是当前Sentinel节点的一家之言,存在误判可能
客观下线:当Sentinel主观下线的节点是master,该Sentinel会询问其他Sentinel对master的判断,当大部分Sentinel都对master的下线做了同意判断,那么这个判断就是比较客观的
Sentinel集群的领导者选举
Sentinel节点已经对master做了客观下线,但还不能立刻进行故障转移,因为故障转移工作只需要一个Sentinel节点来完成,因此在Sentinel集群中要选出一个leader来进行故障转移。
Redis使用了Raft算法实现领导者选举:
- 每个在线的Sentinel节点都有资格成为领导者,他要求其他节点来投自己一票
- 先收到谁的要求就给谁投票,但不能给自己投
- 首先拿到大多数(即一半+1)的票当选领导者
故障转移流程
Sentinel集群领导者负责此次故障转移
- 排除主观下线的从节点
- 选择优先级高的从节点,如果没有再进行3
- 选择复制偏移量最大的从节点(复制的最完整),如果没有再进行4
- 选择runid最小的从节点
4.集群模式
集群方案真正实现了Redis高可用,有很多种实现方式,目前最常用的Redis Cluster是Redis的亲儿子,是Redis作者自己提供的Redis集群化方案,从Redis3.0版本开始正式提供。
集群由多个Redis主从组成,每一个主从代表一个节点,每个节点负责一部分数据,他们之间通过一种特殊的二进制协议交互集群信息。Redis Cluster将所有数据分片,分成16384个槽位,Redis Cluster对key值使用crc16算法进行hash,然后用除留余数发模除16384得到具体的槽位,每个节点负责其中一部分槽位。当客户端连接集群,会得到一份集群的槽位匹配信息,当客户端要查找key,可以直接定位到目标节点。
Cluster去中心化,由多个节点组成,客户端连接时可以只用一个节点的地址,其余节点可通过该节点自动发现,但如果该节点挂了,就必须手动更换地址,因此连接多个地址安全性更高。
Redis Cluster拥有类似哨兵的功能,每个节点仍需设置若干从节点,主节点发生故障,集群可将slave升级为master;否则如果master挂了,集群完全不可用;且Redis Cluster是去中心化,集群内某个节点不可用时,一个节点认为他失联并不代表所以节点都认为他失联,集群要进行一次商议,只有大多数节点认为他失联,才会认为其需要主从切换来容错。
优点
- 无中心架构,支持动态扩容
- Cluster自动具备哨兵监控和故障转移(主从切换)能力
- 客户端连接集群内部地址可自动发现
- 高性能、高可用,有效解决了Redis分布式需求
缺点
- 运维复杂
- 只能使用0号数据库
5.常见问题
1.Sentinel节点为什么是至少三个且奇数个?
- 首先必须是集群,所以不能是1个。而选举过程要大多数同意才行(即最少一半+1个),而奇数个节点在同样选举条件上可以节省一台机器。
- 比如一共5台,有3台同意了就行;而4台,大多数同意也要3台。
2.Redis集群节点数为什么至少是6个?
- 6个包含一主一从,如果每个节点一主二从则需要9个
- 最少3个节点,是因为容错能力相同情况下,奇数节点更节省资源(3个节点的集群允许一个节点宕机;而4个节点的集群也允许一个节点宕机)
3.为什么Redis Cluster槽位设计成16384个?
- 2^14=16384,当槽位再扩就是32768、65536,此时每一次ping都要将槽位作为心跳包,信息太大,占用带宽
- 由于集群节点越多,心跳包携带的数据就越多。当节点超过1000,会导致网络拥堵。因此Redis作者不建议Redis Cluster节点数量超过1000,那么16384槽位也足够用
五.一致性Hash算法
1.一致性Hash算法的介绍
一致性Hash算法是对2的32次方取模,什么意思呢?简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2的32次方-1(即哈希值是一个32位无符号整形),整个哈希环如下:
整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1, 0和2^32-1在零点中方向重合,我们把这个由2^32个点组成的圆环称为Hash环。
假设我们有4台缓存服务器,服务器A、服务器B、服务器C,服务器D,那么,在生产环境中,这4台服务器肯定有自己的IP地址或主机名,我们使用它们各自的IP地址或主机名作为关键字进行哈希计算,使用哈希后的结果对2^32取模,可以使用如下公式示意:
hash(服务器A的IP地址) % 2^32
通过上述公式算出的结果一定是一个0到2^32-1之间的一个整数,我们就用算出的这个整数,代表服务器A,既然这个整数肯定处于0到2^32-1之间,那么,上图中的hash环上必定有一个点与这个整数对应,而我们刚才已经说明,使用这个整数代表服务器A,那么,服务器A就可以映射到这个环上。
以此类推,下一步将各个服务器使用类似的Hash算式进行一个哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用IP地址哈希后在环空间的位置如下:
接下来使用如下算法定位数据访问到相应服务器: 将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。
2.一致性Hash算法的容错性和可扩展性
现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响,如下所示:
如果在系统中增加一台服务器Node X,如下图所示:
此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X !一般的,在一致性Hash算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。
综上所述,一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
3.一致性Hash数据倾斜问题
一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器,其环分布如下:
此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上,从而出现hash环偏斜的情况,当hash环偏斜以后,缓存往往会极度不均衡的分布在各服务器上,如果想要均衡的将缓存分布到2台服务器上,最好能让这2台服务器尽量多的、均匀的出现在hash环上,但是,真实的服务器资源只有2台,我们怎样凭空的让它们多起来呢,没错,就是凭空的让服务器节点多起来,既然没有多余的真正的物理服务器节点,我们就只能将现有的物理节点通过虚拟的方法复制出来。
这些由实际节点虚拟复制而来的节点被称为"虚拟节点",即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。
例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:
同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
六.缓存穿透、缓存击穿、缓存雪崩
1.缓存穿透
缓存穿透是指大量请求绕过缓存直接访问数据库,通常由于请求的数据在缓存和数据库中都不存在。
解决方案包括:
- 缓存空值:对于数据库中不存在的数据,将空值缓存,并设置较短的过期时间。这样可以避免每次请求都查询数据库。
- 布隆过滤器:在请求到达缓存层之前进行拦截,如果请求的数据在布隆过滤器中不存在,直接返回,避免查询数据库。
- 参数校验:在应用层对请求参数进行校验,拦截非法或异常请求,减少无效请求对数据
2.缓存击穿
缓存击穿是指热点数据在缓存失效后,大量请求瞬间涌向数据库。
解决方案包括:
- 使用互斥锁:确保在缓存失效时,只有一个请求能访问数据库并更新缓存,其他请求等待锁释放。
- 提前预热缓存:在系统启动或高峰期前,将热点数据提前加载到缓存中。
- 缓存永不过期:对于热点数据,设置缓存永不过期,通过后台线程定期更新缓存。
3.缓存雪崩
缓存雪崩是指大量缓存数据同时失效,导致大量请求直接打到数据库上。
解决方案包括:
- 分散失效时间:通过随机生成失效时间,防止集体过期。
- 多级缓存架构:使用多级缓存架构,不同层使用不同的缓存,增强可靠性。
- 设置缓存标记:记录缓存数据是否过期,如果过期则触发后台更新。
七.Mysql和Redis一致性问题
常见的数据查询逻辑:
1. 应用程序需要从数据库读取数据时,先查询redis的缓存数据是否命中。
2. 若命中,直接返回。若未命中,再去查询数据库。
3. 将查询到的数据先保存到redis中,并设置过期时间,再将数据返回到应用。
只读数据不会出现一致性问题
常见的写数据逻辑:
1. 先删除缓存再更新数据库。
2. 先更新数据库再删除缓存。
一致性问题出现在多线程并发读写的时候
场景1:先删缓存再更新数据库
1. 线程1发起修改数据请求,会进行删除缓存操作。
2. 接着更新数据库时出现了网络延迟。
3. 线程1由于网络延迟还未对数据库进行修改,此时线程2执行查询请求,会去缓存中查询数据。
4. 线程2在缓存中未查询到数据,再去查询数据库。
5. 线程2将查询到数据旧数据放到缓存中,并将数据返回。
6. 线程1在线程2数据查询完成后,才对数据库进行了修改。
在这个过程中就出现了redis与数据库数据不一致的问题,只有等redis中数据过期时间到了,才能将新数据更新到缓存中。
场景2:先更新数据库再删缓存
1. 线程1发起修改数据请求,先更新数据库。
2. 线程2在线程1更新数据库期间,发起查询请求,从缓存中获取到旧数据(脏数据)。
3. 线程1完成数据库更新后,删除缓存中的数据。
在这个过程中出现了短暂的数据不一致,但redis和数据库数据是最终一致性的。所以推荐先操作数据库再操作缓存。
解决方案
在不考虑redis操作失败的情况下,保证redis与数据库数据一致性的解决方案有4种。
1.延迟双删除机制
该机制是在数据库数据更新后,先延迟一段时间后再次删除缓存数据。线程1写请求,线程2查询请求,通过延迟双删机制保证redis与数据库数据一致性。通过(6)步延迟一段时间后再进行redis的删除,在并发读写情况下保证redis与数据库数据一致性。具体延迟多长时间,需评估项目读数据业务逻辑耗时(即线程2从数据库读取数据到更新缓存成功的时间)。确保查询请求结束,更新请求可以删除查询请求造成的缓存脏数据。但是延迟期间的缓存数据仍然是旧数据。
2.binlog同步删除机制
通过canal组件订对binlog日志进行订阅,模仿数据库的slave数据库的备份请求,使得redis缓存数据删除,保证redis与数据库数据一致性。通过上面两种方式,在并发读写的情况下保证redis与数据库数据最终一致性。但可能存在redis删除失败的情况,一旦出现就会有redis与数据库数据不一致的问题。只有等redis中数据过期时间到了,才能将新数据更新到缓存中。
3.删除缓存重试机制
使用重试机制,保证删除缓存成功。比如重试三次,三次都失败则记录日志到数据库并发送警告让人工介入。在高并发的场景下,重试最好使用异步方式,比如发送消息到 mq 中间件,依赖mq的手动确认机制,确保删除成功才提交消费成功的消息,实现异步解耦。
八.如何使用redis实现分布式锁
Redis 可以通过 setnx(set if not exists)命令实现分布式锁
Redis 分布式锁存在两个问题:
-
死锁问题:未设置过期时间,锁忘记释放,加锁后还没来的及释放锁就宕机了都会导致死锁问题.
-
锁误删问题:设置了超时时间,但是线程执行超过超时时间后锁误删问题.
解决死锁问题
MySQL 中解决死锁问题是通过设置超时时间,Redis 也是如此,但是问题来了,第一步先加锁,然后再设置超时时间,那么就不满足原子性了,那么怎么办 ?
官方在 Redis 2.6.12 版本之后,新增了一个功能,我们可以使用一条命令既执行加锁操作,又设置超时时间:setnx 和 expire
解决锁误删问题
前面我们使用 set 命令的时候,只使用到了 key,那么可以给 value 设置一个标识,表示当前锁归属于那个线程,例如 value=thread1
但是这样解决依然存在问题,因为新增锁标识之后,线程在释放锁的时候,需要执行两步操作了:
-
判断锁是否属于自己
-
如果是,就删除锁
这样就不能保证原子性了,那该怎么办?
使用 lua 脚本来解决
(Redis 本身就能保证 lua 脚本里面所有命令都是原子性操作)
if redis.call("get",KEYS[1]) == ARGV[1]
then return redis.call("del",KEYS[1])
else return 0 end
Redis主从切换问题(红锁)
Redlock是一个由Redis的创始人开发的分布式锁算法,其思想基于Paxos算法。Redlock算法的流程如下:
- 客户端获取当前时间戳t1。
- 客户端依次向N个Redis节点请求锁,每个请求的锁过期时间为t1+TTL(time to live)。
- 如果客户端在大多数节点上都获得了锁,则客户端获得了锁。
- 如果客户端在少数节点上未能获得锁,则客户端将在所有已获得锁的节点上释放已经获得的锁。
- 如果客户端在所有节点上都未能获得锁,则重复步骤1。
其中N为Redis节点数量,TTL指过期时间。
Reddission
Redisson分布式锁是一种基于redis实现的分布式锁,它利用redis的setnx命令实现分布式锁的互斥访问。同时还支持锁的自动续期功能,可以避免因为某个进程崩溃或者网络故障导致锁无法释放的情况。
只要线程一加锁成功,就会启动一个watchdog看门狗,它是一个后台线程,会每隔10秒检查一下,默认生存时间只有30秒,如果线程A还持有锁,那么就会不断的延长锁key的生存时间。可以使用reentracklock,公平锁,读写锁,信号量,闭锁等锁进行加锁操作,完成后然后释放锁。其他线程BCD判断加锁的次数为0,就可以进行加锁操作。
因此,Redisson就是使用watch dog解决了「锁过期释放,业务没执行完」的问题。
1.加锁机制:
一个客户端A要加锁,它首先会根据hash算法选择一台机器,这里注意,仅仅只是选择一台机器。紧接着就会发送一段lua脚本到redis上,比如加锁的那个锁key为"mylock",并且设置的时间是30秒,30秒后,mylock锁就会被释放。
2.锁互斥机制:
如果这个时候客户端B来加锁,它也会会根据hash算法选择一台机器,然后执行了同样的一段lua脚本。它首先回来判断"exists mylock"这个锁存在吗?如果存在,则B会获得一个数字,这个数字就是mylock这个锁的剩余生存时间,此时客户端B就会进入到一个while循环,不停的尝试加锁
3.watch dog自动延期机制:
客户端A加锁的锁key,默认生存时间只有30秒,如果超过了30秒,客户端A还想一直持有这把锁,怎么办?其实只要客户端A一旦加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间
4.可重入加锁机制:
当客户端A获得mylock锁时,里面会有一个hash结构的数据:
mylock:{"8743c9c0-4907-0795-87fd-6c719a6b4586:1":1}
"8743c9c0-4907-0795-87fd-6c719a6b4586:1"就是代表客户端A,数字1就是代表这个客户端加锁了一次;如果客户端A还想要持有锁mylock,那么这个1就会变成2,依次累加。
5.释放锁机制:
如果发现加锁次数变为0了,那么说明这个客户端不再持有锁了,客户端B就可以加锁了。
参考文章:
Redis 八种常用数据类型详解 - JavaGuide - 博客园
Redis为什么那么快?_redis为什么这么快-CSDN博客
Redis持久化AOF&RDB区别是什么?_redis rdb aof区别-CSDN博客
Redis四种部署模式(原理、优缺点及解决方案)_redis_少年做自己的英雄-GitCode 开源社区