redis

1.redis 分布式锁及问题

(1)实现:

加锁:setnx、解锁:del、锁超时:expire

(2)可能出现的问题

①setnx 和expire非原子性问题(加锁之后还没来得及设置超时就挂了)
解决方案:Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令
②超时误删其他进程锁。(A进程执行超时,导致锁释放,这时候B进程获取锁开始处理请求,这时候A进程处理完成,会误删B进程的锁)
解决方案:只能删除自己进程的锁 (lua脚本防止B进程获取过期锁之后误删A进程的锁)
③并发场景,A进程执行超时导致锁释放,这时候B进程获取到锁。
解决方案:开启守护进程,给当前进程要过期的锁延时。
④单点实例安全问题
单机宕机之后导致所有客户端无法获取锁
解决:主从复制,因为是异步完成的所以无法完全实现解决

2.redis 的过期删除和淘汰机制

(1)常规过期删除策略

1)定时删除
通过定时器在过期的时候立即删除
内存释放及时但是消耗更多的CPU,大并发的时候需要消耗CPU资源影响处理请求的速度
内存友好,CPU不友好
2)惰性删除
放任键过期不管,到下次需要去取出的时候检查是否过期并删除
可能存在大量过期键,且不会使用,导致内存溢出
内存不友好,CPU友好
3)定期删除
每隔一段时间检查,删除过期的键
删除多少和检查多少有算法决定

(2)redis采用的 惰性删除 + 定期删除

周期性随机测试一些设置了过期时间的键进行检查,到期则删除
每次清理的时间不超过CPU的25%,达到时间则退出检查
定期没有删除到的键,且以后不会使用的键还是会存在内存中,所以需要配合淘汰策略

(3)淘汰策略(内存不足以写入新数据的时候执行)

volatile-lru :设置了过期时间且最近使用越少越优先淘汰
volatile-ttl :设置了过期时间且过期时间越早越优先淘汰
volatile-random :设置了过期时间中随机删除
allkeys-lru :所有键中过期时间越早越优先淘汰
allkeys-random :所有键中过期随机淘汰
no-enviction :不允许淘汰,内存不足报错

3.redis 发布订阅

使用pub和 sub的 方式处理消息,它采用事件作为基本的通信机制,提供大规模系统所要求的松散耦合的交互模式:订阅者(如客户端)以事件订阅的方式表达出它有兴趣接收的一个事件或一类事件;发布者(如服务器)可将订阅者感兴趣的事件随时通知相关订阅者。
publish 发布消息
subscribe 订阅消息

4.事务机制

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队
详解:https://blog.csdn.net/qq_36228377/article/details/122908614

5.缓存击穿、穿透、雪崩

5.1 缓存穿透(cache penetration)是用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。

解决方案

  • 方案一:缓存空值(null)或默认值
    分析业务请求,如果是正常业务请求时发生缓存穿透现象,可针对相应的业务数据,在数据库查询不存在时,将其缓存为空值(null)或默认值。需要注意的是,针对空值的缓存失效时间不宜过长,一般设置为5分钟之内。当数据库被写入或更新该key的新数据时,缓存必须同时被刷新,避免数据不一致。
  • 方案二:业务逻辑前置校验
    在业务请求的入口处进行数据合法性校验,检查请求参数是否合理、是否包含非法值、是否恶意请求等,提前有效阻断非法请求。比如,根据年龄查询时,请求的年龄为-10岁,这显然是不合法的请求参数,直接在参数校验时进行判断返回。
  • 方案三:使用布隆过滤器请求白名单
    在写入数据时,使用布隆过滤器进行标记(相当于设置白名单),业务请求发现缓存中无对应数据时,可先通过查询布隆过滤器判断数据是否在白名单内,如果不在白名单内,则直接返回空或失败。
  • 方案四:用户黑名单限制
    当发生异常情况时,实时监控访问的对象和数据,分析用户行为,针对故意请求、爬虫或攻击者,进行特定用户的限制;

