关于redis:深入浅出redis缓存应用

3次阅读

共计 4143 个字符,预计需要花费 11 分钟才能阅读完成。

0.1、索引

https://blog.waterflow.link/articles/1663169309611

1、只读缓存

只读缓存的流程是这样的:

当查问申请过去时,先从 redis 中查问数据,如果有的话就间接返回。如果没有的话,就从数据库查问,并写入到缓存中。

当删改申请过去时,会间接从数据库中删除批改数据,并把 redis 中保留的数据删除。

这样做的益处是,所有最新的数据都在数据库中,而数据库是有数据可靠性保障的。

2、读写缓存

读写缓存的流程是这样的:

  • 当查问申请过去时,先从 redis 中查问数据,如果有的话就间接返回。如果没有的话,就从数据库查问,并写入到缓存中。
  • 当增删改申请过去时,得益于 Redis 的高性能拜访个性,数据的增删改操作能够在缓存中疾速实现,处理结果也会疾速返回给业务利用,这就能够晋升业务利用的响应速度。
  • 然而和只读缓存不同的是,最新的数据都是在 redis 中,一旦呈现掉电宕机,因为 redis 的长久化机制,最新的数据有可能会失落,就会给业务带来危险。

所以,依据业务利用对数据可靠性和缓存性能的不同要求,咱们会有同步直写和异步写回两种策略。其中,同步直写策略优先保证数据可

靠性,而异步写回策略优先提供疾速响应。

2.1、同步直写

当增删改申请过去时,申请到 redis 的同时,也会申请 mysql,等到 redis 和 mysql 都写完数据才会返回数据。

这样,即便缓存宕机或产生故障,最新的数据依然保留在数据库中,这就提供了数据可靠性保障。

然而也会升高缓存的使用性能,因为写缓存很快,然而写数据库就要慢很多,整个的响应工夫就会减少。

2.2、异步写回

异步写回优先思考了响应速度,写到缓存会立刻响应客户端。等到数据要从 redis 中淘汰时,再同步到 mysql。

然而如果产生掉电,数据还是没有写到 mysql,还是有失落的危险。

3、如何抉择

  • 如果须要对写申请进行减速,咱们抉择读写缓存;
  • 如果写申请很少,或者是只须要晋升读申请的响应速度的话,咱们抉择只读缓存。

4、对于一致性

  • 对于读写缓存的异步写回,因为是只写 redis,淘汰时才会写入 mysql,如果产生宕机不能保障一致性
  • 对于读写缓存的同步写回,因为 redis 和 mysql 是同时写,须要退出事物机制,要么都执行要么都不执行,能够保障一致性。(问题:如何保障原子性?当有并发写过来时即便都执行了也可能会不统一,这是就要引入锁保障互斥性)
  • 对于只读缓存,如果产生删改操作,利用既要更新数据库,也要在缓存中删除数据。因为 redis 和 mysql 是同时操作,须要退出事物机制,要么都执行要么都不执行,能够保障一致性。(问题:如何保障原子性?)

4.1、对于只读缓存的一致性问题

先删除缓存,再更新数据库

  • 如果缓存删除胜利,然而数据库更新失败,那么,利用再拜访数据时,缓存中没有数据,就会产生缓存缺失。而后,利用再拜访数库,然而数据库中的值为旧值,利用就拜访到旧值了。
  • 如果线程 A 都胜利了,然而同时另一个线程 B 在线程 A 的这俩个申请两头过去。这个时候缓存曾经删除,然而数据库还是旧值,线程 B 发现没有缓存,就从数据库读读取了旧值更新到 redis 中,而后线程 A 把新值更新到数据库。此时 redis 中是旧值,mysql 中是新值。

