【引言】
我们在做数据架构时,除了需要详细了解各类数据库的特性和架构优劣势,还需要站在更高一层的角度去思考如何更好地支持各种各样的业务需求。
数据架构的设计当然也有包含Active MQ,Rocket Mq、kafka、RabbitMQ等消息队列的应用解耦,异步消息,流量削锋等作用,实现高性能,高可用,可伸缩和最终一致性架构。
鉴于现在互联网用户数量大、高并发访问的特性,数据缓存的提出就是一个很自然的事情,业界有句话叫:“性能不够,缓存来凑”;今天我们主要来聊下数据缓存。
最近在系统的学习数据缓存这块的知识,比较有代表性的就是redis、memcached。今天主聊一下redis。
【大纲】
1. redis的概述
2. redis数据类型及使用场景
3. Redis的优点
4. redis的单线程为嘛那么快?
5. Redis高可用如何做?
以下主要内容节选自如下参考
https://blog.csdn.net/qq_36071795/article/details/83988177
文章内容根据自身理解和经验,做了适当整合和内容调整。
一、redis的概述
业务访问量、并发比非常大时,数据库的连接池、处理数据的能力会面临很大的挑战。
关系型数据库的数据持久化是存储在硬盘内,缓存就是内存中存储的热点数据备份。避免数据访问直连数据库,而是去内存中访问数据,可大大降低了数据库的读写压力。
redis是一个非关系型的数据库(not-only-sql即nosql),以键值对方式存储数据,将数据存放在内存中,存取速度快,但是对持久化的支持不够好,故,redis一般配合关系型数据库使用。
一句话:redis可以做数据缓存、分布式缓存,用在数据量大,高并发的情况下。
二、redis的持久化方案RDB和AOF
RDB:快照形式,定期把内存中当前时刻的数据保存到磁盘,是Redis默认支持的持久化方案。特点恢复速度快,但服务器断电的时会丢失部分数据。
AOF(append only file):把所有对redis的操作命令,保存到文件中。数据库恢复时把所有的命令执行一遍即可。
注意:
Redis开启上述两种持久化方式时,优先使用AOF文件来恢复数据库。特点是能保证数据的完整性,但是恢复速度慢。
两者如何选择?
如业务无数据持久化需求,可关闭RDB和AOF;此时redis将变成一个纯内存数据库,如memcached一样。如对数据的完整性要求较高,优选选择AOF;
重点:
官方的建议是两个同时使用,以保障更可靠的持久化方案。
三、Redis数据类型和适用场景
1. Redis五种数据类型
支持string,list,set,sorted set,hash;
2. Redis的5种数据类型的适用场景
String
存放key-value键值对INCR, 等指令本身就具有原子操作的特性,所以我们完全可以利用redis的INCR、INCRBY、DECR、DECRBY等指令来实现原子计数的效果
使用场景:常规key-value缓存应用。常规计数: 微博数,粉丝数
Hash
使用场景:存储部分变更数据,如用户信息。
一个示例来描述下Hash的应用场景:要存储一个用户对象数据,包含以下信息:
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息。如用普通的key/value结构来存储,主要有以下2种存储方式:
第一种方式将用户ID作为查找key,其他信息封装成一个对象以序列化的方式存储。缺点是增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,且修改操作需要对并发进行保护,引入CAS等复杂问题。
第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用户ID 对应属性的名称作为唯一标识来取得对应属性的值,虽省去了序列化开销和并发问题,但是用户ID为重复存储,缺点也很明显:如存在大量这样的数据,内存浪费太大。
这里引出了Redis的Hash解决方案。
Redis的Hash实际是内部存储的Value为一个HashMap,提供了直接存取这个Map成员的接口。
Key仍是用户ID, value是一个Map,这个Map的key是成员的属性名,value是属性值。
这样对数据的修改和存取都可直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过key(用户ID) field(属性标签) 就可以操作对应属性数据了,既不需重复存储数据,也不会带来序列化和并发修改控制问题。
注意:
Redis提供了接口(hgetall)可以直接取到全部的属性数据,但如果内部Map的成员很多,如涉及遍历整个内部Map的操作,由于Redis单线程模型,遍历操作可能会比较耗时,而令其它业务/客户端请求响应延迟或不响应,需特别注意。
Hash具体实现:
上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
HashMap的优势就是查找和操作的时间复杂度都是O(1);
List
List的底层实现是双向链表(LinkedList),向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。
这样的好处是即使是千万个元素的列表,获取头部或尾部的10条记录也是极快的。在10个元素的lists头部插入新元素,和在上千万元素的lists头部插入新元素的速度相同。
使用场景:List可以做消息队列,和kafka类似;
Set
Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用Redis提供的Set数据结构,可以存储一些集合性的数据。
Redis为集合提供了求交集、并集、差集等操作,可方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,可使用不同的命令选择将结果返回给客户端或存到一个新的集合中。
Set是集合,是String类型的无序集合,set是通过Hashtable实现的,概念和数学中个的集合基本类似,可以交集,并集,差集等等,set中的元素是没有顺序的。
注意:
数学中集合中的元素是可以重复的,但是set是一堆不重复值的组合;
当需存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择。且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。
使用场景:点赞。当前用户点赞的话,就将当前用户id存入到对应点赞集合当中,同时判断点反对集合中是否有此id值,有的话就移除; 当前用户点反对的话,与上述操作相反。
Sorted Set
和Set相比,Sorted Set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列并且是插入有序的,即自动排序。
存储全班同学成绩的Sorted Set,其集合value可以是同学的学号,score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用Sorted Set来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
至此为redis的5中数据类型介绍和使用场景。
四、Redis的优点
1. 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2. 支持丰富数据类型,支持string,list,set,sorted set,hash;
3. 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行;
4. 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除。
5. 支持数据持久化(将内存数据持久化到磁盘),支持AOF和RDB两种持久化方式,从而进行数据恢复操作,可以有效地防止数据丢失;
6. 支持主从(master-slave)复制来实现数据备份,主机会自动将数据同步到从机
五、redis的单线程为嘛那么快?
redis分客户端和服务端,一次完整的redis请求事件有如下阶段:
客户端到服务器的网络连接-->redis读写事件发生-->redis服务端的数据处理(单线程)-->数据返回。
平时所说的redis单线程模型,本质上指上述服务端的数据处理。
客户端和服务器是socket通信方式,socket服务端监听可同时接受多个客户端请求也就是说,redis服务同时面对多个redis客户端连接请求,但注意redis服务本身是单线程运行。
redis核心是数据全都在内存里,redis设计者认为单线程操作效率最高的,为毛?
有两个主要原因:一是线程上下文的切换;二是单线程不用引进锁机制
原因1解释:
大家都知道,多线程本质是CPU模拟的,这种模拟线程有一个代价:上下文的切换。对于一个内存的系统来说,没有上下文切换就是效率最高的。
redis用单个CPU绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处理这个事。在内存的情况下,此方案为最佳方案。
但有亲提问,现在的服务都是多CPU,单线程岂不是浪费算力?
这里提供一个解决方案:使用单线程方式是无法发挥多核CPU 性能, 为了充分利用多核CPU算力,可一台server上会启动多个redis进程。而为了保证减少切换开销,有必要为每个redis进程绑定所运行的CPU。
原因2解释:
因redis是单线程,故不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因可能出现的死锁而导致的资源争用和性能消耗;
小结:
大家很清楚,CPU往往不是服务器性能瓶颈,也不会是Redis的瓶颈,Redis瓶颈最有可能是物理内存大小或者网络带宽。既然单线程容易实现,且CPU不会成为瓶颈,那就顺理成章地采用单线程方式。
六、Redis高可用如何做?
主要有两种方式:主从模式和代理模式
方式1:主从模式
熟悉MySQL的亲可能对redis的主从模式更有代入感。通过redis的主从复制机制就可以避免这种单点故障。
Redis主从特性如下:
1. Master可以有多个Slave,不仅主服务器可以有从服务器,从服务器也可以有自己的从服务器;这里建议配置串行一主两从模式,原因为即使发生主从切换,也会保障一主一从模式;
2. 复制在Master端是非阻塞模式的,即便是多个Slave执行首次同步时,也不影响Master保障业务服务;
3. 复制在Slave端也是非阻塞模式的:在redis.conf做了设置,Slave在执行首次同步时仍使用旧数据集提供查询;也可以配置为当Master与Slave失去联系时,让Slave返回客户端一个错误提示;
4. Slave要删掉旧的数据集,并重新加载新版数据时,Slave会阻塞连接请求;
读写分离
主从架构中,可考虑关闭主redis的数据持久化,只让从redis持久化,可提高主redis的处理性能。从redis通常被设置为只读模式,可避免从redis的数据被误修/恶意修改。
主从高可用性切换建议使用哨兵模式
sentinel模式启动redis后,自动监控master/slave的运行状态, 已经被集成在redis2.4 及以上版本。
如redis Master异常,则会进行Master-Slave切换,将其中一个Slave作为Master,将之前的Master作为Slave 。
基本原理是:心跳机制 投票裁决
每个sentinel会向其它sentinal、master、slave定时发送消息,以确认对方是否“活”着,如发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂,所谓“主观认为宕机” Subjective Down,简称SDOWN。
若”哨兵群”中超过多数sentinel均报告某一master没响应,系统才认为该master“彻底死亡”(即:客观上的真正down机,Objective Down,简称ODOWN),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。
方式2:代理模式
twemproxy类似一个代理方式,使用方法和普通redis无任何区别。
设置好代理下管的多个redis实例后,业务连接使用时在本需要连接redis的地方改为连接twemproxy,twemproxy以一个代理身份接收请求,并使用一致性hash算法,将请求转接到具体某一台redis,将结果再返回twemproxy。
注意:twemproxy自身单端口实例的压力,使用一致性hash后,对redis节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。
大家在部署redis高可用架构时,建议使用sentinel模式,其原理为基本原理是:心跳机制 投票裁决,可保证主从模式下高可用性,熟悉MySQL的亲可对比MHA高可用架构理解;当然也可以使用twemproxy代理模式配置高可用架构。