当前位置: 首页 > 科技观察

Redis除了缓存,还解决了哪些问题?

时间:2023-03-19 17:35:41 科技观察

所属分类:1从零开始2基于内存的原生缓存3服务端Redis3.1持久化(Persistence)3.2哨兵(Sentinel)和复制(Replication)3.3集群(Cluster)4客户端Redis4.1数据类型4.2Transactions4.3Lua脚本4.4Pipeline4.5分布式锁总结首先看看Redis是什么。官方介绍说明:Redis是一个基于BSD的开源项目。它是一种在内存中存储结构化数据的存储系统。您可以将它用作数据库、缓存和消息中间件。同时支持字符串、列表、哈希、集合、排序集合、位图、hyperloglogs和地理空间索引等数据类型。它还内置了复制、lua脚本、LRU、事务等功能,通过redissentinel实现高可用,通过rediscluster实现自动分片。以及事务、发布/订阅、自动故障转移等。综上所述,Redis提供了丰富的功能,第一次使用可能会一头雾水。这些函数有什么用?他们解决了什么问题?什么情况下会用到相应的功能呢?那么下面从零开始,一步步演化到粗略的讲解。1、从头开始的需求很简单。我们有一个提供热点新闻列表的api:http://api.xxx.com/hot-news。API的消费者抱怨每个请求大约需要2秒才能返回结果。然后我们着手如何提升API消费者感知的性能,很快最简单粗暴的第一个方案就出来了:API响应添加基于HTTP的缓存控制cache-control:max-age=600,让消费者缓存这个反应十分钟。api消费者如果有效利用响应中的缓存控制信息,可以有效提升其感知性能(10分钟以内)。但是仍然存在两个缺点:第一是缓存生效后10分钟内,API消费者可能会拿到旧数据;二是如果API的客户端忽略缓存,直接访问API,仍然需要2秒。治标不治本。2为了解决调用API仍然需要2秒的问题,基于本地内存的缓存,经过排查,主要原因是使用SQL获取热点新闻的过程消耗了将近2秒,所以我们想到了一个简单粗暴的方案,就是将SQL查询的结果直接缓存在当前api服务器的内存中(设置缓存的有效时间为1分钟)。后续1分钟内的请求直接从缓存中读取,执行SQL不再需要2秒。如果API每秒接收100个请求,那么每分钟就有6000个请求,也就是只有前2秒拥挤的请求需要2秒,后面58秒的所有请求,即使它们都可以完成得到回应,无需再等待2秒。其他API的朋友发现这是个好办法,于是我们很快发现API服务器的内存要满了。..3当server端的Redis在APIserver中缓存满了,我们发现不得不另想办法了。最直接的想法是,我们把这些缓存都扔到一个专用的服务器上,并且对它的内存进行很大的配置。然后我们专注于redis。..至于如何配置和部署redis,这里就不做说明了。redis官方有详细的介绍。然后我们使用单独的服务器作为Redis服务器,解决了API服务器的内存压力。3.1持久化(Persistence)单个Redis服务器一个月总有几天心情不好,心情不好就罢工,导致所有缓存丢失(redis数据存放在内存中)。Redis服务器虽然可以恢复上线,但是由于内存数据丢失导致缓存雪崩,API服务器和数据库的压力还是一下子上来了。那么这个时候Redis的持久化功能就派上用场了,可以缓解缓存雪崩的影响。redis的持久化是指redis会将内存中的数据写入硬盘,并在redis重启时加载数据,将缓存丢失的影响降到最低。3.2哨兵(Sentinel)和复制(Replication)Redis服务器毫无预警的罢工是一件很麻烦的事情。那我们该怎么办呢?答案是:备份一个,你挂了。那么如何知道某台redis服务器宕机,如何切换,如何保证备机是原服务器的完整备份呢?这时候就需要Sentinel和Replication发挥作用了。Sentinel可以管理多个Redis服务器,提供监控、提醒和自动故障转移功能;复制负责让一个Redis服务器配备多个备份服务器。Redis也是利用这两个功能来保证Redis的高可用。另外,Sentinel功能是利用Redis的发布订阅功能。3.3集群(Cluster)单台服务器的资源总是有上限的。我们可以对CPU资源和IO资源使用主从复制来实现读写分离,将部分CPU和IO压力转移到从服务器上。但是内存资源呢,主从模式只是备份相同的数据,不能横向扩展内存;单机内存只能扩大,但总有一个上限。所以我们需要一个允许我们横向扩展的解决方案。最终的目标是让每个服务器只负责其中的一部分,让所有这些服务器形成一个整体。对于外部消费者来说,这组分布式服务器就像一个集中式服务器(之前在博客解读REST:基于Web的应用程序架构中解释了分布式和基于Web的区别)。在Redis官方分布式方案出来之前,有两个方案,twemproxy和codis。这两种方案一般都是依赖proxy进行分发,也就是说redis本身并不关心分布式的东西,而是交给twemproxy和codis。redis官方给出的集群方案是在各个redis服务器中实现分布式部分,使其独立完成分布式需求,不需要其他组件。我们这里不关心这些方案的优缺点。大家注意这里的distribution是要处理什么?即twemproxy和codis独立处理分布式处理的部分逻辑和集群集成到redis服务中的部分逻辑。它在解决什么问题?正如我们之前所说,分布式服务在外界看来就像是集中式服务。所以要做到这一点,有一个问题需要解决:增加或减少分布式服务中的服务器数量应该对消费服务的客户端不敏感;那么就意味着客户端不能穿透分布式服务,把自己绑在某个服务器上,因为一旦这样做,就不能再增加新的服务器,也不能进行故障替换。解决这个问题有两种方式:第一种方式是最直接的,就是我加一个中间层来隔离这个具体的依赖,也就是twemproxy采用的方式,让所有客户端只能通过它来消费redsi服务,用它来隔离这种依赖(但是你会发现twermproxy会变成单点),在这种情况下,每个redis服务器都是独立的,它们不知道彼此的存在;第二种方式是让redis服务器知道对方的存在,利用重定向机制引导客户端完成自己需要的操作。比如客户端链接到某个redis服务器,说我要执行这个操作。redis服务器发现无法完成这个操作。然后把可以完成这个操作的服务器的信息给客户端,让客户端去请求另外一个服务器。这时候你会发现每个redis服务器都需要维护一套完整的分布式服务器信息,否则它怎么知道让客户端找哪个其他服务器来执行客户端想要的操作。上面一段解释了这么多,不知道大家有没有发现,不管是第一种方式还是第二种方式,都有一个共同点,就是分布式服务中的所有服务器,以及它们能够提供信息的服务。这些信息无论如何都必须存在,不同的是第一种方式是将这部分信息单独管理,通过这些信息在后端协调多个独立的redis服务器;第二种方式是让每个redis服务器都持有这个信息,知道对方的存在,达到和第一种方式一样的目的。好处是不需要额外的组件来处理这部分事情。RedisCluster的具体实现细节采用了Hashslots的概念,即预先分配了16384个slots:在client端对Key进行CRC16(key)%16384计算得到对应的slots;在redisserver端,每个server负责一部分slots。当添加或删除新服务器时,这些插槽及其对应的数据将被迁移。同时,每台服务器都保存着插槽及其对应服务器的完整信息。这使服务器能够重定向客户端的请求。4ClientRedis上面第三节主要介绍了Redis服务端的演进步骤,解释了Redis是如何从一个单机服务演进到一个高可用、去中心化、分布式的存储系统。本节重点介绍客户端可以消费的Redis服务。4.1数据类型Redis支持多种数据类型,从最基本的字符串到复杂的常用数据结构:字符串:最基本的数据类型,二进制安全字符串,***512M。列表:按添加顺序维护顺序的字符串列表。set:没有重复元素的无序字符串集合。有序集:有序的字符串集合。hash:键值对的集合。位图:更详细的操作,以位为单位。hyperloglog:一种基于概率的数据结构。这些众多的数据类型主要是为了支持各种场景的需求,当然每种类型都有不同的时间复杂度。其实这些复杂的数据结构相当于我之前在《解读REST》系列博客中介绍的基于网络应用架构风格的远程数据访问(RemoteDataAccess=RDA)的具体实现,即通过执行服务器上有一套标准的操作命令,可以在服务器之间得到想要的缩减结果集,从而简化客户端的使用,提高网络性能。比如没有list这样的数据结构,只能把list保存为string,client拿到完整的list,运行后再完整的提交给redis,这样会造成很大的浪费。4.2事务在以上数据类型中,每种数据类型都有独立的命令进行操作。很多时候,我们需要一次执行多个命令,并且需要它同时成功或失败。Redis对事务的支持也是源于这部分需求,即能够同时支持多个命令的顺序,并保证它们的原子性。4.3Lua脚本是基于事务的。如果我们需要在服务端一次性进行比较复杂的操作(包括一些逻辑判断),那么lua就可以派上用场(比如获取缓存时,延长其过期时间)。Redis保证了lua脚本的原子性。在某些场景下,它可以替代redis提供的事务相关命令。相当于基于网络应用的架构风格中引入的远程评估(RemoteEvluation=REV)的具体实现。4.4管道因为redis客户端和服务器之间的连接是基于TCP的,默认情况下每个连接只能执行一个命令。管道允许一个连接处理多个命令,这样可以节省一些tcp连接开销。管道和事务的区别在于管道是用来节省通信开销的,但是不保证原子性。4.5分布式锁官方推荐使用Redlock算法,即使用string类型,加锁时给出一个具体的key,然后设置一个随机值;解锁时,先用lua脚本进行获取比较,再deletekey。具体命令如下:SETresource_namemy_random_valueNXPX30000ifredis.call("get",KEYS[1])==ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end总结本文着重从抽象的角度讲解redislevel函数及其目的,而不关心它们的具体细节。这样,我们就可以专注于它所解决的问题。基于抽象层次的概念,我们可以在特定场景下选择更合适的解决方案,而不是局限于其技术细节。以上是笔者个人的一些理解,如有不妥之处,敬请指正。