参考
link

Redis是什么

简单来说 Redis 就是一个使用 C 语言开发的,开源的高性能key-value非关系缓存数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,所以读写速度非常快,每秒可以处理超过 10万次读写操作,因此 Redis 被广泛应用于缓存方向。

另外,Redis 除了做分布式缓存之外,也经常用来做分布式锁,甚至是消息队列。

Redis 提供了多种数据类型(字符串、列表、集合、散列表、有序集合 zset)来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。

Redis用途 应用场景

  • 分布式缓存:性能key-value非关系缓存数据库 支持集群模式
  • 分布式锁 : 通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。
  • 限流 :一般是通过 Redis + Lua 脚本的方式来实现限流。
  • 消息队列 :Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
  • 复杂业务场景 :通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。 如排行榜 计算器应用

Redis 常见数据结构以及使用场景分析

  • string
    Redis自己构建了一种 简单动态字符串(simple dynamic string,SDS)
    相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N))
    应用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
  • list
    Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
    应用场景: 发布与订阅或者说消息队列、慢查询。
  • hash
    hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,内部实现类似于数组+链表,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
    应用场景: 系统中对象数据的存储。
  • set
    set 类似于 Java 中的 HashSet
    应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景,
    比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
  • sorted set
    和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。
    应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
  • bitmap
    bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 bitmap 本身会极大的节省储存空间。
    应用场景: 适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。

Redis 单线程模型

Redis 通过IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。

Redis多线程知识

  • Redis 4.0加入了对多线程的支持,增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主处理之外的其他线程来“异步处理”。
  • Redis 6.0 之前主要还是单线程处理
  • Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,但是Redis 的瓶颈主要受限于内存和网络。
  • Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
  • Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 redis.conf,且还需要设置线程数,否则是不生效的。

redis.conf配置文件

io-threads-do-reads yes
io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

Redis6.0 之前 为什么不使用多线程

  1. 单线程编程容易并且更容易维护;
  2. Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
  3. 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。

但后来Redis6.0后引入多线程是为了提高网络 IO 读写性能

Redis 给缓存数据设置过期时间的作用

  • 因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory
  • 很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在, 短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效。

Redis 是如何判断数据是否过期的

Redis 通过一个叫做**过期字典(可以看作是 hash 表)**来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

过期字典是存储在 redisDb 这个结构里的

java redis 流控 redis流式处理_redis

过期的数据的删除策略

  • 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
  • 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。 定期删除对内存更加友好,惰性删除对 CPU 更加友好。

两者各有千秋,所以 Redis 采用的是 定期删除+惰性删除 。

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。

怎么解决这个问题呢?答案就是:Redis 内存淘汰机制

Redis 内存淘汰机制

allkey LRU LFU Random
volatile LRU LFU Random
再加两个
noeviction 写入报错
volatile ttl 有更早过期时间的key优先移除

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的key选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的key选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

4.0 版本后增加以下两种:

  • volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰

  • allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

这里Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略是用于处理内存不足时的需要申请额外空间的数据;而过期策略用于处理过期的缓存数据。

Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)

  • Redis 默认的一种持久化方式叫快照(snapshotting,RDB)
    Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本,产生的数据文件为dump.rdb。
  • 另一种方式是只追加文件(append-only file, AOF)
    则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化, AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。

Redis 事务

  • Redis 事务提供了一种将多个命令(MULTI,EXEC,DISCARD 和 WATCH)请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
  • Redis的事务和数据库的ACID事务不一样
    Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。

Redis 除了做分布式缓存,还能做什么

  • 分布式锁 : 通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。
  • 限流 :一般是通过 Redis + Lua 脚本的方式来实现限流。
  • 消息队列 :Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
  • 复杂业务场景 :通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。

分布式缓存常见的技术选型方案有哪些?

分布式缓存的话,使用的比较多的主要是 Memcached 和 Redis。

Redis 和 Memcached 的区别和共同点

  • 共同点 :
  1. 都是基于内存的数据库,一般都用来当做缓存使用。
  2. 都有过期策略。
  3. 两者的性能都非常高。
  • 区别 :
  1. Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
  2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中
  3. Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
  4. Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
  5. Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的
  6. Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
  7. Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言
  8. Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。

为什么要用 Redis/为什么要用缓存?

主要从“高性能”和“高并发”这两点来看待这个问题。

  • 高性能
    假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
  • 高并发
    直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

什么是缓存击穿、缓存穿透、缓存雪崩

缓存穿透

大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
这个请求的数据可能也不在数据库中

如何避免缓存穿透

  • 1.如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
  • 2.如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)
  • 3.使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。

布隆过滤器原理:它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。

缓存雪奔

指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。

出现场景:
举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。
还有一种缓存雪崩的场景是:有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。

解决办法

**针对 Redis 服务不可用的情况: **

  • 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  • 限流,避免同时处理大量的请求。

针对热点缓存失效的情况:

  • 设置不同的失效时间比如随机设置缓存的失效时间。
  • 缓存永不失效。

缓存击穿

指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。

缓存击穿看着有点像,其实它两区别是,缓存雪奔是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。

解决办法

  1. 使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
  2. “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。

什么是热Key问题,如何解决热key问题

什么是热Key问题?

在Redis中,我们把访问频率高的key,称为热点key
如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。

如何解决热key问题?

  • Redis集群扩容:增加分片副本,均衡读流量;
  • 将热key分散到不同的服务器中;
  • 使用二级缓存,即JVM本地缓存,减少Redis的读请求。

缓存一致性 如何保证Redis缓存和数据库的一致性