1.什么是Redis

1.1 简介

image.png

Redis是一个开源的,基于键值对key-value的NoSQL数据库。Redis内部存放的值可以是string、hash、list、set、zset、bitmaps、GEO等多种数据结构或算法;除了数据存放,Redis还提供了键值过期、订阅发布、事务、流水线、Lua脚本等附加功能,因此Redis能满足很多应用场景。

image.png

Redis会将数据存放在内存中,所以Redis的检索速率非常快(或者说IO吞吐高)。同时,为了保证数据不丢失,Redis会将内存中的数据用快照或者日志的方式存在硬盘上,实现一定程度上的数据持久化。

1.2 什么时候用Redis?

在编程语言中定义变量,也是将数据存放在内存中。Redis的优势是在分布式集群系统中,而单机(单进程)程序里面,直接在编程语言中定义变量/数据结构是更好的选择(Redis的快只是相对于MySQL这类数据库而言的)。

  • 如果未来需要引入分布式架构,使用Redis是一个好选择。否则没有必要。

如果一个系统需要涉及到多个变量、多台主机时,Redis就能起作用了。它基于TCP,可以将自己内存中的变量分享给多个进程、多个主机来访问,这样分布式系统之间就能进行数据同步了。

又因为Redis是在内存中存放,所以它的检索速度相比MySQL会有更快的查询速度。在互联网应用中对数据库的查询性能要求较高,所以Redis就比MySQL更加适合这种场景。

比如存储浏览器中的用户Cookie和Session的对应关系,这里还有一个特点,就是session并不是必须要永久保存的数据,我们能允许一定的数据丢失,后果无非是用户需要重新登录网页而已。

当然,现在更多的选择是将Redis和MySQL等数据库结合,将热点数据(最常访问的数据)放入Redis,全量数据在MySQL中。此时Redis就好比一个数据的缓存了。

  • 保证热点数据的查询性能;
  • 保证全量数据的正确保存;
  • 但是会出现Redis和MySQL中增删改的数据同步问题;
  • 使用Redis+MySQL会大大增加系统复杂性。

1.3 Redis不能做的事情?

因为Redis是内存存储,所以它不适合存储大规模数据。

2.分布式

前面提到,Redis的真正适用场景是分布式系统。那么什么是分布式系统呢?

2.1 单机架构

所谓单机架构,就是一个系统的所有服务都在一台服务器中运行。

如下图所示,假设这是一个简单的电商系统,它的数据库和应用程序都在同一台服务器上。用户访问这个服务器来获取电商服务。

image.png

之前我写的小项目视频点播系统,也是一个单机架构。采用MySQL作为数据库,C++来提供应用服务。

如果一台主机的性能能足够满足我们的用户服务需求了,单机架构就是很不错的选择,因为它的维护复杂度不高,且部署成本也较低。

2.2 分布式

主机的性能指标包括IO性能(内存和硬盘)、网络吞吐、CPU计算性能等,这些性能都会在用户量增长到某一个量级时顶不住,导致服务的请求耗时较长甚至出现处理错误,用户体验差。

此时我们可以采取两个措施,开源节流:

  • 开源:给主机升级硬件
  • 节流:通过软件debug找到影响系统性能的部分,针对性进行软件优化;

但是,一台主机的性能,不管再怎么升级硬件,总归是有上限的。如果一个主机都被升级到顶天了还没有办法服务我们的性能,此时就需要多台主机来帮忙了。这便是分布式系统的基础。

  • 引入分布式其实是个万不得已的操作;
  • 会导致系统复杂度大大提高;
  • 系统出现BUG的可能性越来越高;
  • 程序员被“开猿节流”的可能性越来越高……

2023年年末,各大互联网厂家(阿里、滴滴)旗下的服务挨个boom,正是说明了这一点……

先来看看一个最简单的分布式系统吧,即将应用服务和数据库服务分离。

  • 应用服务器:CPU和内存性能,提高处理速度;
  • 存储服务器:更大更快的硬盘,增加IO吞吐量;

image.png

2.3 负载均衡

进一步扩展,就需要更多的主机,以及负载均衡措施了。

  • 一个负载均衡服务器和两个独立的应用服务器;
  • 用户请求负载均衡服务器;
  • 负载均衡服务器根据两个应用服务器当前的负载,选择较低的服务器来处理请求(这里涉及到负载均衡的算法);

此时两个应用服务器在一定程度上都干了相同的活,就能减少单个机器的压力。

image.png

注意,虽然负载均衡服务器看上去会接受所有用户的请求,但它的请求承担能力是远远大于应用服务器的。因为它的工作很简单,只是一个简单的识别和分配工作,并不涉及到具体的功能。

如果请求量大到负载均衡服务器也顶不住了,那可以添加多个负载均衡服务器(可以采用多地节点的方式)。

添加上负载均衡措施后,就有了服务集群,这个集群中某个服务器挂掉对整个服务器的影响不高(它的请求可以由其他正常运行的服务器承担)一定程度上提高了可用性。

