同为分布式缓存,为何 Redis 更胜一筹?

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 如今,市面上的缓存解决方案已经逐步成熟了,今天我将选取其中一些代表性的方案包括Redis、Memcached和Tair进行对比,帮助大家 在生产实践中更好地进行技术选型。

如今,市面上的缓存解决方案已经逐步成熟了,今天我将选取其中一些代表性的方案包括Redis、Memcached和Tair进行对比,帮助大家 在生产实践中更好地进行技术选型。

一、常用的分布式缓存的对比

常用的分布式缓存包括Redis、Memcached和阿里巴巴的Tair(见下表),因为Redis提供的数据结构比较丰富且简单易用,所以Redis的使用广泛。

下面我们从9个大方面来对比最常用的Redis和Memcached。

1.数据类型

Redis一共支持5种数据类型,每种数据类型对应不同的数据结构,有简单的String类型、压缩串、字典、跳跃表等。 跳跃表是比较新型的数据结构,常用于高性能的查找,可以达到log2N的查询速度,而且跳跃表相对于红黑树,在更新时变更的节点较少,更易于实现并发操作。

Memcache只支持对键值对的存储,并不支持其它数据结构。

2.线程模型

Redis使用单线程实现,Memcache等使用多线程实现,因此我们不推荐在Redis中存储太大的内容,否则会阻塞其它请求。

因为缓存操作都是内存操作,只有很少的计算操作,所以在单线程下性能很好。Redis实现的单线程的非阻塞网络I/O模型,适合快速地操作逻辑,有复杂的长逻辑时会影响性能。 对于长逻辑应该配置多个实例来提高多核CPU的利用率,也就是说,可以使用单机器多端口来配置多个实例,官方的推荐是一台机器使用8个实例。

它实现的非阻塞I/O模型基于Libevent库中关于Epoll的两个文件加上自己简单实现的事件通知模型,简单小巧,作者的思想就是保持实现简单、减少依赖。由于在服务器中只有一个线程,因此提供了管道来合并请求和批量执行,缩短了通信消耗的时间。

Memcache也使用了非阻塞I/O模型,但是使用了多线程,可以应用于多种场景,请求的逻辑可大可小、可长可短,不会出现一个逻辑复杂的请求阻塞对其它请求的响应的场景。它直接依赖Libevent库实现,依赖比较复杂,损失了在一些特定环境下的高性能。

3.持久机制

Redis提供了两种持久机制,包括RDB和AOF,前者是定时的持久机制,但在出现宕机时可能会出现数据丢失,后者是基于操作日志的持久机制。

Memcahe并不提供持久机制,因为Memache的设计理念就是设计一个单纯的缓存,缓存的数据都是临时的,不应该是持久的,也不应该是一个大数据的数据库,缓存未命中时回源查询数据库是天经地义的,但可以通过第三方库MemcacheDB来支持它的持久性。

4.客户端

常见的Redis Java客户端Jedis使用阻塞I/O,但可以配置连接池,并提供了一致性哈希分片的逻辑,也可以使用开源的客户端分片框架Redic。

Memecache的客户端包括Memcache Java Client、Spy Client、XMemcache等,Memcache Java Client使用阻塞I/O,而Spy Client/XMemcache使用非阻塞I/O。

我们知道,阻塞I/O不需要额外的线程,非阻塞I/O会开启额外的请求线程(在Boss线程池里)监听端口,一个请求在处理后就释放工作者线程(在Worker线程池中),请求线程在监听到有返回结果时,一旦有I/O返回结果就被唤醒,然后开始处理响应数据并写回网络Socket连接,所以从理论上来讲,非阻塞I/O的吞吐量和响应能力会更高。

5.高可用

Redis支持主从节点复制配置,从节点可使用RDB和缓存的AOF命令进行同步和恢复。Redis还支持Sentinel和Cluster(从3.0版本开始)等高可用集群方案。

Memecache不支持高可用模型,可使用第三方Megagent代理,当一个实例宕机时,可以连接另外一个实例来实现。

6.对队列的支持

Redis本身支持lpush/brpop、publish/subscribe/psubscribe等队列和订阅模式。

Memcache不支持队列,可通过第三方MemcachQ来实现。

7.事务

Redis提供了一些在一定程度上支持线程安全和事务的命令,例如:multi/exec、watch、inc等。由于Redis服务器是单线程的,任何单一请求的服务器操作命令都是原子的,但跨客户端的操作并不保证原子性,所以对于同一个连接的多个操作序列也不保证事务。

