Redis缓存优化详解


在开发过程中我们最为熟悉的数据库应该就是 MySQLOracle 此类关系型数据库,二者都是将数据存储于硬盘之中。而非关系型数据库 Redis 与之不同的是将数据存储与内存之中,熟悉电脑的小伙伴肯定知道内存读取速度肯定是远远超过硬盘读取速率,所带来的最大区别就是 Redis 读取速度远超传统关系型数据库。

由于读取速度快的特性,Redis 常用于缓存数据,将热点数据缓存以提供访问速度降低服务压力,但 Redis 一样存在一定的局限,也正是将数据存于内存的原因,出于成本考虑其天然就无法进行海量数据存储,通常只能存储一些热点数据。

一、基本介绍

1. 数据结构

Redis 支持多种数据结构,每种数据结构都有不同的用途和特点,主要数据结构如下:

(1) 字符串

字符串类型 (String)Redis 的基本数据结构之一,可以存储任何类型的数据,包括文本、数字等。

(2) 哈希

哈希类型 (Hash) 用于存储键值对集合,每个键都映射到一个值。适合存储对象的字段和值。

(3) 列表

列表类型 (List) 是一个有序的字符串集合,支持在列表的两端进行插入和删除操作。

(4) 集合

集合类型 (Set) 是一个无序的字符串集合,不允许重复的成员。支持集合间的交集、并集、差集等操作。

(5) 有序集合

有序集合类型 (Sorted Set) 和集合类型类似,不同之处在于每个成员都关联了一个分数 (score),用于排序。

(6) 位图

位图类型 (Bitmap) 是一种特殊的字符串类型,用于存储位级别的数据,支持位操作。

(7) HyperLogLog

HyperLogLog 类型用于估计一个集合中不重复元素的数量,通过较少的内存来实现对大数据集的近似计数。

(8) Geospatial

地理位置类型 (Geospatial) 用于存储地理位置信息,支持存储经度和纬度,并提供了诸如计算两个位置之间距离等功能。

二、缓存问题

1. 缓存穿透

缓存穿透是指当用户查询某个不存在的数据时,因 Redis 中不存在该数据,即缓存没有命中,此时查询请求就会转向 MySQL 数据库,结果发现 MySQL 中也不存在该数据,因此只能返回一个空对象代表查询失败。如果这种类请求非常多,或者用户利用这种请求进行恶意攻击,就会给 MySQL 数据库造成很大压力,甚至于崩溃。

缓存穿透对应具体解决的方案如下:

(1) 缓存空对象

Redis 在默认情况下针对空值会进行缓存,这样当发送同一个结果为空的请求也不会重复进行数据库请求,但缺点是会造成 Redis 空间的浪费,需要根据实际情况选择判断。

(2) 布隆过滤器

这里先不讲,下面会进行详细介绍。

2. 缓存击穿

缓存击穿是指用户查询的数据缓存中不存在,但是后端数据库却存在,这种现象出现原因是一般是由缓存中 key 过期导致的。

比如一个热点数据 key,它无时无刻都在接受大量的并发访问,如果某一时刻这个 key 突然失效了,就致使大量的并发请求进入后端数据库,导致其压力瞬间增大。

缓存击穿对应具体解决的方案如下:

(1) 延长过期时间

因为是缓存过期导致的瞬间请求量增加,那么通过将热点数据缓存设置为永不过期即可避免此类情况。

(2) 分布式锁
  • 上锁:当我们通过 key 去查询数据时,首先查询缓存,如果没有就通过分布式锁进行加锁,第一个获取锁的进程进入后端数据库查询,并将查询结果缓到 Redis 中。
  • 解锁:当其他进程发现锁被某个进程占用时,就进入等待状态直至解锁后,其余进程再依次访问被缓存的 key

简单的来说就是类似线程锁,只有一个进程能够进行后台数据访问,其余进程将会等待,当第一个进程读取后台数据后写入 Redis 缓存,此时释放锁则其它进程即可通过缓存读取数据,整个过程中后台数据库仅响应了一次操作。

3. 缓存雪崩