当然,可能针对缓存穿透的情况,也有可能是其他的原因引起,可以针对具体情况,采用对应的措施。
5.2 缓存雪崩
当缓存中大量热点缓存采用了相同的实效时间,就会导致缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。从而形成一系列的连锁反应,造成系统崩溃等情况,这就是缓存雪崩

原因:大量热点key同时过期,缓存服务故障
缓存雪崩的解决方案

  • 通常的解决方案是将key的过期时间后面加上一个随机数(比如随机1-5分钟),让key均匀的失效。
  • 考虑用队列或者锁的方式,保证缓存单线程写,但这种方案可能会影响并发量。
  • 热点数据可以考虑不失效,后台异步更新缓存,适用于不严格要求缓存一致性的场景。
  • 双key策略,主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值。
  • 构建缓存高可用集群(针对缓存服务故障情况)。
  • 当缓存雪崩发生时,服务熔断、限流、降级等措施保障。

5.2 缓存击穿:缓存雪崩是指只大量热点key同时失效的情况,如果是单个热点key,在不停的扛着大并发,在这个key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样。

从定义上可以看出,缓存击穿和缓存雪崩很类似,只不过是缓存击穿是一个热点key失效,而缓存雪崩是大量热点key失效。因此,可以将缓存击穿看作是缓存雪崩的一个子集
解决方案:
缓存击穿的解决方案:

  • 使用互斥锁(Mutex Key),只让一个线程构建缓存,其他线程等待构建缓存执行完毕,重新从缓存中获取数据。单机通过synchronized或lock来处理,分布式环境采用分布式锁。
  • 热点数据不设置过期时间,后台异步更新缓存,适用于不严格要求缓存一致性的场景。
  • ”提前“使用互斥锁(Mutex Key):在value内部设置一个比缓存(Redis)过期时间短的过期时间标识,当异步线程发现该值快过期时,马上延长内置的这个时间,并重新从数据库加载数据,设置到缓存中去。

redis底层数据结构

底层数据结构一共有6种,分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。



从上面可以看出:
String的底层是(简单动态字符串)
List的底层是(双向链表和压缩链表)
Hash的底层是(压缩链表和哈希表)
Set的底层是(整数数组和哈希表)
Sorted Set底层(压缩链表和跳表)
也就是说

String类型的底层实现只有一种数据结构,也就是简单动态字符串。
而List、Hash、Set和Sorted Set这四种数据类型,都有两种底层实现结构。通常情况下,我们会把这四种类型称为集合类型,它们的特点是一个键对应了一个集合的数据

键和值用什么结构组织?

redis是一个k-v数据库。为了实现从键到值的快速访问,Redis使用了一个哈希表来保存所有键值对。

一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。也就是说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。
不管是键类型还是值类型,哈希桶中的元素保存的都不是值本身,而是指向具体值的指针。
如下图中可以看到,哈希桶中的entry元素中保存了key和value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到


因为这个哈希表保存了所有的键值对,所以,它也叫做全局哈希表。

哈希表的最大好处就是让我们可以用O(1)的时间复杂度来快速查找到键值对----我们只需要计算键的哈希值,就可以知道它对应的哈希桶位置,然后就可以访问相应的entry元素
这个查找过程主要依赖于哈希计算,和数据量的多少并没有直接关系。也就是说,不管哈希表里有10万个键还是 100 万个键,我们只需要一次计算就能找到相应的键。
也就是说,整个数据库就是一个全局hash表,而hash表的时间复杂度就是O(1),只需要计算每个键的hash值,就知道对应的hash桶的位置,定位桶里面的entry找到对应数据,这个也是redis快的原因之一。

但是,如果你只是了解哈希表O(1)复杂度和快速查找特性,那么,当你往redis中写入大量数据之后,就可能发现操作有时候会突然变慢了。原因是哈希表的冲突问题和rehash可能带来的操作阻塞

