Featured image of post redis之set

redis之set

redis的数据类型set的概述、应用以及数据结构介绍

本文阅读量

redis缓存

概述

为了给用户提供更好的体验,同时也为了减少公司的成本,我们使用redis作为缓存来减轻数据存储层的压力。

当我们数据量增多或者sql语句复杂(大量联表操作、分组运算),如果不加缓存,不但无法满足高并发量,同时也会给数据存储层带来巨大的负担,甚至可能造成数据存储层的数据库宕机,造成整个服务的崩溃。

总的来说,使用缓存好处有以下两点:

  1. 提高读写速度:当数据量加大,查询语句复杂,查询的效率就会直线降低
  2. 减轻存储层的压力

同样的,使用缓存也会带来一些问题:

  1. 存储层与缓存层数据不一致
  2. 代码的维护加大,无形增加了开发人员的工作量

redis使用缓存的场景

  1. String类型:计数器,缓存用户对象,分布式锁,session共享
  2. List类型:不频繁更新排行榜,消息队列
  3. Hash类型:购物车,缓存对象
  4. Zset:实时排行榜,抢票加速包
  5. Set类型:分库分表之后减少复杂查询,黑白名单,帅选功能等

缓存颗粒度

缓存粒度问题是一个容易被忽视的问题,如果使用不当,可能会造成很多无用空间的浪费,网络带宽的浪费,代码通用性较差等情况,需要综合数据通用性、空间占用比、代码维护性三点进行取舍。

一张表中有整整100个字段,那么我们在redis中该怎么去缓存这个表的数据呢?

  1. 缓存该表的全部字段
  2. 缓存部分字段数据

空间方面

  1. 空间消耗方面
  2. 查询传输时的网络消耗
  3. 序列化与反序列化的cpu消耗
数据类型 通用性 空间占比 代码维护
全部字段 简单
部分字段 较为复杂

通用性:缓存全部数据比缓存部分数据更加通用,但从实际经验来看,很长时间内应用只需要几个重要的属性

空间占比:缓存全部数据比缓存部分数据更占空间。可能存在更多的问题。

  1. 全部数据会造成内存的浪费

  2. 全部数据可能每次传输产生的网络流量会比较大,耗时相对较大,在极端情况下会阻塞网络

  3. 全部数据进行序列化与反序列化CPU消耗很大。

代码维护:全部数据的优势更加明显,部分数据一旦要加新字段需要修改业务代码,修改后还需要更新缓存的数据。

缓存问题

缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的—致性要求很高,那么就不能使用缓存。

另外的一些典型问题就是,缓存击穿、缓存穿透和缓存雪崩。目前,业界也都有比较流行的解决方案。

缓存击穿

概念

极个别热点数据key,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且会写缓存,会导致数据库瞬间压力过大。

解决方案:互斥锁

分布式锁∶使用分布式锁,保证对于每个key同时只有一个线程去査询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

尝试使用lua脚本+互斥锁来进行解决

(1) 就是在缓存失效的时候(判断拿出来的值为空),不是立即去加载数据库。

(2) 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key

(3) 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;

(4) 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。

解决方案:设置缓存永不过期

从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题

物理上面不设置永不过期,但是逻辑上还是要设置过期时间的

设置永不过期这里包含两层意思:

  1. 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是物理过期。
  2. 从功能层面看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

从实战方面来说,设置热点数据永不过期有效杜绝了热点key产生的问题,但是不足的点在于重构缓存期间,会出现数据不一致性的情况,这种情况在应用中是坚决不允许出现的。

解决数据不一致的问题可以是每次查询的时候都去判断下是否超时,超时就立刻更新缓存数据。

缓存穿透

概念

缓存穿透是指查询一个不存在的数据,缓存层与存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层

我们通常作缓存步骤:

  1. 缓存层不命中
  2. 存储层不命中,不将空结果写回缓存
  3. 返回空结果

缓存穿透将导致不存在的数据每次请求都需要到存储层去查询,失去了缓存保护后端存储的意义。缓存穿透问题会可能使后端存储负载加大,由于很多存储不具备高并发性,甚至可能造成后端存储宕机,通常可以在程序中分别统计总调用数,缓存层命中数,存储层命中数,如果发现大量存储层命中为空,可能就是出现了缓存穿透问题。

出现的原因

  1. 自身的业务代码或者数据出现问题
  2. 一些恶意攻击,爬虫等造成大量空命中

解决方案:缓存空对象

当缓存不命中后,将空对象作为数据保留到缓存中,之后再来访问这个数据将会从缓存中进行获取,这样就保护的后端数据源,不会因为缓存穿透,导致大量请求到存储层,发生服务器宕机,或者拖累其他应用。

但是这种方法会存在两个问题:

  1. 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键(如果是属于恶意攻击,消耗会更大),因为这当中可能会有很多的空值的键,比较有效防止的方式是针对这类数据设置一个较短的过期时间,让他自动删除。

  2. 缓存层与存储层的数据会出现一段时间的数据不一致,会对业务出现一定的影响(可以开启一个消息队列,事件在新增的时候异步更新数据)

    比如:此时我们对这个空数据设置了一个过期时间为5分钟,但是这个时候存储层添加了这条数据,那此段时间就会出现缓存层与存储层数据不一致的情况,针对这种情况我们可以使用消息系统或者其他方式消除缓存层存储的空对象。

解决方案:使用布隆过滤器

布隆过滤器是—种数据结构,对所有可能査询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;

可以使用redis里的布隆过滤器插件来实现

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)

将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

https://cloud.tencent.com/developer/article/1975700

解决方案:白名单

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis宕机!通常出现在缓存服务器重启或者大量缓存集中在某一个时间段失效

由于缓存层承载着大量的请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮

预防方案

  1. 保证缓存层服务高可用性。和汽车一样都有多个轮胎一样,如果缓存层设计成高可用的,即使个别节点,个别机器甚至是机房宕掉,依然可以提供服务,例如(Redis哨兵)Redis Sentinel和(Redis集群)Reis Cluster都实现了高可用。同时也要做到异地多活。

    构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)

  2. 后端限流并降级。限流是只允许一定时间内多少用户来进行访问,溢出的我们就直接打回,避免全部负载全部到到mysql服务器,降级是牺牲一些业务,提升mysql服务器请求处理容量。

    限流:对某个key只允许一个线程查询数据和写缓存,其他线程等待

    降级:关掉一些不重要的模块(查看订单,查看物流,退款等)

  3. 过期时间错开。雪崩一般是指同一时刻出现大批量的数据过期导致请求全部打到mysql服务器,到服务器宕机,那么我们可以通过限制缓存的过期时间来进行预防

  4. 数据加热,就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

使用 Hugo 构建
主题 StackJimmy 设计