Memcached的单个命令也是线程安全的,单个连接的多个命令序列不是线程安全的,它也提供了inc等线程安全的自加命令,并提供了gets/cas保证线程安全。

8.数据淘汰策略

Redis提供了丰富的淘汰策略,包括maxmemory、maxmemory-policy、volatile-lru、allkeys-lru、volatile-random、allkeys-random、volatile-ttl、noeviction(return error)等。

Memecache在容量达到指定值后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。在某些情况下LRU机制反倒会带来麻烦,会将不期待的数据从内存中清除,在这种情况下启动Memcache时,可以通过“M”参数禁止LRU算法。

9.内存分配

Redis为了屏蔽不同平台之间的差异及统计内存占用量等,对内存分配函数进行了一层封装,在程序中统一使用zmalloc、zfree系列函数,这些函数位于zmalloc.h/zmalloc.c文件中。封装就是为了屏蔽底层平台的差异,同时方便自己实现相关的统计函数。具体的实现方式如下:

若系统中存在Google的TC_MALLOC库,则使用tc_malloc一族的函数代替原本的malloc一族的函数。

若当前系统是Mac系统,则使用系统的内存分配函数。

对于其它情况,在每一段分配好的空间前面同时多分配一个定长的字段,用来记录分配的空间大小,通过这种方式来实现简单有效的内存分配。

Memcache采用slab table的方式分配内存,首先把可得的内存按照不同的大小来分类,在使用时根据需求找到最接近于需求大小的块分配,来减少内存碎片,但是这需要进行合理配置才能达到效果。

从上面的对比可以看到,Redis在实现和使用上更简单,但是功能更强大,效率更高,应用也更广泛。下面将对Redis进行初步介绍,给初学者一个初体验式的学习引导。

二、Redis初体验

Redis是一个能够存储多种数据对象的开源Key-Value存储系统,使用ANSI C语言编写,可以仅仅当作内存数据库使用,也可以作为以日志为存储方式的数据库系统,并提供多种语言的API。

1.使用场景

我们通常把Redis当作一个非本地缓存来使用,很少用到它的一些高级功能。在使用中最容易出问题的是用Redis来保存JSON数据,因为Redis不像Elasticsearch或者PostgreSQL那样可以很好地支持JSON数据。 所以我们经常把JSON当作一个大的String直接放到Redis中,但现在的JSON数据都是连环嵌套的,每次更新时都要先获取整个JSON,然后更改其中一个字段再放上去。

一个常见的JSON数据的Java对象定义如下:

public class Commodity {

private long price;

private String title;

……

}

在海量请求的前提下,在Redis中每次更新一个字段,比如销量字段,都会产生较大的流量。在实际情况下,JSON字符串往往非常复杂,体积达到数百KB都是有可能的,导致在频繁更新数据时使网络I/O跑满,甚至导致系统超时、崩溃。

因此,Redis官方推荐采用哈希来保存对象,比如有3个商品对象,ID分别是123、124和12345,我们通过哈希把它们保存在Redis中,在更新其中的字段时可以这样做:

HSET commodity:123 price 100

HSET commodity:124 price 101

HSET commodity:12345 price 101

HSET commodity:123 title banana

HSET commodity:124 title apple

HSET commodity:12345 title orange

也就是说,用商品的类型名和ID组成一个Redis哈希对象的KEY。在获取某一属性时只需这样做就可以获取单独的属性: HGET commodity: 12345。

2.Redis的高可用方案:哨兵

Redis官方推出了一个集群管理工具,叫作哨兵(Sentinel),负责在节点中选出主节点,按照分布式集群的管理办法来操作集群节点的上线、下线、监控、提醒、自动故障切换(主备切换),且实现了著名的RAFT选主协议,从而保证了系统选主的一致性。

这里给出一个哨兵的通用部署方案。哨兵节点一般至少要部署3份,可以和被监控的节点放在一个虚拟机中,常见的哨兵部署如图所示。

在这个系统中,初始状态下的机器A是主节点,机器B和机器C是从节点。

由于有3个哨兵节点,每个机器运行1个哨兵节点,所以这里设置quorum = 2,也就是在主节点无响应后,有至少两个哨兵无法与主节点通信,则认为主节点宕机,然后在从节点中选举新的主节点来使用。