另外,负载均衡服务器也需要使用一些算法,比如想办法让同一个用户的请求始终走到同一个应用服务器上,避免在不同应用服务器中进行数据交换。

2.4 数据库读写分离

前面提到的措施都是在优化应用服务器的负载,那么数据库服务器的负载应该如何降低呢?

比较简单的一种办法就是数据库的读写分离。即读数据库的操作和写数据库的操作由不同的服务器来承担。

  • 应用程序需要写入,请求A服务器
  • A服务器处理写入后,定期同步到B服务器
  • 应用程序需要读取时请求B服务器

考虑到实际场景中,读取的频率会远远高于写入,在这种情况下,读取服务器B大部分情况下只需要处理读请求,压力会降低。而且我们可以提供多个和B一样的读服务器,加上负载均衡,进一步减少读服务器的压力。

为什么说读的频率远远高于写入?以聊天软件为例:

  • 当用户打开聊天软件的主界面时,软件需要对当前聊天框里的所有联系人/群组里面的消息进行一次查询;
  • 当用户打开自己的个人信息时,软件需要查询个人信息(当然这个信息可能是本地早就缓存好了的);
  • 当用户打开朋友圈的时候,需要查询好友的朋友圈,并把最新的说说推送给当前用户
  • ……

上述的这一系列操作都是读查询,完全没有涉及到写。当用户使用聊天软件,没有发出消息时,聊天软件一直在做的都是读操作!以此类推,大部分软件在使用的时候,都是读操作居多,写操作相对较少。

2.5 热点缓存

有一个经典的二八原则,在数据库场景也能适用:20%的数据能支撑80%的访问。

所以我们可以引入针对这20%的热点数据的缓存,保证这些热点数据能更快的被查询(读)。Redis可以说就是用来做缓存的。

缓存需要更快的查询速度,代价就是它存放不了太多的数据。比如Redis需要将数据存入内存中,内存的容量远远小于磁盘,存不了很多数据。所以缓存一定要使用适合场景的算法,保证缓存中的数据有尽可能高的命中率。

当然,这样会导致缓存服务器会承受较大的查询压力。不过完整数据库的查询压力又被进一步减少了。

2.6 分库分表

当一个服务器存储不下当前系统的全量数据时,就需要进行分库分表了。即将数据库进行一定的拆分,存放在多个服务器中。

如下图所示,将原本一个数据库的不同表,放在不同服务器中。这样一个服务器上就只用存放一部分数据,减少了存储容量的同时,也减少了单个服务器在查询方面的压力。同时,我们可以为某个表建立一个存储的集群,这也是依照负载均衡的基本思路来扩展主从。

如果一个表过大,没有办法放入一台主机,也可以根据表的主键来将该表拆分放入不同的服务器。比如服务器A放入主键1到10万,服务器B放入10万零1到20万等等……

这个拆分可以一直持续下去,直到性能满足了业务的需要。具体的拆分方式也是根据具体业务来进行处理的。

image.png

分布式的处理其实都是基于具体业务的需要来分析的。业务决定了我们应该选取的技术解决方案。

2.7 微服务架构

前面提到的处理都是在服务器拆分、数据库拆分上处理的。我们的应用服务器依旧是一个服务器能完成这个业务的所有任务。这样会导致该应用服务器的代码非常庞大,且难以进行维护。

我们可以将整个系统根据功能进行拆分,用户管理是一个应用服务器、商品交易是一个应用服务器、交易处理也是一个应用服务器,在不同的服务器之间可以通过消息中间件之类的方式进行数据通信。

而诸如安全处理等大家都用得上的公共服务,可以单开一个进行处理。

image.png

实际上,微服务是在解决团队协作的问题。单一应用服务器在规模大了之后会非常非常复杂,而使用微服务架构,可以将团队按照业务拆分成不同的小组,每个小组只需要维护产品中的一部分服务,这样维护单个服务的难度就降低了,更重要的是,企业的组织架构的分配变得更加方便。

微服务的优势

微服务的优势:

  • 解决了团队组织架构分配的问题
  • 方便代码功能的复用(比如不同服务它的用户管理模块的代码其实是差别不大的,方便进行复用)
  • 不同的服务,可以进行针对性的部署(比如某些服务的性能需求不高,就可以部署在配置相对较差的服务器上)

微服务的缺点

微服务架构对应用服务的拆分会进一步扩大整个系统的复杂性,反正就是越拆越复杂。缺点如下:

  • 系统响应速度降低:因为不同的模块是在不同的服务集群上处理,它们之间需要使用网络进行通信,相比于单个完整的应用服务器,整个系统的响应速度降低了。要想保证系统响应速度降低幅度小,就得引入更多的硬件资源来处理(更快的网卡)
  • 系统出现问题概率提高:因为微服务会引入更多的机器,导致出现问题的概率被提高。需要更多更详细的监控和告警方式来保证多台云服务器的可用性。

3.分布式中涉及到的相关概念

3.1 应用Application/系统System

一个应用(系统)就是一个或者一组服务器程序