缓存雪崩是指缓存中大批量的 key 同时过期,而此时数据访问量又非常大,从而导致后端数据库压力突然暴增,甚至会挂掉。它和缓存击穿不同之处在于缓存击穿是在并发量特别大时,某一个热点 key 突然过期,而缓存雪崩则是大量的 key 同时过期,因此它们根本不是一个量级。

具体的解决的方案和缓存击穿类似,同样可以设置热点数据永不过期。

三、布隆过滤器

以上面缓存穿透的场景举个例子,我们在查询数据库之前无法得知用户所查询的数据是否存在,若用户使用大量的不存在进行访问将给数据库造成极大的压力。

布隆过滤器即为了应对类似场景,其可以判断用户请求在数据库中是否存在,不存在则跳过数据库访问。但其也存在一定局限性,其存在一定几率的误判,即数据库存在数据但其判断不存在,即对于存在的数据可能存在误判,但如果其判断为不存在那么该数据一定不存在。

为什么会存在误判情况?首先我们需要了解 Bloom 过滤器的实现原理。

1. 数据结构

在之前的数据缓存中的, Redis 是根据用户指定的 Key 直接进行存储,而 Bloom 过滤器是将 Key 多次进行二次哈希算法求值时以二进制形式存于内存之中。

在传统 key value 形式中当需要判断一个数据是否存在时需要根据 key 去对应的内存中查找,而这样做的前提是需要将所有的数据对应的 key 进行存储。对于需用到其对应 value 的数据此类方式无伤大雅,但是对于 value 无用仅需判断是否存在的情况此类方式显然过度浪费。

此处以用户注册为例,当需要判断对应的用户名是否已经被使用,最简单的方法即读取当前数据库中已使用的用户名进行匹配,而为了更好的效率,我们可能会将已存在的用户名存入 Redis 中从而提供检索匹配效率。但当用户数量达到一定量级,将全部的用户名存入 Redis 显然对服务器内存压力是不小的,而使用布隆过滤器则可大大节省内存占用。

首先,让我们来看一个简化版的布隆过滤器实现思路,对于值无关数据若需要实现存在匹配,可以通过哈希算法实现。其实现逻辑为基础的哈希拉链法,即将目标数据经过特定哈希算法计算,将计算得到的数值在定长数组对应位置标识记为 1

如下图示例中对于输入的值 key 经过哈希算法 Hash() 计算后值为 0,则将对应下标位置标识置为 1,当多个 key 的哈希值重复时则采用拉链法,同时定长数组长度越大,重复的概率也就越小。

2. 算法优化

通过上述的方式,即可实现仅通过一个定长数组记录了之前庞大的数据,从而大大节省了内存占用。但是上述也提到了一个重复的问题,虽然采用拉链法可以进行解决,但却增加了复杂度,因此布隆过滤器中采用了多重哈希的实现思路。在之前提到的通过一个哈希算法计算标识,而若采用多重哈希的则将大大降低重复率,即一个 key 使用不同哈希算法计算值,将得到的多个值其对应定长数组标志置为 1

如下图中的 key 经过 Hash1()Hash2() 两个算法计算后得到的值为 01,则将对应定长数组下标为 01 的标志置为 1。当判断一个数据是否存在时,仅需要对其执行对应的多个哈希算法,将得到的值在定长数组中进行检索,若所有对应位置的标识皆为 1 则表明该数据一定存在。

3. 数据误判

通过多重哈希的方式,即将原始数据巧妙的转化为基础的一维数组,从而提高了存储效率。在之前提到了布隆过滤器的实现存在误判的情景,即上述中两个不同的 key 的经过多个哈希算法计算得到的结果却一致,从而导致数据不存在却误判。对于此类情况只需增加哈希算法个数即可减少发生的概率,如由两个哈希算法提升至四个算法,此时重复的概率将进一步降低。

经过上述的步骤,即将复杂的数据转化为一维数组,由因数组存放的数据皆为 01,故可将其替换为字节数组,即每一位存放 1bit 数据,相对于整型的 4bit 进一步节省了空间,布隆过滤器中也正是这么做的,称每一个定长数组为一个 位图,实现逻辑同上此处就不再重复阐述。


文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录