Redis

说说什么是Redis?

Redis是一种基于键值对(key-value)的NoSQL数据库。

比一般键值对数据库强大的地方,Redis会将所有数据都存放在内存中,所以它的读写性能非常出色。

Redis中的value支持string(字符串)、hash(哈希)、 list(列表)、set(集合)、zset(有序集合)、HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)等多种数据结构,因此 Redis可以满足很多的应用场景。

不仅如此,Redis还可以将内存的数据利用快照和日志的形式保存到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据不会“丢失”。

除了上述功能以外,Redis还提供了键过期、发布订阅、事务、流水线、Lua脚本等附加功能。

总之,Redis是一款强大的性能利器。

Redis可以用来干什么?

1. 缓存

这是Redis应用最广泛地方,基本所有的Web应用都会使用Redis作为缓存,来降低数据源压力,提高响应速度。

2. 计数器

Redis天然支持计数功能,而且计数性能非常好,可以用来记录浏览量、点赞量等等。

3. 排行榜

Redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。

4. 社交网络

赞/踩、粉丝、共同好友/喜好、推送、下拉刷新。

5. 消息队列

Redis提供了发布订阅功能和阻塞队列的功能,可以满足一般消息队列功能。

6. 分布式锁

分布式环境下,利用Redis实现分布式锁,也是Redis常见的应用。

Redis的应用一般会结合项目去问,以一个电商项目的用户服务为例:

  • Token存储:用户登录成功之后,使用Redis存储Token

  • 登录失败次数计数:使用Redis计数,登录失败超过一定次数,锁定账号

  • 地址缓存:对省市区数据的缓存

  • 分布式锁:分布式环境下登录、注册等操作加分布式锁

  • ……

Redis 有哪些数据结构?

Redis有五种基本数据结构。

string