先更新数据库,再删除缓存中的值

  • 如果利用先实现了数据库的更新,然而,在删除缓存时失败了,那么,数据库中的值是新值,而缓存中的是旧值,这必定是不统一的。这个时候,如果有其余的并发申请来拜访数据,依照失常的缓存拜访流程,就会先在缓存中查问,但此时,就会读到旧值了。
  • 如果线程 A 删除了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查问缓存时,发现缓存命中,就会间接从缓存中读取旧值。不过,在这种状况下,如果其余线程并发读缓存的申请不多,那么,就不会有很多申请读取到旧值。而且,线程 A 个别也会很快删除缓存值,这样一来,其余线程再次读取时,就会产生缓存缺失,进而从数据库中读取最新值。所以,这种状况对业务的影响较小。( 能够了解为最终一致性,读到旧数据只是临时的,最终都会读到新数据

所以个别我的项目中应用只读缓存,先更新数据库,再删除缓存。这样的代价是最小的,而且尽量保障了一致性。

5、缓存异样

5.1、缓存雪崩

缓存雪崩是指,大量的申请无奈在 redis 中解决(redis 没拦住),间接打到了 mysql,导致数据库压力激增,甚至服务解体。

redis 无奈解决的起因有两种:

缓存中大量数据同时过期

解决方案:

  • 给过期工夫减少一个较小的随机数,过期的数据通过工夫去摊派
  • 服务降级,间接返回错误信息

Redis 缓存实例产生故障宕机了,无奈解决申请,这就会导致大量申请一下子积压到数据库层

解决方案:

  • 服务熔断或者申请限流,redis 客户端间接返回,不会申请到 redis 服务,然而影响范畴比拟大
  • 构建 redis 集群,进步可用性

5.2、缓存击穿

缓存击穿是指,拜访某个热点数据,无奈在缓存中解决,大量申请打到 mysql,导致数据库压力激增,甚至服务解体。

解决方案:

  • 对于频繁拜访的热点数据不设置过期工夫

5.3、缓存穿透

缓存穿透是指,要拜访的数据既不在 redis 中,也不在 mysql 中。申请 redis 发现数据不存在,持续拜访 mysql 发现数据还是不存在,而后也无奈写回缓存,下次持续申请的时候还是会打到 mysql。

解决方案:

  • 缓存空值或者缺省值
  • 应用布隆过滤器

布隆过滤器

布隆过滤器由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,能够用来疾速判断某个数据是否存在(精确说是判断不存在,如果布隆过滤器不存在数据库中肯定不存在,如果布隆过滤器判断存在,数据库不肯定存在,这是布隆过滤器的机制决定的)。当咱们想标记某个数据存在时(例如,数据已被写入数据库),布隆过滤器会通过三个操作实现标记:

  • 首先,应用 N 个哈希函数,别离计算这个数据的哈希值,失去 N 个哈希值。
  • 而后,咱们把这 N 个哈希值对 bit 数组的长度取模,失去每个哈希值在数组中的对应地位。
  • 最初,咱们把对应地位的 bit 位设置为 1,这就实现了在布隆过滤器中标记数据的操作。

如果数据不存在(例如,数据库里没有写入数据),咱们也就没有用布隆过滤器标记过数据,那么,bit 数组对应 bit 位的值依然为 0。

所以当咱们写入数据库时,应用布隆过滤器做个标记。当缓存缺失后,利用查询数据库时,能够通过查问布隆过滤器疾速判断数据是否存在。如果不存在,就不必再去数据库中查问了。

6、利用场景

咱们看下 go-zero 中是如何应用缓存的,go-zero 中应用的只读缓存,当数据有更新删除操作的时候,redis 中的对应 Primary 记录和查问条件记录会同步删除。go-zero 中对某行的缓存,会缓存主键到行记录的缓存,和查问条件(惟一索引)到主键的缓存

咱们看下查问的逻辑(针对的是单行的记录):

  1. 通过查问条件查问某条记录时,如果没有查问条件到主键的缓存
  2. 通过查问条件到 mysql 查问行记录,而后把主键到行记录的缓存,和查问条件(惟一索引)到主键的缓存更新到 redis(前者的过期工夫会多余后者几秒工夫)
  3. 持续回到 1,如果有查问条件到主键的缓存,如果没有主键到记录的缓存,通过主键到 mysql 查问并写入 redis

上面看下 go-zero 源码:

// v - 须要读取的数据对象
// key - 缓存 key
// query - 用来从 DB 读取残缺数据的办法
// cacheVal - 用来写缓存的办法
func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) error,
  cacheVal func(v interface{}) error) error {
  // singleflight 一批申请过去,只容许一个去真正拜访数据,避免缓存击穿
  val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) {
    // 从 cache 里读取数据
    if err := c.doGetCache(key, v); err != nil {
      // 如果是事后放进来的 placeholder(用来避免缓存穿透)的,那么就返回预设的 errNotFound
      // 如果是未知谬误,那么就间接返回,因为咱们不能放弃缓存出错而间接把所有申请去申请 DB,// 这样在高并发的场景下会把 DB 打挂掉的
      if err == errPlaceholder {return nil, c.errNotFound} else if err != c.errNotFound {
        // why we just return the error instead of query from db,
        // because we don't allow the disaster pass to the DBs.
        // fail fast, in case we bring down the dbs.
        return nil, err
      }

      // 申请 DB
      // 如果返回的 error 是 errNotFound,那么咱们就须要在缓存里设置 placeholder,避免缓存穿透
      if err = query(v); err == c.errNotFound {if err = c.setCacheWithNotFound(key); err != nil {logx.Error(err)
        }

        return nil, c.errNotFound
      } else if err != nil {
        // 统计 DB 失败
        c.stat.IncrementDbFails()
        return nil, err
      }

      // 把数据写入缓存
      if err = cacheVal(v); err != nil {logx.Error(err)
      }
    }
  
    // 返回 json 序列化的数据
    return jsonx.Marshal(v)
  })
  if err != nil {return err}
  if fresh {return nil}

  // got the result from previous ongoing query
  c.stat.IncrementTotal()
  c.stat.IncrementHit()

  // 把数据写入到传入的 v 对象里
  return jsonx.Unmarshal(val.([]byte), v)
}

从下面代码咱们能够看到:

  1. 应用 sigleflight 避免缓存击穿
  2. 缓存穿透,应用了占位符,即在 redis 中保留一个空值
正文完
 0