3.2 模块Module/组件Componet

应用中的不同功能被拆分成不同的模块,共同组成最终的应用程序

3.3 分布式Distributed

引入多个服务器/主机,协同配合完成一系列工作(物理上的多个主机)

3.4 集群Cluster

引入多个服务器/主机,协同配合完成一系列工作(逻辑上的多个主机)

3.5 主Master/从Slave

多个服务器节点中,一个是主节点,其他是从节点。从节点需要从主节点中同步数据。

3.6 中间件Middleware

和具体业务无关,但在系统中需要使用的服务。比如数据库、缓存、消息队列等。

3.7 可用性Availability

系统整体可用时间/总时间

三个9代表可用性是99.999%

3.8 响应时间Response Time RT

用于衡量服务器的性能,响应时间越快越好

3.9 吞吐Throughput和并发Concurrent

衡量系统处理请求的能力,也是衡量性能的方式之一。

4.Redis的特性

我们可以在redis的官网上看到redis的特性,一个一个来认识一下吧

image-20240210135822201

4.1 In-memory data structures

前面已经提到过,Redis是一个KV方式的内存级数据库,它的Key都是string,value可以是string、hash、list、set、zset、bitmaps、GEO等各种数据结构。

使用Key-Value方式来存放数据的数据库,被称为非关系数据库,NoSQL也是类似的含义。

4.2 Progarmmability

针对Redis的操作,可以通过简单的交互式命令来存放数据,也可以通过脚本来批量执行一些操作。

4.3 Extentsibility

Redis提供了一系列API,可以在Redis原有功能的基础上配置扩展,比如让Redis支持更多数据结构。支持C/C++/Rust语言。

4.4 Persistence

Redis是将数据存放在内存中的,但也提供了持久化方式来保证系统重启时数据不会丢失。

4.5 Clustering

Redis作为分布式系统的中间件,原生支持集群部署。

Horizontal scalability 指的是系统的水平扩展能力,类似于“分库分表”。

4.6 High availability

Redis原生支持主从结构,从节点相当于主节点的备份,主节点失效时从节点能立马顶上去(替补)。

4.7 天下武功唯快不破

Redis能被广泛的使用,最大的原因还是因为它很快!

  1. Redis数据在内存中,比访问硬盘的数据库快的多;
  2. Redis的核心功能逻辑较为简单,只是操作内存中的数据结构;
  3. Redis使用了IO多路复用(epoll)来处理网络请求;
  4. Redis使用的单线程模型(高版本Redis引入了多线程),减少了线程竞争的不必要的开销。

这里要补充一点,多线程提高效率的前提是执行的是CPU密集型的操作,多线程才能充分利用CPU的多核心资源。而Redis的核心功能只是操作内存中的数据,对CPU的负载并不高。

相比MySQL在增删改数据的时候需要关注数据库中的各种约束,这些检查就增加了负担,耗时也会被Redis的简单修改更长。

另外,IO多路复用是一个很重要的概念,它是操作系统提供的接口(比如Linux提供的C语言接口,而Redis正好就是C语言编写的)让一个线程有办法同时处理多个网络请求。如果没有多路复用,一个线程就只能给一个请求服务,Redis的单线程设计就不好用了。具体可以点击链接查看本站的博客。【Linux】高级IO和多路转接 | select/poll/epoll | 慕雪的寒舍

5.Redis的单线程模型

上文提到,Redis是采用单线程模型进行设计的,虽然高版本Redis引入了多线程,但只限于网络IO部分(多线程来处理网络请求)实际的存储操作还是使用单线程来处理的。

复习一下多线程并发可能导致的问题吧:假设两个Redis客户端链接了服务器,都希望对同一个key进行set操作,客户端A希望将key加一,客户端B也希望将key加一,这两个请求几乎同时发送给Redis的服务器,假设Redis服务器采用多线程来处理,那么就有可能出现

  • A的请求读取了key的当前值,准备进行加一操作;
  • B的请求读取了key的当前值,也准备进行加一操作;
  • 由于两个读取几乎是同时发生的,A和B的处理线程读取到的key的值一致;
  • 最终导致key只被加一了(实际需要加二);

而实际上,Redis采用单线程模型,A和B的请求肯定会有个先后顺序,最终处理是串行(依次执行)的,要么是先处理A的加一,再处理B的加一,最终肯定能得到加二的正确结果。

Redis在网络IO上引入多线程,但在核心处理保持单线程,可以在提高网络IO处理效率的基础上,保证对数据的操作一定是串行的。

这就好比一个食堂只有一个打饭窗口,中午下课了一堆人去食堂(网络IO多线程),但进了食堂之后还是得排队打饭(串行)。

因为Redis的核心操作对CPU的负载并不高,所以它没有必要使用多线程来提高数据处理方面的效率(因为使用多线程就务必需要加锁/解锁以及资源竞争,本来开门取东西只需要一下子就搞定了,现在还多了解锁/关门上锁的步骤,效率可能不增反降)。

The end