字符串最基础的数据结构。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字 (整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。

字符串主要有以下几个典型使用场景:

  • 缓存功能

  • 计数

  • 共享Session

  • 限速

hash

哈希类型是指键值本身又是一个键值对结构。

哈希主要有以下典型应用场景:

  • 缓存用户信息缓存对象

list

列表(list)类型是用来存储多个有序的字符串。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色。

列表主要有以下几种使用场景:

  • 消息队列

  • 文章列表

set

集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的。

集合主要有如下使用场景:

  • 标签(tag)

  • 共同关注

sorted set

有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个权重(score)作为排序的依据。

有序集合主要应用场景:

  • 用户点赞统计

  • 用户排序

Redis为什么快呢?

Redis的速度非常的快,单机的Redis就可以⽀撑每秒十几万的并发,相对于MySQL来说,性能是MySQL的几十倍。速度快的原因主要有:

  1. 内存存储:Redis 将数据存储在内存中,相比于传统的基于磁盘的数据库,内存访问速度更快,能够提供更高的读写性能。
  2. 单线程架构:Redis 是单线程的,避免了线程上下文切换的开销,同时也避免了多线程并发访问数据时的锁竞争问题。
  3. 高效的数据结构:Redis 支持多种高效的数据结构,如字符串、哈希表、列表、集合和有序集合等,这些数据结构都是基于内存的,能够快速地进行读写操作。
  4. 基于非阻塞的IO多路复⽤机制:当一个程序需要同时处理多个 I/O 事件时,可以使用 I/O 多路复用技术。它允许一个进程同时监视多个文件描述符,等待它们中的任何一个变为可读或可写状态,从而实现并发 I/O 操作。常见的 I/O 多路复用机制有 select、poll 和 epoll。在 Linux 系统中,epoll 是最常用的 I/O 多路复用机制,因为它具有更高的性能和可扩展性,可以处理大量的并发连接。使用 I/O 多路复用技术可以避免阻塞和线程创建销毁的开销,提高程序的性能和可伸缩性。
  5. 持久化机制:Redis 支持多种持久化机制,如快照和 AOF(Append Only File)方式,可以将内存中的数据保存到磁盘中,保证了数据的可靠性和持久性。

Redis为什么早期选择单线程?

官方FAQ表示,因为Redis是基于内存的操作,CPU成为Redis的瓶颈的情况很少见,Redis的瓶颈最有可能是内存的大小或者网络限制。

如果想要最大程度利用CPU,可以在一台机器上启动多个Redis实例。

PS:网上有这样的回答,吐槽官方的解释有些敷衍,其实就是历史原因,开发者嫌多线程麻烦,后来这个CPU的利用问题就被抛给了使用者。

同时FAQ里还提到了, Redis 4.0 之后开始变成多线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 Key 的删除等等。

Redis6.0使用多线程是怎么回事?

Redis不是说用单线程的吗?怎么6.0成了多线程的?

Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的。

这样做的⽬的是因为Redis的性能瓶颈在于⽹络IO⽽⾮CPU,使⽤多线程能提升IO读写的效率,从⽽整体提⾼Redis的性能。

持久化

Redis是一种基于内存的键值数据库,可以持久化存储数据,以便在重启或崩溃后恢复数据。Redis提供了两种持久化方式:RDB(Redis DataBase)和AOF(Append Only File)。

RDB

RDB是一种快照(snapshot)持久化方式,可以定期保存Redis数据的某个时间点的快照。当Redis发生宕机等异常情况时,可以使用最近的RDB文件进行数据恢复。

RDB文件是一种二进制文件,包含了Redis的内存数据在某个时间点的快照。RDB文件的创建可以通过配置文件中的save选项设置触发时间点,也可以通过调用SAVE或BGSAVE命令手动创建。

优点:

  • RDB文件保存了Redis某个时间点的数据,非常适合用于备份、灾难恢复等场景。
  • RDB文件是二进制格式,非常紧凑,不需要进行解析,非常适合长期存储。

缺点:

  • RDB文件保存的是某个时间点的快照,如果Redis在快照之后发生崩溃,将会丢失这个时间点之后的数据。
  • RDB文件需要定期创建和保存,如果数据量非常大,保存RDB文件会对Redis的性能产生影响。

AOF

AOF是一种追加(append-only)持久化方式,会在Redis每次执行写操作时将操作记录追加到AOF文件中。当Redis发生异常情况时,可以使用AOF文件进行数据恢复。

AOF文件是一个文本文件,保存了所有Redis写操作的追加记录。AOF文件的创建可以通过配置文件中的appendonly选项设置,也可以通过调用BGREWRITEAOF命令手动创建。

优点:

  • AOF文件保存了Redis执行过的所有写操作,非常适合用于数据恢复。
  • AOF文件是一个文本文件,易于查看和维护。

缺点:

  • AOF文件相比RDB文件更加消耗磁盘空间,特别是当Redis进行大量写操作时,AOF文件会快速增长。
  • AOF文件相比RDB文件更加消耗CPU资源,特别是当Redis进行大量写操作时,AOF文件的追加写操作会占用较高的CPU资源。

同时,Redis也提供了AOF和RDB的混合持久化方式,即在配置文件中同时开启AOF和RDB选项,这样就可以在发生异常情况时,既可以使用AOF文件进行数据恢复,也可以使用RDB文件进行快速数据恢复。

讲一下缓存击穿、缓存穿透、缓存雪崩

缓存击穿

缓存击穿指的是一个缓存中不存在但是数据库中存在的数据,在高并发的情况下,多个请求同时查询该数据,由于缓存中没有该数据,导致这些请求都穿透缓存直接访问数据库,造成数据库压力过大。

缓存击穿的解决方案包括:

  • 设置热点数据永不过期或者过期时间比较长。
  • 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  • 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
  • 多个进程使用分布式锁,保证同一时间只有一个进程能够获得锁,从而访问共享资源,其他请求等待结果。

缓存穿透

缓存穿透指的是查询一个不存在的数据,由于缓存中没有这个数据,所以每次都要去请求数据库,这时如果请求并发量过大,就会导致数据库崩溃。

缓存穿透的解决方案包括:

  • 使用布隆过滤器,过滤掉不存在的请求。

    • 布隆过滤器

      布隆过滤器(Bloom Filter)是一种快速、高效的数据结构,用于判断一个元素是否存在于一个集合中。它的基本思想是使用多个哈希函数对元素进行哈希,将哈希值对应的位数组中的对应位置标记为1。在判断一个元素是否存在于集合中时,只需要对该元素进行哈希,并检查哈希值对应的位数组中的对应位置是否都为1,如果都为1,则认为该元素存在于集合中,否则认为不存在。

      布隆过滤器具有以下优点:

      1. 快速:布隆过滤器的查询和插入操作都只需要进行哈希和位运算,时间复杂度很低。
      2. 空间效率高:布隆过滤器只需要使用一个位数组和多个哈希函数,占用的内存很少。
      3. 可扩展性好:布隆过滤器可以通过动态添加哈希函数或扩大位数组的方式进行扩展,以适应不同的应用场景。

      但是,布隆过滤器也有一些缺点,主要包括:

      1. 误判率较高:由于布隆过滤器使用多个哈希函数进行哈希,每个元素都会被多个位标记为1,因此可能会导致误判。
      2. 不能删除元素:由于一个元素可能被多个位标记为1,因此无法删除某个元素,除非清空整个位数组。

      在实际应用中,布隆过滤器常常被用于解决海量数据的判重问题,例如爬虫去重、垃圾邮件过滤、URL去重等。它可以有效地降低数据存储和查询的成本,并提高系统性能。

      代码实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      import java.util.BitSet;
      import java.util.Random;

      public class BloomFilter {

      private final BitSet bitSet;
      private final int bitSetSize;
      private final int expectedNumberOfElements;
      private final int numberOfHashFunctions;
      private final Random random = new Random();

      /**
      * 构造函数
      *
      * @param expectedNumberOfElements 预计要插入的元素数量
      * @param falsePositiveProbability 期望的误判率
      */
      public BloomFilter(int expectedNumberOfElements, double falsePositiveProbability) {
      this.expectedNumberOfElements = expectedNumberOfElements;
      this.bitSetSize = (int) Math.ceil(expectedNumberOfElements / (-Math.log(2) * Math.log(1 - Math.pow(falsePositiveProbability, 1.0 / expectedNumberOfElements))));
      this.numberOfHashFunctions = (int) Math.ceil(Math.log(2) * bitSetSize / expectedNumberOfElements);
      this.bitSet = new BitSet(bitSetSize);
      }

      /**
      * 添加元素
      *
      * @param element 元素
      */
      public void add(String element) {
      for (int i = 0; i < numberOfHashFunctions; i++) {
      int hash = getHash(element, i);
      bitSet.set(hash, true);
      }
      }

      /**
      * 是否可能包含元素
      *
      * @param element 元素
      * @return true/false
      */
      public boolean contains(String element) {
      for (int i = 0; i < numberOfHashFunctions; i++) {
      int hash = getHash(element, i);
      if (!bitSet.get(hash)) {
      return false;
      }
      }
      return true;
      }

      /**
      * 获取哈希值
      *
      * @param element 元素
      * @param index 下标
      * @return 哈希值
      */
      private int getHash(String element, int index) {
      return Math.abs(random.nextInt() ^ element.hashCode() ^ index);
      }
      }

      在该实现中,BloomFilter类的构造函数接收预计要插入的元素数量和期望的误判率作为参数,然后计算出位数组的长度、哈希函数的数量等信息。在添加元素时,会使用多个哈希函数对元素进行哈希,并将对应的位数组位置设为true。在检查元素是否存在时,同样会使用多个哈希函数对元素进行哈希,并检查对应的位数组位置是否为true。如果所有的位数组位置都为true,就认为元素存在于布隆过滤器中。

      注意,该示例只是一个简单的实现,并没有考虑并发安全等问题。实际应用中,需要考虑多线程并发访问、分布式环境下的数据一致性等问题。

  • 将不存在的数据也缓存起来,并设置一个较短的过期时间。

缓存雪崩

缓存雪崩指的是缓存中大量的数据同时失效,导致请求都落到了数据库上,引起数据库压力过大,甚至崩溃。

缓存雪崩的解决方案包括:

  • 使用分布式缓存,并将数据的过期时间随机分布在一个时间段内,避免大量的数据同时失效。
  • 设置热点数据的永不过期策略,避免热点数据被缓存删除。
  • 使用限流等措施,避免请求过多。

另外,对于缓存问题,还有一些其他的缓存优化技巧:

  • 使用本地缓存和分布式缓存相结合的方式,提高缓存命中率。
  • 使用多级缓存,避免缓存的单点故障。
  • 使用缓存预热技术,提前将常用的数据加载到缓存中。
  • 使用缓存的持久化技术,保证缓存的数据不会因服务器宕机而丢失。
  • 使用缓存的监控技术,及时发现和解决缓存故障。

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

  1. 读写双写:在写入数据库时,同时将数据写入Redis缓存中。在读取数据时,先从Redis缓存中获取数据,如果缓存中没有,则从数据库中读取数据,并将读取到的数据写入缓存中。
  2. 缓存失效机制:在写入数据时,设置缓存过期时间,当缓存过期后,再从数据库中读取数据,并将读取到的数据写入缓存中。
  3. 监听数据变化:使用Redis的发布/订阅功能,监听数据库中数据的变化,当数据发生变化时,使缓存中的数据失效,下次读取数据时,从数据库中读取最新的数据。
  4. 分布式锁:在更新缓存和数据库数据时,使用分布式锁来保证同一时间只有一个线程可以进行更新操作,避免产生并发问题。

怎么基于Redis实现分布式锁

使用Redis的setnx命令来实现分布式锁。当一个线程需要获取锁时,它会尝试在Redis中设置一个指定的key,如果设置成功,则说明该线程获取了锁,否则说明锁已被其他线程占用。当该线程释放锁时,需要删除这个key。

0%