分布式缓存应用场景与redis持久化机制
生产级Redis 高并发分布式锁实战1:高并发分布式锁如何实现 https://www.cnblogs.com/yizhiamumu/p/16556153.html
生产级Redis 高并发分布式锁实战2:缓存架构设计问题优化 https://www.cnblogs.com/yizhiamumu/p/16556667.html
总结篇3:redis 典型缓存架构设计问题及性能优化 https://www.cnblogs.com/yizhiamumu/p/16557996.html
总结篇4:redis 核心数据存储结构及核心业务模型实现应用场景 https://www.cnblogs.com/yizhiamumu/p/16566540.html
DB\redis\zookeeper分布式锁设计 https://www.cnblogs.com/yizhiamumu/p/16663243.html
在缓存和数据库双写场景下,一致性是如何保证的 https://www.cnblogs.com/yizhiamumu/p/16686751.html
如何保证 Redis 的高并发和高可用?讨论redis的单点,高可用,集群 https://www.cnblogs.com/yizhiamumu/p/16586968.html
分布式缓存应用场景与redis持久化机制 https://www.cnblogs.com/yizhiamumu/p/16702154.html
Redisson 源码分析及实际应用场景介绍 https://www.cnblogs.com/yizhiamumu/p/16706048.html
Redis 高可用方案原理初探 https://www.cnblogs.com/yizhiamumu/p/16709290.html
RedisCluster集群架构原理与通信原理 https://www.cnblogs.com/yizhiamumu/p/16704556.html
分布式缓存应用场景与redis持久化机制
随着用户数和访问量越来越大,我们的系统应用需要支撑更多的并发量。
但是往往我们的应用服务器资源是有限的,数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的),如何能够有效利用有限的资源来提供尽可能大的吞吐量?
一个有效的办法就是引入缓存,打破标准流程,每个环节中请求可以从缓存中直接获取目标数据并返回,从而减少计算量,有效提升响应速度,让有限的资源服务更多的用户。
缓存介质
从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,可以分成内存、硬盘文件、数据库。
- 内存:将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原。
- 硬盘:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
- 数据库:增加缓存的策略的目的之一就是为了减少数据库的I/O压力。现在使用数据库做缓存介质是不是又回到了老问题上了?其实,数据库也有很多种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。
缓存分类和应用场景
缓存有各类特征,而且有不同介质的区别,那么实际工程中我们怎么去对缓存分类呢?在目前的应用服务框架中,比较常见的,时根据缓存雨应用的藕合度,分为local cache(本地缓存)和remote cache(分布式缓存):
-
本地缓存:编程直接实现缓存,指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
-
分布式缓存:常用的是redis 和memcached 。指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
目前各种类型的缓存都活跃在成千上万的应用服务中,还没有一种缓存方案可以解决一切的业务场景或数据类型,我们需要根据自身的特殊场景和背景,选择最适合的缓存方案。
好的程序员能根据数据类型、业务场景来准确判断使用何种类型的缓存,如何使用这种缓存,以最小的成本最快的效率达到最优的目的。
redis 和memcached 有什么区别?
- mc 可以缓存图片和视频,redis 支持除更多的数据结构。redis 典型的应用场景是用户订单列表,用户消息,帖子评论等。
- redis 可以使用虚拟内存,redis 可持久化和aof 灾难恢复,支持主从数据备份。如果redis 挂了,内存能够快速恢复热数据,不会将压力瞬间压在数据库上,没有cache 预热的过程。对于只读和数据一致性要求不高的场景可以采用持久化存储。
- redis 可以做消息队列。redis 支持集群,可以实现主动复制,读写分离,mc 如果想实现高可用,需要进行二次开发。
- mc 存储的vlaue 最大为1M。
选择mc 的场景:
- 1 纯kv, 数据量非常大的业务。
原因是: - 1 mc 的内存分配采用的是预分配内存池的管理方式,能够省去内存分配的时间。redis 是临时申请空间,可以导致碎片化
- 2 虚拟内存使用,mc 将所有的数据存储在物理内存里,redis 有自己的vm 机制,理论上能够存储比物理内在更多的数据,当数据超量时,引发swap, 把冷数据刷新到磁盘上。从这点上看,数据量大时,mc 更快
- 3 网络模型。mc 使用非阻塞的io 复用模型,redis 也是使用非阻塞的io 复用模型,但是redis 还提供了一些非kv 存储之外的排序,聚合功能,复杂的cpu 计算,会阻塞整个io 调度,从这点上由于redis 提供的功能较多,mc 更快一些。
- 4 线程模型,mc 使用多线程,主线程监听,worker 子线程接受请求,执行读写,这个过程可能存在锁冲突。redis 使用单线程,虽然无锁冲突,但是难以利用多核的特性提升吞吐量。
选择redis 场景:
- 1 存储方式上:mc 会把数据全部存储在内存中,断电后会挂掉,数据不能超过内存的大小。redis 有部分数据存在硬盘上,这样能保证数据持久性。
- 2 数据支持类型上:redis 支持更丰富的数据类型
- 3 使用底层模型不同:底层实现方式以及客户端之间通信的应用协议不同。redis 直接构建了vm 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- 4 value 大小。redis 可以达到1 G,而mc 只有1 M。
mc 多线程模型引入了缓存一致性和锁,加锁带来了性能损耗。
为什么 redis 单线程还如此快?
因为底层有高效数据存储结构。整个redis 存储结构是全局哈希表,哈希运算非常快,时间复杂度为常量。
memcached缓存
在服务端,memcached集群环境实际就是一个个memcached服务器的堆积;cache的分布式主要是在客户端实现,通过客户端的路由处理来达到分布式解决方案的目的。
客户端做路由的原理是,应用服务器在每次存取某key的value时,通过某种算法把key映射到某台memcached服务器nodeA上,因此这个key所有操作都在nodeA上。
结构图如图下所示。
memcached客户端采用一致性hash算法作为路由策略,如上图,相对于一般hash(如简单取模)的算法,一致性hash算法除了计算key的hash值外,还会计算每个server对应的hash值,然后将这些hash值映射到一个有限的值域上(比如0~2^32)。
通过寻找hash值大于hash(key)的最小server作为存储该key数据的目标server。如果找不到,则直接把具有最小hash值的server作为目标server。同时,一定程度上,解决了扩容问题,增加或删除单个节点,对于整个集群来说,不会有大的影响。最近版本,增加了虚拟节点的设计,进一步提升了可用性。
memcached是一个高效的分布式内存cache,memcached的内存管理机制,仅支持基础的key-value键值对类型数据存储。所以在memcached内存结构中有两个非常重要的概念:slab和chunk。如下图所示。
slab是一个内存块,它是memcached一次申请内存的最小单位。在启动memcached的时候一般会使用参数-m指定其可用内存,但是并不是在启动的那一刻所有的内存就全部分配出去了,只有在需要的时候才会去申请,而且每次申请一定是一个slab。Slab的大小固定为1M(1048576 Byte),一个slab由若干个大小相等的chunk组成。每个chunk中都保存了一个item结构体、一对key和value。
虽然在同一个slab中chunk的大小相等的,但是在不同的slab中chunk的大小并不一定相等,在memcached中按照chunk的大小不同,可以把slab分为很多种类(class),默认情况下memcached把slab分为40类(class1~class40),在class 1中,chunk的大小为80字节,由于一个slab的大小是固定的1048576字节(1M),因此在class1中最多可以有13107个chunk(也就是这个slab能存最多13107个小于80字节的key-value数据)。
memcached内存管理采取预分配、分组管理的方式,分组管理就是我们上面提到的slab class,按照chunk的大小slab被分为很多种类。
内存预分配过程是怎样的呢?
向memcached添加一个item时候,memcached首先会根据item的大小,来选择最合适的slab class:例如item的大小为190字节,默认情况下class 4的chunk大小为160字节显然不合适,class 5的chunk大小为200字节,大于190字节,因此该item将放在class 5中(显然这里会有10字节的浪费是不可避免的),计算好所要放入的chunk之后,memcached会去检查该类大小的chunk还有没有空闲的,如果没有,将会申请1M(1个slab)的空间并划分为该种类chunk。例如我们第一次向memcached中放入一个190字节的item时,memcached会产生一个slab class 2(也叫一个page),并会用去一个chunk,剩余5241个chunk供下次有适合大小item时使用,当我们用完这所有的5242个chunk之后,下次再有一个在160~200字节之间的item添加进来时,memcached会再次产生一个class 5的slab(这样就存在了2个pages)。
总结来看,memcached内存管理需要注意:
- chunk是在page里面划分的,而page固定为1m,所以chunk最大不能超过1m。
- chunk实际占用内存要加48B,因为chunk数据结构本身需要占用48B。
- 如果用户数据大于1m,则memcached会将其切割,放到多个chunk内。
- 已分配出去的page不能回收。
对于key-value信息,最好不要超过1m的大小;同时信息长度最好相对是比较均衡稳定的,这样能够保障最大限度的使用内存;同时,memcached采用的LRU清理策略,合理甚至过期时间,提高命中率。
无特殊场景下,key-value能满足需求的前提下,使用memcached分布式集群是较好的选择。理由是,
- 搭建与操作使用都比较简单;
- 分布式集群在单点故障时,只影响小部分数据异常,目前还可以通过Magent缓存代理模式,做单点备份,提升高可用;
- 整个缓存都是基于内存的,因此响应时间是很快,不需要额外的序列化、反序列化的程序,但同时由于基于内存,数据没有持久化,集群故障重启数据无法恢复。
- 高版本的memcached已经支持CAS模式的原子操作,可以低成本的解决并发控制问题。
Redis缓存
Redis是一个远程内存数据库(非关系型数据库),它可以存储键值对与5种不同类型的值之间的映射,可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,还可以使用客户端分片来扩展写性能。
Redis内部使用一个redisObject对象来标识所有的key和value数据,redisObject最主要的信息如图所示:type代表一个value对象具体是何种数据类型,encoding是不同数据类型在Redis内部的存储方式,比如——type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或是int,如果是int则代表世界Redis内部是按数值类型存储和表示这个字符串。
图左边的raw列为对象的编码方式:字符串可以被编码为raw(一般字符串)或Rint(为了节约内存,Redis会将字符串表示的64位有符号整数编码为整数来进行储存);列表可以被编码为ziplist或linkedlist,ziplist是为节约大小较小的列表空间而作的特殊表示;集合可以被编码为intset或者hashtable,intset是只储存数字的小集合的特殊表示;hash表可以编码为zipmap或者hashtable,zipmap是小hash表的特殊表示;有序集合可以被编码为ziplist或者skiplist格式,ziplist用于表示小的有序集合,而skiplist则用于表示任何大小的有序集合。
从网络I/O模型上看,Redis使用单线程的I/O复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll、kqueue和select。对于单纯只有I/O操作来说,单线程可以将速度优势发挥到最大,但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU计算过程中,整个I/O调度都是被阻塞住的,在这些特殊场景的使用中,需要额外的考虑。相较于memcached的预分配内存管理,Redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片。Redis跟据存储命令参数,会把带过期时间的数据单独存放在一起,并把它们称为临时数据,非临时数据是永远不会被剔除的,即便物理内存不够,导致swap也不会剔除任何非临时数据(但会尝试剔除部分临时数据)。
我们描述Redis为内存数据库,作为缓存服务,大量使用内存间的数据快速读写,支持高并发大吞吐;而作为数据库,则是指Redis对缓存的持久化支持。Redis由于支持了非常丰富的内存数据库结构类型,如何把这些复杂的内存组织方式持久化到磁盘上?Redis的持久化与传统数据库的方式差异较大,Redis一共支持四种持久化方式,主要使用的前两种:
- 定时快照方式(snapshot):该持久化方式实际是在Redis内部一个定时器事件,每隔固定时间去检查当前数据发生的改变次数与时间是否满足配置的持久化触发的条件,如果满足则通过操作系统fork调用来创建出一个子进程,这个子进程默认会与父进程共享相同的地址空间,这时就可以通过子进程来遍历整个内存来进行存储操作,而主进程则仍然可以提供服务,当有写入时由操作系统按照内存页(page)为单位来进行copy-on-write保证父子进程之间不会互相影响。它的缺点是快照只是代表一段时间内的内存映像,所以系统重启会丢失上次快照与重启之间所有的数据。
- 基于语句追加文件的方式(aof):aof方式实际类似MySQl的基于语句的binlog方式,即每条会使Redis内存数据发生改变的命令都会追加到一个log文件中,也就是说这个log文件就是Redis的持久化数据。
- 虚拟内存(VM),主要问题是代码复杂,重启慢,复制慢等等,目前已经被作者放弃。
- Diskstore 方式,也就是传统的 B-tree 的方式。
在设计思路上,前两种是基于全部数据都在内存中,即小数据量下提供磁盘落地功能,而后两种持久化方式仍然是在实验阶段,并且 vm 方式基本已经被作者放弃,所以实际能在生产环境用的只有前两种,换句话说 Redis 目前还只能作为小数据量存储(全部数据能够加载在内存中),海量数据存储方面并不是 Redis 所擅长的领域。
Reids持久化的目的主要还是容灾,有时候还需要定时把RDB或者AOF文件通过shell脚本传递到其他服务器上。
AOF三种策略:
always:每条Redis写命令都同步写入硬盘
everysec:每秒执行一次同步,将多个命令写入硬盘
no:由操作系统决定何时同步
AOF 重写:
随着Redis的运行,被执行的写命令不断同步到AOF文件中,AOF文件的体积越来越大,极端情况将会占满所有的硬盘空间。如果AOF文件体积过大,还原的过程也会相当耗时。为了解决AOF文件不断膨胀的问题,需要redis基于当前快照来重写AOF。
比如 reids在使用过程中通过LUR来淘汰一部分数据,使得redis的内存大小一直在100g,而AOF文件大小可能到达1000g,当达到某个临界值的时候,我们会移除1000的rof文件,基于现在的100g内存快照重构一个100g的aof文件。
aof的方式的主要缺点是追加log文件可能导致体积过大,当系统重启恢复数据时如果是aof的方式则加载数据会非常慢,几十G的数据可能需要几小时才能加载完,当然这个耗时并不是因为磁盘文件读取速度慢,而是由于读取的所有命令都要在内存中执行一遍。另外由于每条命令都要写log,所以使用aof的方式,Redis的读写性能也会有所下降。
Redis的持久化使用了Buffer I/O,所谓Buffer I/O是指Redis对持久化文件的写入和读取操作都会使用物理内存的Page Cache,而大多数数据库系统会使用Direct I/O来绕过这层Page Cache并自行维护一个数据的Cache。
而当Redis的持久化文件过大(尤其是快照文件),并对其进行读写时,磁盘文件中的数据都会被加载到物理内存中作为操作系统对该文件的一层Cache,而这层Cache的数据与Redis内存中管理的数据实际是重复存储的。虽然内核在物理内存紧张时会做Page Cache的剔除工作,但内核很可能认为某块Page Cache更重要,而让你的进程开始Swap,这时你的系统就会开始出现不稳定或者崩溃了,因此在持久化配置后,针对内存使用需要实时监控观察。
与memcached客户端支持分布式方案不同,Redis更倾向于在服务端构建分布式存储,如图
Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本,它没有中心节点,具有线性可伸缩的功能。如上图,其中节点与节点之间通过二进制协议进行通信,节点与客户端之间通过ascii协议进行通信。在数据的放置策略上,Redis Cluster将整个key的数值域分成4096个hash槽,每个节点上可以存储一个或多个hash槽,也就是说当前Redis Cluster支持的最大节点数就是4096。Redis Cluster使用的分布式算法也很简单:crc16( key ) % HASH_SLOTS_NUMBER。整体设计可总结为:
- 数据hash分布在不同的Redis节点实例上;
- M/S的切换采用Sentinel;
- 写:只会写master Instance,从sentinel获取当前的master Instance;
- 读:从Redis Node中基于权重选取一个Redis Instance读取,失败/超时则轮询其他Instance;Redis本身就很好的支持读写分离,在单进程的I/O场景下,可以有效的避免主库的阻塞风险;
- 通过RPC服务访问,RPC server端封装了Redis客户端,客户端基于Jedis开发。
可以看到,通过集群+主从结合的设计,Redis在扩展和稳定高可用性能方面都是比较成熟的。
但是,在数据一致性问题上,Redis没有提供CAS操作命令来保障高并发场景下的数据一致性问题,不过它却提供了事务的功能,Redis的Transactions提供的并不是严格的ACID的事务(比如一串用EXEC提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行)。但是这个Transactions还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间有会有其它客户端命令插进来执行)。
Redis还提供了一个Watch功能,你可以对一个key进行Watch,然后再执行Transactions,在这过程中,如果这个Watched的值进行了修改,那么这个Transactions会发现并拒绝执行。在失效策略上,Redis支持多大6种的数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰;
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰;
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 ;
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰;
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰;
- no-enviction(驱逐):禁止驱逐数据。
Redis 复制流程概述
Redis 的复制功能是完全建立在之前我们讨论过的基于内存快照的持久化策略基础上的,也就是说无论你的持久化策略选择的是什么,只要用到了 Redis 的复制功能,就一定会有内存快照发生,那么首先要注意你的系统内存容量规划,原因可以参考我上一篇文章中提到的 Redis 磁盘 IO 问题。
Redis 复制流程在 Slave 和 Master 端各自是一套状态机流转,涉及的状态信息是:
Slave 端:
REDIS_REPL_NONE
REDIS_REPL_CONNECT
REDIS_REPL_CONNECTED
Master 端:
REDIS_REPL_WAIT_BGSAVE_START
REDIS_REPL_WAIT_BGSAVE_END
REDIS_REPL_SEND_BULK
REDIS_REPL_ONLINE
- Slave 端在配置文件中添加了 slave of 指令,于是 Slave 启动时读取配置文件,初始状态为 REDIS_REPL_CONNECT。
- Slave 端在定时任务 serverCron(Redis 内部的定时器触发事件) 中连接 Master,发送 sync 命令,然后阻塞等待 master 发送回其内存快照文件 (最新版的 Redis 已经不需要让 Slave 阻塞)。
- Master 端收到 sync 命令简单判断是否有正在进行的内存快照子进程,没有则立即开始内存快照,有则等待其结束,当快照完成后会将该文件发送给 Slave 端。
- Slave 端接收 Master 发来的内存快照文件,保存到本地,待接收完成后,清空内存表,重新读取 Master 发来的内存快照文件,重建整个内存表数据结构,并最终状态置位为 REDIS_REPL_CONNECTED 状态,Slave 状态机流转完成。
- Master 端在发送快照文件过程中,接收的任何会改变数据集的命令都会暂时先保存在 Slave 网络连接的发送缓存队列里(list 数据结构),待快照完成后,依次发给 Slave, 之后收到的命令相同处理,并将状态置位为 REDIS_REPL_ONLINE。
整个复制过程完成,流程如下图所示:
Redis 复制机制的缺陷
从上面的流程可以看出,Slave 从库在连接 Master 主库时,Master 会进行内存快照,然后把整个快照文件发给 Slave,也就是没有象 MySQL 那样有复制位置的概念,即无增量复制,这会给整个集群搭建带来非常多的问题。
比如一台线上正在运行的 Master 主库配置了一台从库进行简单读写分离,这时 Slave 由于网络或者其它原因与 Master 断开了连接,那么当 Slave 进行重新连接时,需要重新获取整个 Master 的内存快照,Slave 所有数据跟着全部清除,然后重新建立整个内存表,一方面 Slave 恢复的时间会非常慢,另一方面也会给主库带来压力。
所以基于上述原因,如果你的 Redis 集群需要主从复制,那么最好事先配置好所有的从库,避免中途再去增加从库。
Cache 还是 Storage
了解Redis 的复制与持久化功能后, 我们知道,实际上 Redis 目前发布的版本还都是一个单机版的思路,主要的问题集中在,持久化方式不够成熟,复制机制存在比较大的缺陷,这时我们又开始重新思考 Redis 的定位:Cache 还是 Storage?
如果作为 Cache 的话,似乎除了有些非常特殊的业务场景,必须要使用 Redis 的某种数据结构之外,我们使用 Memcached 可能更合适,毕竟 Memcached 无论客户端包和服务器本身更久经考验。
如果是作为存储 Storage 的话,我们面临的最大的问题是无论是持久化还是复制都没有办法解决 Redis 单点问题,即一台 Redis 挂掉了,没有太好的办法能够快速的恢复,通常几十 G 的持久化数据,Redis 重启加载需要几个小时的时间,而复制又有缺陷,如何解决呢?
Redis 可扩展集群搭建
1. 主动复制避开 Redis 复制缺陷。
既然 Redis 的复制功能有缺陷,那么我们不妨放弃 Redis 本身提供的复制功能,我们可以采用主动复制的方式来搭建我们的集群环境。
所谓主动复制是指由业务端或者通过代理中间件对 Redis 存储的数据进行双写或多写,通过数据的多份存储来达到与复制相同的目的,主动复制不仅限于用在 Redis 集群上,目前很多公司采用主动复制的技术来解决 MySQL 主从之间复制的延迟问题,比如 Twitter 还专门开发了用于复制和分区的中间件 gizzard( https://github.com/twitter/gizzard ) 。
主动复制虽然解决了被动复制的延迟问题,但也带来了新的问题,就是数据的一致性问题,数据写 2 次或多次,如何保证多份数据的一致性呢?
如果你的应用对数据一致性要求不高,允许最终一致性的话,那么通常简单的解决方案是可以通过时间戳或者 vector clock 等方式,让客户端同时取到多份数据并进行校验,如果你的应用对数据一致性要求非常高,那么就需要引入一些复杂的一致性算法比如 Paxos 来保证数据的一致性,但是写入性能也会相应下降很多。
通过主动复制,数据多份存储我们也就不再担心 Redis 单点故障的问题了,如果一组 Redis 集群挂掉,我们可以让业务快速切换到另一组 Redis 上,降低业务风险。
2. 通过 presharding 进行 Redis 在线扩容。
通过主动复制我们解决了 Redis 单点故障问题,那么还有一个重要的问题需要解决:容量规划与在线扩容问题。
我们前面分析过 Redis 的适用场景是全部数据存储在内存中,而内存容量有限,那么首先需要根据业务数据量进行初步的容量规划,比如你的业务数据需要 100G 存储空间,假设服务器内存是 48G,那么根据上一篇我们讨论的 Redis 磁盘 IO 的问题,我们大约需要 3~4 台服务器来存储。这个实际是对现有业务情况所做的一个容量规划,假如业务增长很快,很快就会发现当前的容量已经不够了,Redis 里面存储的数据很快就会超过物理内存大小,那么如何进行 Redis 的在线扩容呢?
Redis 的作者提出了一种叫做 presharding 的方案来解决动态扩容和数据分区的问题,实际就是在同一台机器上部署多个 Redis 实例的方式,当容量不够时将多个实例拆分到不同的机器上,这样实际就达到了扩容的效果。
拆分过程如下:
- 在新机器上启动好对应端口的 Redis 实例。
- 配置新端口为待迁移端口的从库。
- 待复制完成,与主库完成同步后,切换所有客户端配置到新的从库的端口。
- 配置从库为新的主库。
- 移除老的端口实例。
- 重复上述过程迁移好所有的端口到指定服务器上。
以上拆分流程是 Redis 作者提出的一个平滑迁移的过程,不过该拆分方法还是很依赖 Redis 本身的复制功能的,如果主库快照数据文件过大,这个复制的过程也会很久,同时会给主库带来压力。所以做这个拆分的过程最好选择为业务访问低峰时段进行。
Redis 复制的改进思路
我们线上的系统使用了我们自己改进版的 Redis, 主要解决了 Redis 没有增量复制的缺陷,能够完成类似 Mysql Binlog 那样可以通过从库请求日志位置进行增量复制。
我们的持久化方案是首先写 Redis 的 AOF 文件,并对这个 AOF 文件按文件大小进行自动分割滚动,同时关闭 Redis 的 Rewrite 命令,然后会在业务低峰时间进行内存快照存储,并把当前的 AOF 文件位置一起写入到快照文件中,这样我们可以使快照文件与 AOF 文件的位置保持一致性,这样我们得到了系统某一时刻的内存快照,并且同时也能知道这一时刻对应的 AOF 文件的位置,那么当从库发送同步命令时,我们首先会把快照文件发送给从库,然后从库会取出该快照文件中存储的 AOF 文件位置,并将该位置发给主库,主库会随后发送该位置之后的所有命令,以后的复制就都是这个位置之后的增量信息了。
Redis 与 MySQL 的结合
目前大部分互联网公司使用 MySQL 作为数据的主要持久化存储,那么如何让 Redis 与 MySQL 很好的结合在一起呢?我们主要使用了一种基于 MySQL 作为主库,Redis 作为高速数据查询从库的异构读写分离的方案。
为此我们专门开发了自己的 MySQL 复制工具,MySQL-Redis 异构读写分离,可以方便的实时同步 MySQL 中的数据到 Redis 上。
总结:
- Redis 的复制功能没有增量复制,每次重连都会把主库整个内存快照发给从库,所以需要避免向在线服务的压力较大的主库上增加从库。
- Redis 的复制由于会使用快照持久化方式,所以如果你的 Redis 持久化方式选择的是日志追加方式 (aof), 那么系统有可能在同一时刻既做 aof 日志文件的同步刷写磁盘,又做快照写磁盘操作,这个时候 Redis 的响应能力会受到影响。所以如果选用 aof 持久化,则加从库需要更加谨慎。
- 可以使用主动复制和 presharding 方法进行 Redis 集群搭建与在线扩容。
Redis的丰富的数据结构特性及一般使用场景小结:
- 在主页中显示最新的项目列表:Redis使用的是常驻内存的缓存,速度非常快。LPUSH用来插入一个内容ID,作为关键字存储在列表头部。LTRIM用来限制列表中的项目数最多为5000。如果用户需要的检索的数据量超越这个缓存容量,这时才需要把请求发送到数据库。
- 删除和过滤:如果一篇文章被删除,可以使用LREM从缓存中彻底清除掉。
- 排行榜及相关问题:排行榜(leader board)按照得分进行排序。ZADD命令可以直接实现这个功能,而ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
- 按照用户投票和时间排序:排行榜,得分会随着时间变化。LPUSH和LTRIM命令结合运用,把文章添加到一个列表中。一项后台任务用来获取列表,并重新计算列表的排序,ZADD命令用来按照新的顺序填充生成列表。列表可以实现非常快速的检索,即使是负载很重的站点。
- 过期项目处理:使用Unix时间作为关键字,用来保持列表能够按时间排序。对current_time和time_to_live进行检索,完成查找过期项目的艰巨任务。另一项后台任务使用ZRANGE…WITHSCORES进行查询,删除过期的条目。
- 计数:进行各种数据统计的用途是非常广泛的,比如想知道什么时候封锁一个IP地址。INCRBY命令让这些变得很容易,通过原子递增保持计数;GETSET用来重置计数器;过期属性用来确认一个关键字什么时候应该删除。
- 特定时间内的特定项目:这是特定访问者的问题,可以通过给每次页面浏览使用SADD命令来解决。SADD不会将已经存在的成员添加到一个集合。
- Pub/Sub:在更新中保持用户对数据的映射是系统中的一个普遍任务。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,让这个变得更加容易。
- 队列:在当前的编程中队列随处可见。除了push和pop类型的命令之外,Redis还有阻塞队列的命令,能够让一个程序在执行时被另一个程序添加到队列。
redis 使用总结:
- 根据业务需要选择合适的数据类型,并为不同的应用场景设置相应的紧凑存储参数。
- 当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能以及最大的内存使用量。
- 如果需要使用持久化,根据是否可以容忍重启丢失部分数据在快照方式与语句追加方式之间选择其一,不要使用虚拟内存以及 diskstore 方式。
- 不要让你的 Redis 所在机器物理内存使用超过实际内存总量的 3/5。
redis 常见性能问题和解决方案:
- 1 master 最好不要做持久化工作,如RDB 内存快照和AOF 日志文件
- 2 如果数据比较重要,某个slave 开启AOF 备份,策略设置成每秒同步一次
- 3 为了主从复制的速度和连接的稳定性,master 和slave 最好在一个局域网内
- 4 尽量避免在压力大得主库上增加从库
- 主从复制不要采用网状结构,尽量是线性结构。
redis set key value, 其中key 类型都是string 类型。五种基本类型都是对应的value。