在发生网络分区时,若机器A所在的主机网络不可用,则机器B和机器C上的两个Sentinel实例会启动failover并把机器B选举为主节点。

Sentinel集群的特性保证了机器B和机器C上的两个Sentinel实例得到了关于主节点的最新配置。但机器A上的Sentinel节点依然持有旧的配置,因为它与外界隔离了。

在 网络恢复后,我们知道 机器 A 上的 Sentinel 实例 将会更新它的配置。但是,如果客户端所连接的 主机节点也 被网络隔离, 则 客户端将依然可以向 机器 A 的 Redis 节点 写数据,但 在 网络恢复后, 机器 A 的 Redis 节点 就会变成一个 从节点 ,那么在网络隔离期间,客户端向 机器 A的 Redis 节点写入 的数据将会丢失 ,这是不可避免的。

如果把 Redis 当作 缓存来使用,那么 我们 也许能容忍这部分数据的丢失 ,但若 把 Redis 当作一个存储系统来使用,就无法容忍这部分数据的丢失了 , 因为 Redis 采用的是异步复制,在这样的场景下 无法 避免数据的丢失。

在这里,我们可以通过以下配置来配置每个Redis实例,使得数据不会丢失:

min-slaves-to-write 1

min-slaves-max-lag 10

通过上面的配置,当一个Redis是主节点时,如果它不能向至少一个从节点写数据(上面的min-slaves-to-write指定了slave的数量),则它将会拒绝接收客户端的写请求。由于复制是异步的,所以主节点无法向从节点写数据就意味着从节点要么断开了连接,要么没在指定的时间内向主节点发送同步数据的请求。

所以,采用这样的配置可排除网络分区后主节点被孤立但仍然写入数据,从而导致数据丢失的场景。

3.Redis集群

Redis在3.0中也引入了集群的概念,用于解决一些大数据量和高可用的问题,但是,为了达到高性能的目的,集群不是强一致性的,使用的是异步复制,在数据到主节点后,主节点返回成功,数据被异步地复制给从节点。

首先,我们来学习Redis的集群分片机制。Redis使用CRC16(key) mod 16384进行分片,一共分16384个哈希槽,比如若集群有3个节点,则我们按照如下规则分配哈希槽:

A节点包含0-5500的哈希槽;

B节点包含5500-11000的哈希槽;

C节点包含11000-16384的哈希槽。

这里设置了3个主节点和3个从节点,集群分片如图所示。

图中共有3个Redis主从服务器的复制节点,其中任意两个节点之间都是相互连通的,客户端可以与其中任意一个节点相连接,然后访问集群中的任意一个节点,对其进行存取和其他操作。

那Redis是怎么做到的呢?首先,在Redis的每个节点上都会存储哈希槽信息,我们可以将它理解为是一个可以存储两个数值的变量,这个变量的取值范围是0-16383。根据这些信息,我们就可以找到每个节点负责的哈希槽,进而找到数据所在的节点。

Redis集群实际上是一个集群管理的插件,当我们提供一个存取的关键字时,就会根据CRC16的算法得出一个结果,然后把结果除以16384求余数,这样每个关键字都会对应一个编号为0-16383的哈希槽,通过这个值找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。但是这些都是由集群的内部机制实现的,我们不需要手工实现。

欢迎工作一到五年的Java工程师朋友们加入Java架构开发:744677563

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
116 85
|
1月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
1月前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
19天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
52 5
|
23天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
42 8
|
1月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
57 16
|
1月前
|
缓存 NoSQL Redis
Redis 缓存使用的实践
《Redis缓存最佳实践指南》涵盖缓存更新策略、缓存击穿防护、大key处理和性能优化。包括Cache Aside Pattern、Write Through、分布式锁、大key拆分和批量操作等技术,帮助你在项目中高效使用Redis缓存。
252 22
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
40 5
|
1月前
|
缓存 NoSQL 中间件
redis高并发缓存中间件总结!
本文档详细介绍了高并发缓存中间件Redis的原理、高级操作及其在电商架构中的应用。通过阿里云的角度,分析了Redis与架构的关系,并展示了无Redis和使用Redis缓存的架构图。文档还涵盖了Redis的基本特性、应用场景、安装部署步骤、配置文件详解、启动和关闭方法、systemctl管理脚本的生成以及日志警告处理等内容。适合初学者和有一定经验的技术人员参考学习。
197 7