1. 概述
简单来说,Redis就是一个数据结构存储器,可以用作数据库、缓存和消息中间件,它和传统数据库主要有两点不同:
-
它是Key-Value型数据库,不是关系型数据库,所有数据以Key-Value的形式存在服务器的内存中,其中Value可以是多种数据结构:字符串(String), 哈希(hashes), 列表(list), 集合(sets) 和有序集合(sorted sets)等类型;
-
它所有运行时数据都存在内存中,总所周知,内存的存取效率比磁盘要高不止一个数量级,Redis的性能必定非常优秀,根据官方数据,Redis可以轻松支持超过10万次QPS的读写频率;
有了以上两个主要特性支撑,虽然它起步较晚,但发展迅速,目前已经成为主流架构中缓存服务的首选。除了以上两个特性,Redis还提供了非常丰富的能力,包括:
-
原子性,Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行,我们能非常方便地实现事务;
-
支持发布、订阅,可以用来实现消息系统;
-
支持过期逻辑,做缓存时非常实用;
-
支持内存中的数据持久化,不用担心服务器宕机带来的灾难性后果;
-
提供了简单的事务功能,能在一定程度上保证事务特性
-
支持Lua脚本,可以利用Lua创造出新的Redis命令;
-
提供了流水线(Pipeline)功能,这样客户端能将一批命令一次性传到Redis,减少了网络的开销,在请求数据较小的情况下,可以大幅提升吞吐量;
-
Redis使用单线程模型,预防了多线程可能产生的竞争问题,简单且稳定。
-
原生支持主从复制,为高可用实现提供有力支持;
-
受到社区和各大公司的广泛认可,支持Redis的客户端语言非常多,几乎涵盖了主流的编程语言,例如Java、PHP、Python、C、C++、Nodejs等。
适合Redis的应用场景非常多,本节列几个简单的场景供大家参考:
-
缓存频繁读取但修改频率小的数据 ,比如首页推荐的产品列表等,不怕丢数据,丢了可以从数据库中重新加载;
-
用户Session ,不怕丢数据,丢了用户重新登录即可;
-
缓存批量任务的中间结果。不怕丢数据,丢了重新计算中间数据就可以了;
-
分布式锁。
2. 性能
由于各种原因,Redis的性能非常好,也由于Redis的性能非常好,才会有这么多人关注Redis,但是到底有多好,没有实验数据做支撑,别人问咱们的时候,咱们也不好张嘴就来,在本节中我们来做个简单的性能测试,测试目的主要有两点,第一就是看看Redis的性能到底有多好,在数据上有个感性认识,第二,我们来分析下影响Redis性能的因素到底有哪些。
首先我们总结下Redis性能好的原因:
-
业务数据的存取基于内存实现,内存的IO速度很快;
-
Redis使用单线程模型,预防了多线程可能产生的CPU资源争夺造成的性能损耗;
-
支持Pipeline,将一批命令一次性传到Redis,减少了网络的开销。
2.1 性能测试环境
在做性能测试之前,我们把本次测试的环境说清楚。
-
机器性能: 使用一台1核CPU,内存为1G的阿里云服务器去压测一台2核CPU,内存4G的Redis服务器,Redis服务器的CPU型号为2.5 GHz主频的Intel ® Xeon ® E5-2682 v4(Broadwell) 确保压测客户端的机器性能不会遇到瓶颈
-
网络环境: 两台服务器的网卡都使用1000M,在同一个局域网内,内网带宽1000M 两台机器的内网IP分别为:172.17.167.56(Redis服务器)、172.17.167.55(压测机)
-
系统环境 两台服务器都使用CentOS 7,将一下两个参数设置成: vm.overcommit_memory = 1 net.core.somaxconn = 2048
-
Redis相关配置 有三个配置需要注意一下: #后代运行 daemonize yes #不开启applend形式的数据持久化能力 appendfsync no #不开启快照能力
#save <seconds> <changes>
-
测试工具 使用Redis自带的测试工具redis-benchmark进行测试,下面是一条在压测机上运行的测试命令:
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 1000000 -t set -d 100 -P 8 -q
我们简单介绍下这条命令的各个参数的语义:
-h 目标Redis服务网络地址
-p 目标Reids服务的端口
-c 客户端并发长连接数
-n 本次测试需要发起的请求数
-t 测试请求的方法
-d 测试请求的数据大小
-P 开启Pipeline模式,并制定Pipeline通道数量
-q 只显示requests per second这一个结果
上面这条命令的语义就是,向172.17.167.56:6379这个Redis发送100万个请求,使用20个长连接发送,所有请求都是set命令,每个set命令的包体为100字节,使用8条Pipeline通道发送,并且只显示requests per second这一个结果。
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 1000000 -t set -d 100 -P 8 -q
SET: 534759.38 requests per second
2.2 基本性能测试
本节中的性能测试主要观察一个指标,就是Redis每秒处理多少个请求,RPS(Request per second)。
2.2.1 客户端长连接数量对性能的影响
我们做四个测试,分别使用1个长连接,5个长连接,10个长连接,50个长连接发送100万个请求大小为100字节的请求,对比四个测试结果看看客户端长连接数量对Redis服务性能有什么影响:
-
1个长连接 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 1 -n 1000000 -t set -d 100 -q SET: 8768.55 requests per second
-
5个长连接 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 5 -n 1000000 -t set -d 100 -q SET: 35334.44 requests per second
-
10个长连接 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 10 -n 1000000 -t set -d 100 -q SET: 52430.14 requests per second
-
50个长连接 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 50 -n 1000000 -t set -d 100 -q SET: 52413.65 requests per second
从上面三个测试用例的测试结果来看,我们可以发现:
-
只有一个长连接通信时,RPS是8700左右;
-
在长连接数量增加时,RPS的值会接近线性地增加;
-
长连接数量增加到一定数值时,整个Redis的RPS就稳定了,稳定在52400左右;
客户端长连接的数量会影响Redis整体吞吐量,但长连接数量增长到一个平衡值之后,长连接的数量不再影响系统的整体吞吐量,这个平衡值要看实际情况,网速、请求包大小等因素都会有影响。
2.2.2 请求包大小的影响
请求包的大小肯定会影响Redis每秒处理请求数量,这个是毋庸置疑的,但是具体是怎么影响的,我们做几个实验来观察下:
-
请求包大小为2字节 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 10 -n 1000000 -t set -d 2 -q SET: 52474.16 requests per second CPU平均损耗:42%
-
请求包大小为1000字节 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 10 -n 1000000 -t set -d 1000 -q SET: 52430.14 requests per second CPU损耗:48%
-
请求包大小为1400字节 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 10 -n 1000000 -t set -d 1400 -q SET: 45396.77 requests per second CPU平均损耗:41%
-
请求包大小为1500字节 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 10 -n 1000000 -t set -d 1500 -q SET: 25518.67 requests per second CPU平均损耗:29%
-
请求包大小为5000字节 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 10 -n 1000000 -t set -d 5000 -q SET: 12736.74 requests per second CPU平均损耗:24%
-
请求包大小为10000字节 ./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 1000000 -t set -d 10000 -q SET: 6476.81 requests per second CPU平均损耗:18%
从上面六个实验的结果来分析,我们可以得出以下结论:
-
在Redis服务器的CPU资源充足的情况下,2个字节的请求和1000个字节的请求对于Redis的整体吞吐量无任何影响;
-
请求大小在1400字节以内时,Redis的性能表现稳定,当请求大小等于1500字节时,Redis整体性能下降得很厉害。由于一般TCPIP网络的MTU设置为1500字节,一旦测试数据尺寸超过1500字节时会被拆分为多个数据包在网络上传输,加剧了性能下降的幅度。
-
请求大小大于1500之后,Redis的性能随着包体的增加成接近线性关系地下降。
2.3 Pipleline模式
Redis是基于同步的请求应答模型提供服务的,正常情况下,客户端发送一个请求,在等到Redis的应答后才会继续发送第二个请求。在这种情况下,如果同时需要执行大量的命令,每一个长连接的利用率不高,大多数时间都在等待,这种模式长连接的利用率不高,如下图。
Redis 提供了一种聚合请求和应答的pipeline模式,简单说就是讲多个命令聚合在一个请求中发送给Redis,Redis执行这一批命令,在执行过程中,讲执行结果缓存到内存中,等这所有一批命令都被执行完成后,讲所有的命令执行结果放在一个应答中返回给客户端。
Pipeline 在某些场景下非常有用,比如有多个 command对相应结果没有互相依赖,对结果响应也无需立即获得,那么 pipeline 就可以充当这种“批处理”的工具;而且在一定程度上,可以较大的提升性能,性能提升的原因主要是 TCP 连接中减少了“交互往返”的时间。本文图示的例子,三个正常的command一般数据量较小,放在一个pipeline请求,一般一个tcp报文就发送给服务器端了,而非pipeline模式需要发送三次,每次都需要等待应答回来后才能继续发送,在传输与处理效率上,pipeline机制明显要高效很多。下面我们做一个实验来验证下具体效率会高多少。
-
命令大小为100字节,不使用pipeline传输
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 1000000 -t set -d 100 -q
SET: 52394.43 requests per second
-
命令大小为100字节,使用pipeline传输,每个请求携带8个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 2000000 -t set -d 100 -q -P 8
SET: 495662.97 requests per second
-
命令包大小为100字节,使用pipeline传输,每个请求携带10个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 2000000 -t set -d 100 -q -P 10
SET: 558659.25 requests per second
-
命令包大小为100字节,使用pipeline传输,每个请求携带11个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 2000000 -t set -d 100 -q -P 11
SET: 312940.09 requests per second
-
命令包大小为100字节,使用pipeline传输,每个请求携带15个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 2000000 -t set -d 100 -q -P 15
SET: 452386.34 requests per second
-
请求包大小为100字节,使用pipeline传输,每个请求携带40个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 5000000 -t set -d 100 -q -P 40
SET: 487519.53 requests per second
-
请求包大小为200字节,不使用pipeline传输
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 2000000 -t set -d 200 -q
SET: 51167.91 requests per second
-
请求包大小为200字节,使用pipeline传输,每个请求携带4个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 2000000 -t set -d 200 -q -P 4
SET: 220288.56 requests per second
-
请求包大小为200字节,使用pipeline传输,每个请求携带6个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 2000000 -t set -d 200 -q -P 6
SET: 161147.36 requests per second
-
请求包大小为200字节,使用pipeline传输,每个请求携带8个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 2000000 -t set -d 200 -q -P 8
SET: 221361.38 requests per second
-
请求包大小为3000字节,不使用pipeline传输
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 1000000 -t set -d 3000 -q
SET: 21104.17 requests per second
-
请求包大小为3000字节,使用pipeline传输,每个请求携带8个命令
./redis-benchmark -h 172.17.167.56 -p 6379 -c 20 -n 1000000 -t set -d 3000 -q -P 8
SET: 21713.17 requests per second
从上面一系列实验数据我们可以得出以下结论:
-
命令包体越小时,pipeline机制会对Redis的整体性能帮助越大,命令包体100字节时,redis整体性能有一个数量级左右的提升;
-
一个pineline请求的整体大小(命令包大小乘以命令个数)如果和TCPIP网络的MTU设置比较匹配,不容易产生碎片请求时,性能最好,这个不好强求;
-
命令包体较大时,pipeline机制对Redis的整体性能没有任何帮助。
在命令传输内容较小,且命令之间无依赖关系时,我们使用pipeline机制可以大幅提供Redis的整体吞吐量。有些系统可能对可靠性要求很高,每次操作都需要立马知道这次操作是否成功,是否数据已经写进redis了,那这种场景就不适合。还有命令传输的内容较大时(比如3k及以上),pipeline对性能也没有优化能力,也不建议使用pipeline机制。
3. 数据持久化
Redis是一个支持持久化的内存数据库,也就是说redis支持将内存中的数据同步到磁盘来保证持久化。redis支持两种持久化方式,第一种是定期讲包含全量数据的内存快照保存到磁盘也是默认方式;第二种是记录所有数据写操作的日志,使用这些日志恢复数据,这种模式我们又称之为AOF模式。两种模式各有优劣,下面我们分别了解下两种模式的运行机制。
3.1 内存快照模式持久化
某个瞬间Redis服务器内存中的所有内容我们称之为内存快照,定期将内存快照异步保存到磁盘进行持久化,在数据出现问题,或者服务器宕机等情况出现是,将内存快照加载到内存,这个定期备份、恢复的机制对数据的安全性有重要的意义。
Redis会自动将内存快照保存成一个RDB类型的文件到Redis的根目录,调用BGSAVE命令能手动触发快照保存,保存快照的动作是后台进程完成的,保存快照期间其他客户端仍然和可以读写REDIS服务器。后台保存快照到磁盘时会占用大量内存。
如果调用SAVE命令保存内存中的数据到磁盘,将阻塞客户端请求,直到保存完毕。调用SHUTDOWN命令,Redis服务器会先调用SAVE,所有数据持久化到磁盘之后才会真正退出。在Redis的配置文件中可以配置定期保存内存快照的触发条件:
# save <时间> <变更次数>
# 两个条件同时满足,发生一次保存内存快照的动作,如果配置多条规则,规则之间是或的关系
# 如果不想落地内存中的数据,直接注释掉下面三个配置即可
# 如果配置成save "",之前落地的数据都可能被删除
# 下面这条配置的语义是距上次保存快照时间超过60秒,并且数据变更次数达到1000次,则保存一次内存快照
# 内存快照文件格式为dump.rdb
save 60 1000
stop-writes-on-bgsave-error yes 这个配置也是非常重要的一项配置,这是当备份进程出错时,主进程就停止接受新的写入操作,是为了保护持久化的数据一致性问题。如果自己的业务对数据一致性有较高的要求,需要打开这个配置。
Redis启动的时候会判断是否存在RDS文件,如果存在就从RDS文件中加载所有数据到内存。
3.2 AOF模式持久化
默认情况下Redis会异步落地内存快照数据到磁盘,这种模式对于很多场景是够用的。但这种模式有个缺点就是对于突发情况,比如突然停电,落地的文件数据会丢失几分钟数据,极端情况丢数据这事对于普通应用程序可能可以接收,但对于类似银行这种机构是苟能容忍的。因此Redis提供一种更可靠的模式来保证数据的安全,AOF是一种可选的更安全的持久化模式,能很好地解决上面说的数据丢失的问题。默认配置下,AOF模式在意外故障发生时最多丢失一秒钟的数据。
AOF文件是可识别的纯文本,它的内容就是一个个的Redis标准命令,有比较好的可读性。AOF日志也不是完全按客户端的请求来生成日志的,比如命令 INCRBYFLOAT 在记AOF日志时就被记成一条SET记录, 因为浮点数操作可能在不同的系统上会不同,所以为了避免同一份日志在不同的系统上生成不同的数据集,所以这里只将操作后的结果通过SET来记录。每一条写命令都生成一条日志,所以AOF文件会很大。
Redis在落地AOF文件的时候,有三种模式
-
appendfsync always : 每次有客户端发送写操作,都需要落地到磁盘,性能最差,但最安全。
-
appendfsync everysec : 顾名思义,每秒写一次,均衡模式。
-
appendfsync no : 操作系统在需要的时候才落地数据到磁盘,性能最好,但可能有数据丢失风险。对大多数Linux操作系统,是每30秒进行一次fsync,将缓冲区中的数据写到磁盘上。
Redis实用的默认模式是everysec,这是一种均衡的模式。
在AOF同步文件同步模式设置为always或者everysec的时候,会有一个后台线程去做这个事,同时产生大量磁盘IO。这些IO操作经常会阻塞后台内存快照落地线程和AOF日志重写线程,甚至导致整个Redis被阻塞,目前没有很好的解决方案。
为了缓解这个问题,Redis增加了AOF阻塞机制,生成AOF文件之前会先检查BGSAVE或者BGREWRITEAOF是否在运行,如果是,那么就先阻止AOF操作。这就意味这在BGSAVE或者BGREWRITEAOF时,Redis不会去写AOF,可能会因此丢掉30秒以内的数据。如果你因为AOF写入产生延迟问题,可以将AOF阻塞机制的相关配置no-appendfsync-on-rewrite设置为yes。该配置设置为no为最安全,最不可能丢失数据的方式。
AOF和内存快照两种持久化模式能同时启动,不会互相影响。如果AOF模式生效了,那么Redis启动的时候会首先载入AOF文件来保证数据的可靠性。
3.3 AOF重写
在AOF文件增长到足够大超过配置的百分比的时候,Redis提供AOF重写功能,AOF重写会聚合Key的所有操作,目的是让一个KEY只有一条记录留在AOF文件中,从而大大缩小AOF文件的尺寸。
AOF重写是重新生成一份AOF文件,新的AOF文件中一条记录的操作只会有一次,而不像一份老文件那样,可能记录了对同一个值的多次操作。其生成过程和RDB类似,也是fork一个进程,直接遍历数据,写入新的AOF临时文件。 在写入新文件的过程中,所有的写操作日志还是会写到原来老的 AOF文件中,同时还会记录在内存缓冲区中。当重完操作完成后,会将所有缓冲区中的日志一次性写入到临时文件中。然后调用原子性的rename命令用新的 AOF文件取代老的AOF文件。重写后,AOF文件变成一个非常小的全量文件。
命令:BGREWRITEAOF, 我们应该经常调用这个命令来来重写。
auto-aof-rewrite-percentage 100
当前的AOF文件大小超过上一次重写的AOF文件大小的百分之多少时会再次进行重写,如果之前没有重写过,则以启动时的AOF大小为依据。
auto-aof-rewrite-min-size 64mb
限制了允许重写的最小AOF文件尺寸。
3.4 从持久化文件中恢复数据
Redis重启的时候会自动从RDS文件或者AOF文件中加载数据到内存。Redis在启动的时候会优先判断AOF持久化文件是否存在,如果存在优先加载AOF持久化文件。如果AOF持久化文件不存在再去检查RDS持久化文件是否存在,存在的话,加载之。为什么优先加载AOF文件呢,因为AOF在持久化上能够做到更加安全。具体流程如下图所示:
3.5 持久化策略选择
不同的业务场景选择不同的持久化策略,具体业务场景分为以下几种:
-
我们仅仅把Redis当做缓存来使用,Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,比如排行榜数据,用户登录信息等,可以关闭持久化,如果丢失数据可以通过其它途径补回,这种情况最高效,也不会引起各种数据不一致问题;
-
Redis中的数据独此一份,并没有数据库打底,但Redis中数据并不特别重要,可以忍受丢失一段时间,比如一些统计计算的中间数据,丢失的数据可以根据原始数据重新计算出来,这个时候我们可以选择RDS或者appendfsync always模式以外的所有模式的AOF。这是一种讨巧的持久化方案,既不消耗很多性能,又能在出现意外情况时恢复大部分数据;
-
Redis中的数据独此一份,除了没有数据库打底,还非常重要,不允许丢失任何数据。这个时候我们必须使用appendfsync always模式的AOF持久化策略,并且把no-appendfsync-on-rewrite配置打开,保证AOF吃花花过程出现问题的时候,Redis拒绝服务,不会丢失任何数据。
还有一种做法就是将Redis的主从配置打开,利用一台从服务器去做持久化,其他服务器快速应答业务请求。
4. Redis数据结构详解
Redis并不是简单的key-value存储,实际上他是一个数据结构服务器,支持不同类型的值。也就是说,你不必仅仅把字符串当作键所指向的值。下列这些数据类型都可作为值类型。
二进制安全的 字符串 string
二进制安全的 字符串列表 list of string
二进制安全的 字符串集合 set of string,换言之:它是一组无重复未排序的element。可以把它看成JAVA中的HashSet。
有序集合sorted set of string,类似于集合set,但其中每个元素都和一个浮点数score(评分)关联。element根据score排序。可以把它看成JAVA的HashMap–其key等于element,value等于score,但元素总是按score的顺序排列,无需额外的排序操作。
4.1 Key
Redis key值是二进制安全的,这意味着可以用任何二进制序列作为key值,比如”foo”的简单字符串到一个JPEG文件的内容都可以。空字符串也是有效key值。
关于key的几条规则:
太长的键值不是个好主意,例如1024字节的键值就不是个好主意,不仅因为消耗内存,而且在数据中查找这类键值的计算成本很高。
太短的键值通常也不是好主意,如果你要用”u:1000:pwd”来代替”user:1000:password”,这没有什么问题,但后者更易阅读,并且由此增加的空间消耗相对于key object和value object本身来说很小。当然,没人阻止您一定要用更短的键值节省一丁点儿空间。
最好坚持一种模式。例如:”object-type:id:field”就是个不错的注意,像这样”user:1000:password”。
4.2 Strings
这是最简单Redis类型。如果你只用这种类型,Redis就像一个可以持久化的memcached服务器(注:memcache的数据仅保存在内存中,服务器重启后,数据将丢失)。
我们来玩一下字符串类型。
4.2.1 操作
$ redis-cli set mykey "my binary safe value"
OK
$ redis-cli get mykey
my binary safe value
正如你所见到的,通常用SET command 和 GET command来设置和获取字符串值。
值可以是任何种类的字符串(包括二进制数据),例如你可以在一个键下保存一副jpeg图片。值的长度不能超过1GB。
虽然字符串是Redis的基本值类型,但你仍然能通过它完成一些有趣的操作。例如:原子递增:
$ redis-cli set counter 100
OK $ redis-cli incr counter
(integer) 101
$ redis-cli incr counter
(integer) 102
$ redis-cli incrby counter 10
(integer) 112
INCR 命令将字符串值解析成整型,将其加一,最后将结果保存为新的字符串值,类似的命令有INCRBY, DECR and DECRBY。实际上他们在内部就是同一个命令,只是看上去有点不同。
INCR是原子操作意味着什么呢?就是说即使多个客户端对同一个key发出INCR命令,也决不会导致竞争的情况。例如如下情况永远不可能发生:客户端1和客户端2同时读出“10”,他们俩都对其加到11,然后将新值设置为11。最终的值一定是12,read-increment-set操作完成时,其他客户端不会在同一时间执行任何命令。
对字符串,另一个的令人感兴趣的操作是GETSET命令,顾名思义:他为key设置新值并且返回原值。这有什么用处呢?例如:你的系统每当有新用户访问时就用INCR命令操作一个Redis key。你希望每小时对这个信息重置一次。你就可以GETSET这个key并给其赋值0并读取原值。
4.2.2 使用场景
UserId的生成,我们直接使用Redis的Strings数据结构,主要用到了INCR的原子性和Redis的全局性两个特点。
内容变更不频繁的对象,直接用protobuff序列化之后,以字符串的形式写入Redis,需要的时候把字符串从Redis中取出,然后反序列化成对象后使用。比如用户基本信息的缓存我们就用这种形式存储。
这种做法的好处是:存储、获取的时候代码很简单,不容易出错;
对于这种对象,如果对象内容发生变更,一般的处理都是在变更时直接删除Redis中的对象对应的字符串,下次需要使用该对象的时候生成字符串写入Redis。
4.3 List
4.3.1 操作
一般意义上讲,列表就是有序元素的序列:10,20,1,2,3就是一个列表。但用数组实现的List和用Linked List实现的List,在属性方面大不相同。
Redis lists基于Linked Lists实现。这意味着即使在一个list中有数百万个元素,在头部或尾部添加一个元素的操作,其时间复杂度也是非常小。用LPUSH 命令在十个元素的list头部添加新元素,和在千万元素list头部添加新元素的速度相同。
那么,坏消息是什么?在数组实现的list中利用索引访问元素的速度极快,而同样的操作在linked list实现的list上没有那么快。
Redis Lists用linked list实现的原因是:对于数据库系统来说,至关重要的特性是:能非常快的在很大的列表上添加元素。另一个重要因素是,正如你将要看到的:Redis lists能在非常短时间取得常数长度。
LPUSH 命令可向list的左边(头部)添加一个新元素,而RPUSH命令可向list的右边(尾部)添加一个新元素。最后LRANGE 命令可从list中取出一定范围的元素
$ redis-cli rpush messages "Hello how are you?"
OK
$ redis-cli rpush messages "Fine thanks. I‘m having fun with Redis"
OK
$ redis-cli rpush messages "I should look into this NOSQL thing ASAP"
OK
$ redis-cli lrange messages 0 2
1. Hello how are you?
2. Fine thanks. I‘m having fun with Redis
3. I should look into this NOSQL thing ASAP
注意LRANGE 带有两个索引,一定范围的第一个和最后一个元素。这两个索引都可以为负来告知Redis从尾部开始计数,因此-1表示最后一个元素,-2表示list中的倒数第二个元素,以此类推。
4.3.2 使用场景
List最大的优点就是你可以每次都以原先添加的顺序访问数据。对于只需要顺序批量读取,不需要按照特定值检索的数据,我们使用Lists数据结构。
比如IM系统中的未读消息,就可以存在Lists中,每次读出来后就可以删除。
Blog系统里面的Feed列表,也可以存储在List中,使用LRANGE可以实现分页。每个Feed的评论也可以单独存储一个List,写入读取都很方便。
一些网站 的一些访问量比较大的内容,比如推荐文章,热门用户等内容都比较适合使用Lists结构来存储。
4.4 Hashes
4.4.1 操作
Redis拥有一个键值对的数据结果,类似Java中的HashMap
> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"
Hahes数据结构用来存储对象非常方便,基本上你想存储多少个字段到对象中都可以(除非超过内存限制)。
HMSET可以设置多个键值对到Hashes对象中去,HGET只能获取一个键值对出来。HMGET可以获取多个键值对出来。
> hmget user:1000 username birthyear no-such-field
1) "antirez"
2) "1977"
3) (nil)
HINCRBY之类的命令可以针对Hashes对象中的某一个键值对进行计算操作:
> hincrby user:1000 birthyear 10
(integer) 1987
> hincrby user:1000 birthyear 10
(integer) 1997
需要提醒大家的是,比较小的Hashes对象(拥有的元素少)被专门优化过,会得到一个非常好的性能。
4.4.2 使用场景
对于需要经常变更的对象,我们使用Hashes结构来存储。
好处显而易见,第一你可以给一个对象存储任意多的字段,第二访问很方便,不用频繁序列化和反序列化。
在IM系统中,用户在线状态就推荐用Hashes结果来存储。keepalive的时候会非常高效,IM的keepalive的请求量是非常大的。
对于一些列配置类的数据,也比较适合用Hashes来缓存。比如会员的相关配置就可以都存在一个Hashes结构中,取起来很方便。
4.5 Sets
4.5.1 操作
Sets集合是未排序的集合,其元素是二进制安全的字符串。SADD命令可以向集合添加一个新元素。和sets相关的操作也有许多,比如检测某个元素是否存在,以及实现交集,并集,差集等等。一例胜千言:
$ redis-cli sadd myset 1
(integer) 1
$ redis-cli sadd myset 2
(integer) 1
$ redis-cli sadd myset 3
(integer) 1
$ redis-cli smembers myset
1. 3
2. 1
3. 2
我向集合中添加了三个元素,并让Redis返回所有元素。如你所见它们是无序的。
现在让我们检查某个元素是否存在:
$ redis-cli sismember myset 3
(integer) 1
$ redis-cli sismember myset 30
(integer) 0
"3"是这个集合的成员,而"30"不是。集合特别适合表现对象之间的关系。例如用Sets集合可以很容易实现标签功能。
下面是一个简单的方案:对每个想加标签的对象,用一个标签ID集合与之关联,并且对每个已有的标签,一组对象ID与之关联。
例如假设我们的新闻ID 1000被加了三个标签tag 1,2,5和77,就可以设置下面两个集合:
$ redis-cli sadd news:1000:tags 1
(integer) 1
$ redis-cli sadd news:1000:tags 2
(integer) 1
$ redis-cli sadd news:1000:tags 5
(integer) 1
$ redis-cli sadd news:1000:tags 77
(integer) 1
$ redis-cli sadd tag:1:objects 1000
(integer) 1
$ redis-cli sadd tag:2:objects 1000
(integer) 1
$ redis-cli sadd tag:5:objects 1000
(integer) 1
$ redis-cli sadd tag:77:objects 1000
(integer) 1
要获取一个对象的所有标签,如此简单:
$ redis-cli smembers news:1000:tags
1. 5
2. 1
3. 77
4. 2
而有些看上去并不简单的操作仍然能使用相应的Redis命令轻松实现。例如我们也许想获得一份同时拥有标签1, 2, 10和27的对象列表。这可以用SINTER命令来做,他可以在不同集合之间取出交集。因此为达目的我们只需:
$ redis-cli sinter tag:1:objects tag:2:objects tag:10:objects tag:27:objects
... no result in our dataset composed of just one object ;) ...
在命令参考文档中可以找到和集合相关的其他命令,令人感兴趣的一抓一大把。一定要留意SORT命令,Redis集合和list都是可排序的。
4.5.2 使用场景
需要经常判断值是否在列表中,比如系统黑名单,被屏蔽的图片,在线列表等等,我们用Sets结构存储效率最高。
4.6 Sorted Sets
集合是使用频率很高的数据类型,但是…对许多问题来说他们也有点儿太不讲顺序了;因此Redis1.2引入了有序集合。他和集合非常相似,也是二进制安全的字符串集合,但是这次带有关联的score,以及一个类似LRANGE的操作可以返回有序元素,此操作只能作用于有序集合,它就是,ZRANGE 命令。
基本上有序集合从某种程度上说是SQL世界的索引在Redis中的等价物。例如在上面提到的reddit.com例子中,并没有提到如何根据用户投票和时间因素将新闻组合生成首页。我们将看到有序集合如何解决这个问题,但最好先从更简单的事情开始,阐明这个高级数据类型是如何工作的。让我们添加几个黑客,并将他们的生日作为“score”。
$ redis-cli zadd hackers 1940 "Alan Kay"
(integer) 1
$ redis-cli zadd hackers 1953 "Richard Stallman"
(integer) 1
$ redis-cli zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
$ redis-cli zadd hackers 1916 "Claude Shannon"
(integer) 1
$ redis-cli zadd hackers 1969 "Linus Torvalds"
(integer) 1
$ redis-cli zadd hackers 1912 "Alan Turing"
(integer) 1
对有序集合来说,按生日排序返回这些黑客易如反掌,因为他们已经是有序的。有序集合是通过一个dual-ported 数据结构实现的,它包含一个精简的有序列表和一个hash table,因此添加一个元素的时间复杂度是O(log(N))。这还行,但当我们需要访问有序的元素时,Redis不必再做任何事情,它已经是有序的了:
$ redis-cli zrange hackers 0 -1
1. Alan Turing
2. Claude Shannon
3. Alan Kay
4. Richard Stallman
5. Yukihiro Matsumoto
6. Linus Torvalds
你知道Linus比Yukihiro年轻吗?无论如何,我想反向对这些元素排序,这次就用 ZREVRANGE 代替 ZRANGE 吧:
$ redis-cli zrevrange hackers 0 -1
1. Linus Torvalds
2. Yukihiro Matsumoto
3. Richard Stallman
4. Alan Kay
5. Claude Shannon
6. Alan Turing
大家需要注意,ZSets只是有一个“默认的”顺序,但你仍然可以用 SORT 命令对有序集合做不同的排序(但这次服务器要耗费CPU了)。要想得到多种排序,一种可选方案是同时将每个元素加入多个有序集合。
有序集合之能不止于此,他能在区间上操作。例如获取所有1950年之前出生的人。我们用 ZRANGEBYSCORE 命令来做:
$ redis-cli zrangebyscore hackers -inf 1950
1. Alan Turing
2. Claude Shannon
3. Alan Kay
我们请求Redis返回score介于负无穷到1950年之间的元素(两个极值也包含了)。
也可以删除区间内的元素。例如从有序集合中删除生日介于1940到1960年之间的黑客。
$ redis-cli zremrangebyscore hackers 1940 1960
(integer) 2
ZREMRANGEBYSCORE 这个名字虽然不算好,但他却非常有用,还会返回已删除的元素数量。
5. 集群高可用
要实现Redis的高可用,我们首先得建立一个Redis集群,集群内有两个或以上的服务节点,我们主要要实现一个目标:集群内的一台Redis服务节点坏了,不应该影响业务。下面是Redis常见的两种高可用方案的描述。
主备方式
这种通常是一台主机、一台或多台备机,在正常情况下主机对外提供服务,并把数据同步到备机,当主机宕机后,备机立刻开始服务。 Redis主备切换使用比较多的是keepalived,它使主机备机对外提供同一个虚拟IP,客户端通过虚拟IP进行数据操作,正常期间主机一直对外提供服务,宕机后VIP自动漂移到备机上。优点是对客户端毫无影响,仍然通过VIP操作。 缺点也很明显,在绝大多数时间内备机是一直没使用,被浪费着的。
主从方式
这种采取一主多从的办法,主从之间进行数据同步。 当Master宕机后,通过选举算法(Paxos、Raft)从slave中选举出新Master继续对外提供服务,主机恢复后以slave的身份重新加入。 主从另一个目的是进行读写分离,这是当单机读写压力过高的一种通用型解决方案。 其主机的角色只提供写操作或少量的读,把多余读请求通过负载均衡算法分流到单个或多个slave服务器上。
缺点是主机宕机后,Slave虽然被选举成新Master了,但对外提供的IP服务地址却发生变化了,意味着会影响到客户端。 解决这种情况需要一些额外的工作,在当主机地址发生变化后及时通知到客户端,客户端收到新地址后,使用新地址继续发送新请求。
方案选择
主备(keepalived)方案配置简单、人力成本小,在数据量少、压力小的情况下推荐使用。 如果数据量比较大,不希望过多浪费机器,推荐使用主从方式,主从方式需要额外的监控、通知等环节,较复杂。主从方式的高可用方案使用读写分离来提高资源的使用率,但会带来一个问题,在数据主从同步过程中产生数据不一致的问题,而主备模式不会。
本章不详细描述Redis的高可用解决方案,本书有专门章节专门介绍这种有状态类服务的高可用整体方案,会详细介绍如何实现高可用,如何通过数据分片的方式来实现海量数据的存储等问题。
5.1 主从数据同步
用slaveof的配置来设置本机的Redis作为从实例,实时从主实例读取数据,成为其镜像。主要是高可用场景需要本功能。以下是Redis主从同步的一些特性:
-
同一个Master可以同步多个Slaves;
-
Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力。因此我们可以将Redis的Replication架构视为图结构;
-
Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求;
-
Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据;
-
为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成。即便如此,系统的伸缩性还是得到了很大的提高;
-
Master可以将数据快照保存操作交给Slaves完成,从而避免了在Master中要有独立的进程来完成此操作,减轻Master的压力;
-
Redis的数据同步是异步进行的,你可以配置主实例在和从实例断掉连接的时候停止接受客户端发过来的写请求,从而保证数据的一致性,这样可以保证数据的一致性,但也会因为从节点的故障导致整个Redis的故障。
-
同步是自动完成的,不需要人工干预。主从之间的网络短暂的断开后,从再次连上主之后,会自动从上次断开的时候同步数据。
-
当新的从实例连入主实例,或者从实例断开连接时间比较长,再连入主实例的时候,为了保证数据一致,主实例会将全量数据同步给从实例。这种主要有两种模式: - 文件模式:主实例创建一个任务去写DB文件到磁盘,文件创建完毕后,主实例增量方式读取文件中的数据传输给从实例;使用Disk-backed模式同步数据有个好处就是,这个文件一旦生成,多个slave实例过来全量同步,都可以重用这一个文件; - 网络模式:主实例创建一个任务把全量数据直接写入从实例的socket连接上,数据不落地。;使用Diskless模式同步全量数据,一旦一个全量同步行为开始了,其他slave实例的同步请求过来时,只能先排到队列里面去等下一次全量同步开始。 使用diskless的数据同步时,master实例会等一会(时间可配),看看这段时间内是否有多个slave实例同时请求全量同步,好凑齐了一块给所有实例传输数据。 在网络带宽充裕的情况下,diskless的同步避免了磁盘io,性能会好很多。
5.2 Redis Sentinel(哨兵)
Redis提供一个叫Redis Sentinel的监控程序做主从监控和主从切换工作。他主要有以下功能: - 监控(Monitoring): Redis Sentinel实时监控主服务器和从服务器运行状态。 - 提醒(Notification):当被监控的某个 Redis 服务器出现问题时, Redis Sentinel 可以向系统管理员发送通知, 也可以通过 API 向其他程序发送通知。 - 自动故障转移(Automatic failover): 当一个主服务器不能正常工作时,Redis Sentinel 可以将一个从服务器升级为主服务器, 并对其他从服务器进行配置,让它们使用新的主服务器。当应用程序连接到 Redis 服务器时, Redis Sentinel会告之新的主服务器地址和端口。 Redis Sentinel 是一个分布式系统, 你可以在架构中运行多个 Sentinel 进程,这些进程通过相互通讯来判断一个主服务器是否断线,以及是否应该执行故障转移。 在配置Redis Sentinel时,至少需要有1个Master和1个Slave。当Master失效后,Redis Sentinel会报出失效警告,并通过自动故障转移将Slave提升为Master,并提供读写服务;当失效的Master恢复后,Redis Sentinel会自动识别,将Master自动转换为Slave并完成数据同步。
目前并未见过大型项目使用Redis Sentinel用于生产,这个方案较复杂,排查问题较难,还容易出问题,这个方案仅供大家了解下。
6. 内存限制及LRU自动清理Key
Redis可以使用maxmemory来配置内存的使用上限,一旦Redis使用的内存达到设置的上限,那么会出现两种情况:
-
按照LRU算法自动清理过期Key来释放内存;
-
拒绝所有客户端发上来的写请求;
Redis有个配置叫maxmemory-policy,这个配置决定了Redis内存触限后的处理策略:
-
volatile-lru :根据LRU算法删除过期Key
-
allkeys-lru :根据LRU算法删除所有Key
-
volatile-random :随机删除过期数据
-
allkeys-random :随机删除任意数据
-
volatile-ttl :根据最近过期时间来删除(辅以TTL)
-
noeviction:不删除任何数据,拒绝客户端写请求
如果Reids在当前策略下找不到可以删除的key,那么Redis会拒绝所有客户端的写请求。 写请求的命令包括: set setnx setex append incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby getset mset msetnx exec sort 本配置项默认值为 : maxmemory-policy noeviction
7. 安全
Redis 被设计成仅有可信环境下的可信用户才可以访问,这意味着将 Redis 实例直接暴露在网络上或者让不可信用户可以直接访问 Redis的服务端口,是不安全的。正常情况下,使用Redis的应用程序是将Redis作为数据库,缓存,消息系统,应用程序访问Redis是内网行为,因此Redis一个非常重要的安全措施就是,只绑定内网地址作为自己的服务地址,并且在防火墙上配置成对外隔离,只有内部应用服务可以访问 。
如果仅供本机访问:
bind 127.0.0.1
如果仅供局域网访问:
bind 192.168.0.1
尽量避免绑定成:
bind 0.0.0.0
虽然 Redis 没有尝试去实现访问控制,但是提供了一个轻量级的认证方式,可以编辑redis.conf 文件来启用。当认证授权方式启用后,Redis 将会拒绝来自没有认证的用户的任何查询。一个客户端可以通过发送 AUTH 命令并带上密码来给自己授权。如果你的内网环境有不信任的主机在运行,那么你需要设置这个密码。如果你的内网是安全的,那么不建议设置本密码。
这个密码由系统管理员在redis.conf 文件里面用明文设置,它需要足够长以应对暴力攻击,Redis允许客户端每秒尝试15万次密码匹配,如果你密码不够强,很容易被破解。但同时密码控制也会影响到从库复制,从库必须在配置文件里使用masterauth 指令配置相应的密码才可以进行复制操作。
Redis还有一招挺绝的,就是修改高级别的命令的指令名。比如把CONFIG修改成b840fc02d524045429941cc15f59e41cb7be6c52,基本上就可以避免外部人员调用本命令了。
Redis也可以屏蔽一些命令名,使用rename-command命令将命令的指令名设置为空字符串即可。需要注意的是,这些对命令名的修改也会同步到AOF文件中,或者传输给从实例中,引起其他问题。