现在,市面上的缓存解决方案已经逐步成熟了,今天我将选取其中一些表明性的方案包括Redis、Memcached和Tair进行对比,帮助你们 在生产实践中更好地进行技术选型。算法
1、经常使用的分布式缓存的对比spring
经常使用的分布式缓存包括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文件中。封装就是为了屏蔽底层平台的差别,同时方便本身实现相关的统计函数。具体的实现方式以下:
Memcache采用slab table的方式分配内存,首先把可得的内存按照不一样的大小来分类,在使用时根据需求找到最接近于需求大小的块分配,来减小内存碎片,可是这须要进行合理配置才能达到效果。
从上面的对比能够看到,Redis在实现和使用上更简单,可是功能更强大,效率更高,应用也更普遍。下面将对Redis进行初步介绍,给初学者一个初体验式的学习引导。
2、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分别是12三、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个节点,则咱们按照以下规则分配哈希槽:
这里设置了3个主节点和3个从节点,集群分片如图所示。
图中共有3个Redis主从服务器的复制节点,其中任意两个节点之间都是相互连通的,客户端能够与其中任意一个节点相链接,而后访问集群中的任意一个节点,对其进行存取和其余操做。
那Redis是怎么作到的呢?首先,在Redis的每一个节点上都会存储哈希槽信息,咱们能够将它理解为是一个能够存储两个数值的变量,这个变量的取值范围是0-16383。根据这些信息,咱们就能够找到每一个节点负责的哈希槽,进而找到数据所在的节点。
Redis集群其实是一个集群管理的插件,当咱们提供一个存取的关键字时,就会根据CRC16的算法得出一个结果,而后把结果除以16384求余数,这样每一个关键字都会对应一个编号为0-16383的哈希槽,经过这个值找到对应的插槽所对应的节点,而后直接自动跳转到这个对应的节点上进行存取操做。可是这些都是由集群的内部机制实现的,咱们不须要手工实现。
注:关注做者微信公众号,了解更多分布式架构、微服务、netty、MySQL、spring、、性能优化、等知识点。
公众号:《 Java大蜗牛 》