为什么哈希表操作变慢了?

因为哈希冲突

  • 这里的哈希冲突,也就是指,两个key的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。

  • 毕竟,哈希桶的个数通常要少于key的数量,hash冲突是不可避免

哈希冲突怎么办?

redis解决哈希冲突的方式,就是链式哈希。

  • 所谓的链式哈希,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间一次用指针连接。如下图:
  • entry1、entry2 和 entry3 都需要保存在哈希桶 3 中,导致了哈希冲突。此时,entry1 元素会通过一个next指针指向 entry2,同样,entry2 也会通过next指针指向entry3。这样一来,即使哈希桶 3 中的元素有 100 个,我们也可以通过 entry 元素中的指针,把它们连起来。这就形成了一个链表,也叫作哈希冲突链。
  • 但是,这里依然存在一个问题,哈希冲突链上的元素只能通过指针逐一查找再操作,如果哈希表中写入的数据越来越多,哈希冲突的可能也会越来越多,这就会导致某些哈希冲突链过长,也就是一个哈希桶下面挂了太多的数据的,那么其性能就会下降。

怎么解决呢?

redis对会哈希表做rehash操作
什么是rehash操作呢?rehash也就是增加现有的哈希桶数量,让逐渐增大的entry元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

渐进式rehash

那具体怎么做呢?

其实,为了使得rehash操作更加高效,redis默认使用了两个全局哈希表:哈希表1和哈希表2.一开始,当你刚插入数据时,默认使用哈希表1,此时的哈希表2并没有被分配空间。随着数据逐步增多,redis开始执行rehash,这个过程分为三步:

  • 把哈希表2分配更大的空间,比如是当前哈希表1大小的两倍
  • 把哈希表1中的数据重新映射并拷贝到哈希表2中
  • 释放哈希表1的空间

上面步骤有一个问题,那就是第二步涉及大量的数据拷贝,如果一次性把哈希表1中的数据都迁移完,会造成redis线程阻塞,无法服务其他请求。此时,redis就无法快速访问数据了。
为了避免这个问题,redis采用了渐进式rehash
简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的entries。如下图所示:

zipList 压缩列表

  • 双端链表我们已经熟悉了。但是有一个问题:如果在一个链表节点中存储一个小数据,比如一个字节。那么对应的就要保存头节点,前后指针等额外的数据。
  • 这样就浪费了空间,同时由于反复申请与释放也容易导致内存碎片化。这样内存的使用效率就太低了。
  • 于是,压缩列表上场了!
    当一个列表只有少量数据的时候,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做列表键的底层实现。

ziplist 是由一系列特殊编码的连续内存块组成的顺序型的数据结构,ziplist 中可以包含多个 entry 节点,每个节点可以存放整数或者字符串
ziplist 在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表占用字节数、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

struct ziplist {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}

如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N)


跳表

有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。
具体来说,跳表是在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。如下图:

  • 如果我们要在链表中查找33这个元素,只能从头开始遍历链表,查找6次,直到找到33为止。为此,复杂度是O(N),查找效率很低。

  • 为了提高查找速度,我们来增加一级索引:从第一个元素开始,每两个元素选一个出来作为索引。这些索引再通过指针指向原始的链表。比如,从前两个元素中抽取元素 1 作为一级索引,从第三、四个元素中抽取元素 11 作为一级索引。此时,我们只需要 4 次查找就能定位到元素 33 了。

  • 如果我们还想再快,可以再增加二级索引:从一级索引中,再抽取部分元素作为二级索引。例如,从一级索引中抽取 1、27、100 作为二级索引,二级索引指向一级索引。这样,我们只需要 3 次查找,就能定位到元素 33 了。


  • 从上面可以看出,跳表每一层都有一条有序的链表,最底层的链表包含了所有的元素。这样跳跃表就可以支持在 O(logN) 的时间复杂度里查找到对应的节点。

你可能感兴趣的:(redis)