redis的学习笔记
一、mysql存储性能方面瓶颈及演变
①单机mysql
②Memcached(缓存)+MySQL+垂直拆分
③Mysql主从读写分离
④分表分库+水平拆分+mysql集群
⑤MySQL的扩展性瓶颈
MySQL数据库也经常存储一些大文本字段,导致数据库表非常的大,在做数据库恢复的时候就导致非常的慢,不容易快速恢复数据库。比如1000万4KB大小的文本就接近40GB的大小,如果能把这些数据从MySQL省去,MySQL将变得非常的小。关系数据库很强大,但是它并不能很好的应付所有的应用场景。MySQL的扩展性差(需要复杂的技术来实现),大数据下IO压力大,表结构更改困难,正是当前使用MySQL的开发人员面临的问题
⑥今天的样子
二、nosql的介绍
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题,包括超大规模数据的存储。(例如谷歌或Facebook每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。
三、Nosql的数据模型
①聚合模型
(1)kv键值
(2)文档型数据库(bson格式比较多)
(3)列族
顾名思义,是按列存储数据的。最大的特点是方便存储结构化和半结构化数据,方便做数据压缩,对针对某一列或者某几列的查询有非常大的IO优势。
(4)图形
②四者对比
四、核心理念CAP+BASE
①CAP
(1)cap概念
经典CAP
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,
最多只能同时较好的满足两个。
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
(2)CAP的3进2
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。
而由于当前的网络硬件肯定会出现延迟丢包等问题,所以
分区容忍性是我们必须需要实现的。
所以我们只能在一致性和可用性之间进行权衡,没有NoSQL系统能同时保证这三点。
=======================================================================================================================
C:强一致性 A:高可用性 P:分布式容忍性
CA 传统Oracle数据库
AP 大多数网站架构的选择
CP Redis、Mongodb
注意:分布式架构的时候必须做出取舍。
一致性和可用性之间取一个平衡。多余大多数web应用,其实并不需要强一致性。
因此牺牲C换取P,这是目前分布式数据库产品的方向
=======================================================================================================================
一致性与可用性的决择
对于web2.0网站来说,关系数据库的很多主要特性却往往无用武之地
数据库事务一致性需求
很多web实时系统并不要求严格的数据库事务,对读一致性的要求很低, 有些场合对写一致性要求并不高。允许实现最终一致性。
数据库的写实时性和读实时性需求
对关系数据库来说,插入一条数据之后立刻查询,是肯定可以读出来这条数据的,但是对于很多web应用来说,并不要求这么高的实时性,比方说发一条消息之 后,过几秒乃至十几秒之后,我的订阅者才看到这条动态是完全可以接受的。
对复杂的SQL查询,特别是多表关联查询的需求
任何大数据量的web系统,都非常忌讳多个大表的关联查询,以及复杂的数据分析类型的报表查询,特别是SNS类型的网站,从需求以及产品设计角 度,就避免了这种情况的产生。往往更多的只是单表的主键查询,以及单表的简单条件分页查询,SQL的功能被极大的弱化了。
②BASE
BASE就是为了解决关系数据库强一致性引起的问题而引起的可用性降低而提出的解决方案。
BASE其实是下面三个术语的缩写:
基本可用(Basically Available)
软状态(Soft state)
最终一致(Eventually consistent)
它的思想是通过让系统放松对某一时刻数据一致性的要求来换取系统整体伸缩性和性能上改观。为什么这么说呢,缘由就在于大型系统往往由于地域分布和极高性能的要求,不可能采用分布式事务来完成这些指标,要想获得这些指标,我们必须采用另外一种方式来完成,这里BASE就是解决这个问题的办法
③ 分布式+集群简介
分布式系统
分布式系统(distributed system)
由多台计算机和通信的软件组件通过计算机网络连接(本地网络或广域网)组成。分布式系统是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。分布式系统可以应用在在不同的平台上如:Pc、工作站、局域网和广域网上等。
简单来讲:
1分布式:不同的多台服务器上面部署不同的服务模块(工程),他们之间通过Rpc/Rmi之间通信和调用,对外提供服务和组内协作。
2集群:不同的多台服务器上面部署相同的服务模块,通过分布式调度软件进行统一的调度,对外提供服务和访问。
五、redis的三个特点
六、安装
参考对应的安装手册
七、HelloWorld
①拷贝一份redis.conf文件,修改拷贝的redis.conf文件,修改属性 daemonize no --->daemonize yes
daemonize 设置yes或者no区别
通俗的解释:设置成no时,redis的服务端是在前台运行,设置成yes,redis的服务端是在后台运行。
- daemonize:yes:redis采用的是单进程多线程的模式。当redis.conf中选项daemonize设置成yes时,代表开启守护进程模式。在该模式下,redis会在后台运行,并将进程pid号写入至redis.conf选项pidfile设置的文件中,此时redis将一直运行,除非手动kill该进程。
- daemonize:no: 当daemonize选项设置成no时,当前界面将进入redis的命令行界面,exit强制退出或者关闭连接工具(putty,xshell等)都会导致redis进程退出。
②查看是否有启动的redis进程,ps -ef|grep redis
③启动redis,启动服务器 redis-server /myredis/redis.conf
启动客户端 redis-cli -p 6379
redis-cli -h {host} -p {port}方式连接,然后所有的操作都是在交互的方式实现,不需要再执行redis-cli了。
④ping --->pongpin
⑤set k1 hello
⑥get k1
⑦关闭redis
八、redis的杂项知识
①测试redis的性能
redis是分布式的内存数据库
redis-benchmark命令
②单进程
多路 I/O 复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
使用 epoll 或 libevent 等因为异步非阻塞 IO 编程只能这么做。与之对应的是同步阻塞 IO 编程,使用多进程或多线程实现多条连接的处 理,比如 apache。一般情况下,异步非阻塞 IO 模型性能是远高于同步阻塞 IO 模型的,可以参考 nginx 与 apache 性能的对比。
总结:rendis快的原因
①基于内存存储
②多路复用IO
③设置数据数量(默认是是16个库)
redis下,数据库是由一个整数索引标识,而不是由一个数据库名称。默认情况下,一个客户端连接到数据库0。redis配置文件中下面的参数来控制数据库总数:
④Select命令切换数据库
切换库,用下标切换,例如切换到1号库,selet 0
⑤Dbsize查看当前数据库的key的数量
⑥Flushdb:清空当前库
⑦Flushall;通杀全部库(清空所有库)
九、Redis的五大数据类型简介
①String(字符串)
string是redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个redis中字符串value最多可以是512M
②Hash(哈希)
Redis hash 是一个键值对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
类似Java里面的Map<String,Object>
③List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。
它的底层实际是个链表
④Set(集合)
Redis的Set是string类型的无序集合。它是通过HashTable实现实现的
⑤zset(sorted set:有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。
redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
⑥redis常见数据类型操作命令参考文档
十、Redis的五大数据类型详解
①Redis 键(key)
常用
(1)keys *
(2)exists key的名字,判断某个key是否存在
(3)move key db --->当前库就没有了,被移除了 剪切
把k3从1号库移到2号库
(4)expire key 秒钟:为给定的key设置过期时间
设置k2,10秒过期
(5)ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
(6)type key 查看你的key是什么类型
②redis字符串(String)
(1)常用
(2)append
(3)strlen
(4)incr
(5)incrby
(6)decrby
(7)getrange/setrange
getrange:获取指定区间范围内的值,类似between......and的关系
从零到负一表示全部
setrange设置指定区间范围内的值,格式是setrange key值 具体值
(8)setex(set with expire)键秒值/setnx(set if not exist)
setex:设置带过期时间的key,动态设置。
setex 键 秒值 真实值
setnx:只有在 key 不存在时设置 key 的值。
(9)mset/mget/msetnx
mset:同时设置一个或多个 key-value 对。
mget:获取所有(一个或多个)给定 key 的值。
msetnx:同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
(10)getset(先get再set)
getset:将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
简单一句话,先get然后立即set
③Redis列表(List)
(1)常用
(2)lpush/rpush/lrange
* 从left往right删除2个值等于v1的元素,返回的值为实际删除的数量
* LREM list3 0 值,表示删除全部给定的值。零个就是全部值
(3)lpop/rpop
(4)lindex,按照索引下标获得元素(从上到下)
(5)llen
(6)lrem key 删N个value
(7)ltrim key 开始index 结束index,截取指定范围的值后再赋值给key
ltrim:截取指定索引区间的元素,格式是ltrim list的key 起始索引 结束索引
(8)rpoplpush 源列表 目的列表
lset key index value
(9)lrange
(10)linsert key before/after 值1 值2
在list某个已有值的前后再添加具体值
(11)性能总结
它是一个字符串链表,left、right都可以插入添加;如果键不存在,创建新的链表;如果键已存在,新增内容;如果值全移除,对应的键也就消失了。
链表的操作无论是头和尾效率都极高,但假如是对中间元素进行操作,效率就很惨淡了。Redis集合(Set)
④Redis集合(Set)
(1)常用
(2)sadd/smembers/sismember
(3)scard,获取集合里面的元素个数
(4) srem key value 删除集合中元素
(5) srandmember key 某个整数(随机出几个数)
*从set集合里面随机取出2个
*如果超过最大数量就全部取出
*如果写的值是负数,比如-3,表示需要取出3个,但是可能会有重复值
(6)spop key 随机出栈
(7)smove key1 key2 在key1里某个值 作用是将key1里的某个值赋给key2
(8) 数学集合类
差集:sdiff
在第一个set里面而不在后面任何一个set里面的项
交集:sinter
并集:sunion
⑤Redis哈希(Hash)
(1)常用
KV模式不变,但V是一个键值对
(2)hset/hget/hmset/hmget/hgetall/hdel
(3)hlen
得到hash的长度
(4)hexists key 在key里面的某个值的key是否存在
id是存在key值,emil是不存在的key值
(5)hkeys/hvals
(6)hincrby/hincrbyfloat
(7)hsetnx
不存在赋值,存在了无效
⑥Redis有序集合Zset(sorted set)
(1)实现
在set基础上,加一个score值。之前set是k1 v1 v2 v3,现在zset是k1 score1 v1 score2 v2
(2)常用
(3) zadd/zrange
(4) zrangebyscore key 开始score 结束score
(5) zrem key 某score下对应的value值,作用是删除元素
删除元素,格式是zrem zset的key 项的值,项的值可以是多个
zrem key score某个对应值,可以是多个值
(6) zcard/zcount key score区间/zrank key values值,作用是获得下标值/zscore key 对应值,获得分数
zcard :获取集合中元素个数
zcount :获取分数区间内元素个数,zcount key 开始分数区间 结束分数区间
zrank: 获取value在zset中的下标位置
zscore:按照值获得对应的分数
(7)zrevrank key values值,作用是逆序获得下标值
正序、逆序获得下标索引值
(8) zrevrange
(9) zrevrangebyscore key 结束score 开始score
十一、解析配置文件redis.conf
①它在哪
(1)地址
(2)为什么我将它拷贝出来单独执行
因为无法保证一次就能修改正确,所以要进行备份
②Units单位
1 配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit
2 对大小写不敏感
③INCLUDES包含
和我们的Struts2配置文件类似,可以通过includes包含,redis.conf可以作为总闸,包含其他
④GENERAL通用
(1)Pidfile
redis进程管道id文件位置
(2)Tcp-backlog
设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列。
在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值,所以需要确认增大somaxconn和tcp_max_syn_backlog两个值来达到想要的效果
在redis2.8版本中有一个tcp-backlog配置, 说明如下:
# TCP listen() backlog.
#
# In high requests-per-second environments you need an high backlog in order
# to avoid slow clients connections issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
# in order to get the desired effect.
tcp-backlog 100然后运行ss命令显示:
1
2
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 100 *:6379 *:*
我们看到Send-Q的值为100, 即是我们配置的tcp-backlog值. 为了搞清楚这个值的意思, 了解了下tcp的三次握手进行中的一些queue的知识. 参考下图我们可以看到在server接收到sny的时候会进入到一个syn queue队列, 当server端最终收到ack时转换到accept queue队列. 上面终端显示在listen状态下的连接, 其Send-Q就是这个accept queue队列的最大值. 只有server端执行了accept后才会从这个队列中移除这个连接. 这个值的大小是受somaxconn影响的, 因为是取的它们两者的最小值, 所以如果要调大的话必需修改内核的somaxconn值.
(3)绑定的网络
翻看网上的文章,此处多翻译为“指定redis只接收来自于该IP地址的请求,如果不进行设置,那么将处理所有请求,在生产环境中最好设置该项”。这种解释会totally搞糊涂初学者,甚至是错误的。该处的英文原文为
# If you want you can bind a single interface, if the bind option is not
# specified all the interfaces will listen for incoming connections.
# bind 127.0.0.1该处说明bind的是interface,也就是说是网络接口。服务器可以有一个网络接口(通俗的说网卡),或者多个。打个比方说机器上有两个网卡,分别为192.168.205.5 和192.168.205.6,如果bind 192.168.205.5,那么只有该网卡地址接受外部请求,如果不绑定,则两个网卡口都接受请求。
OK,不知道讲清楚没有,在举一个例子。在我上面的实验过程中,我是将bind项注释掉了,实际上我还有一种解决方案。由于我redis服务器的地址是 192.168.1.4 。如果我不注释bind项,还有什么办法呢?我可以做如下配置:
# bind 192.168.1.4
这里很多人会误以为绑定的ip应该是请求来源的ip。其实不然,这里应该绑定的是你redis服务器本身接受请求的ip
(4)过期时间Timeout
(5)Tcp-keepalive
0代表一直连接
单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
TCP连接保活策略,可以通过tcp-keepalive配置项来进行设置,单位为秒,假如设置为60秒,则server端会每60秒向连接空闲的客户端发起一次ACK请求,以检查客户端是否已经挂掉,对于无响应的客户端则会关闭其连接。所以关闭一个连接最长需要120秒的时间。如果设置为0,则不会进行保活检测。
(6)Loglevel服务器的日志级别
(7)Logfile
日志的名字
(8)Syslog-enabled
是否把日志输出到syslog中
(9)Syslog-ident
指定syslog里的日志标志
(10)Syslog-facility
指定syslog设备,值可以是USER或LOCAL0-LOCAL7
(11)Databases
redis默认的数据库数量
⑤SECURITY安全
访问密码的查看、设置和取消
⑥LIMITS限制
(1)Maxclients
设置redis同时可以与多少个客户端进行连接。默认情况下为10000个客户端。当你
无法设置进程文件句柄限制时,redis会设置为当前的文件句柄限制值减去32,因为redis会为自
身内部处理逻辑留一些句柄出来。如果达到了此限制,redis则会拒绝新的连接请求,并且向这
些连接请求方发出“max number of clients reached”以作回应。
(2)Maxmemory
设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,
那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素
(3)Maxmemory-policy
volatile-lru:使用LRU算法(LRU算法)移除key,只对设置了过期时间的键
allkeys-lru:使用LRU算法移除key
volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
allkeys-random:移除随机的key
volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
noeviction:不进行移除。针对写操作,只是返回错误信息
(4)Maxmemory-samples
设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,
redis默认会检查这么多个key并选择其中LRU的那个
⑦常见配置redis.conf介绍
参数说明
redis.conf 配置项说明如下:
1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no
2. 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
pidfile /var/run/redis.pid
3. 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
port 6379
4. 绑定的主机地址
bind 127.0.0.1
5.当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
timeout 300
6. 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
loglevel verbose
7. 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
logfile stdout
8. 设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id
databases 16
9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
save <seconds> <changes>
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
10. 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes
11. 指定本地数据库文件名,默认值为dump.rdb
dbfilename dump.rdb
12. 指定本地数据库存放目录
dir ./
13. 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
slaveof <masterip> <masterport>
14. 当master服务设置了密码保护时,slav服务连接master的密码
masterauth <master-password>
15. 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH <password>命令提供密码,默认关闭
requirepass foobared
16. 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
maxclients 128
17. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory <bytes>
18. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no
19. 指定更新日志文件名,默认为appendonly.aof
appendfilename appendonly.aof
20. 指定更新日志条件,共有3个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快)
always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec
选择策略:
如果用户使用appendfsync always选项的话,那么每个Redis写命令都会被写人硬盘,
从而将发生系统崩溃时出现的数据丢失减到最少。不过遗憾的是,因为这种同步策略需要对硬盘
进行大量写人,所以Redis处理命令的速度会受到硬盘性能的限制:转盘式硬盘(spinning disk )
在这种同步频率下每秒只能处理大约200个写命令,而固态硬盘(solid-state drive, SSD)每秒
大概也只能处理几万个写命令。
警告:固态硬盘和appendfsync always使用固态硬盘的用户请谨慎使用appendfsync
always选项,因为这个选项让Redis每次只写入一个命令,而不是像其他appendfsync选项那
样一次写入多个命令,这种不断地写入少量数据的做法有可能会引发严重的写入放大(write
amplification)问题,在某些情况下甚至会将固态硬盘的寿命从原来的几年降低为几个月。
为了兼顾数据安全和写人性能,用户可以考虑使用appendfsync everysec选项,让Redis
以每秒一次的频率对AOF文件进行同步。Redis每秒同步一次AOF文件时的性能和不使用任何
持久化特性时的性能相差无几,而通过每秒同步一次AOF文件,Redis可以保证,即使出现系统
崩溃,用户也最多只会丢失一秒之内产生的数据。当硬盘忙于执行写人操作的时候,Redis还会
优雅地放慢自己的速度以便适应硬盘的最大写人速度。
最后,如果用户使用appendfsync n。选项,那么Redis将不对AOF文件执行任何显式
的同步操作,而是由操作系统来决定应该在何时对AOF文件进行同步。这个选项在一般情况下
不会对Redis的性能带来影响,但系统崩溃将导致使用这种选项的Redis服务器丢失不定数量的
数据。另外,如果用户的硬盘处理写人操作的速度不够快的话,那么当缓冲区被等待写人硬盘的
数据填满时,Redis的写人操作将被阻塞,并导致Redis处理命令请求的速度变慢。因为这个原因,
一般来说并不推荐使用appendfsync n。选项,在这里介绍它只是为了完整列举appendfsync
选项可用的3个值。
21. 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
vm-enabled no
22. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
vm-swap-file /tmp/redis.swap
23. 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
vm-max-memory 0
24. Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
vm-page-size 32
25. 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
vm-pages 134217728
26. 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
vm-max-threads 4
27. 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
glueoutputbuf yes
28. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-zipmap-entries 64
hash-max-zipmap-value 512
29. 指定是否**重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
activerehashing yes
30. 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
十二、Redis的持久化
RDB(Redis DataBase)
①是什么
快照原理:
1.redis调用fork,现在有了子进程和父进程。
2. 父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的写时复制机制(copy on write)父子进程会共享相同的物理内存,当父进程处理写请求时os会为父进程要修改的数据内存创建副本物理内存,而不是写共享的物理内存。所以子进程的地址空间内的数据是fork时刻整个数据库的一个快照。
3.当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。
client 也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有 client的请求,这种方式会阻塞所有client请求。所以不推荐使用。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不 是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。
另外由于快照方式是在一定间隔时间做一次的,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。如果应用要求不能丢失任何修改的话,可以采用aof持久化方式。
写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
②Fork
③Rdb保存文件类型
Rdb 保存的是dump.rdb文件
④配置位置
⑤如何触发RDB快照
创建快照的办法有以下几种。
(1) 客户端可以通过向Redis发送BGSAVE命令来创建一个快照。对于支持BGSAVE命令的
平台来说(基本上所有平台都支持,除了Windows平台),Redis会调用fo护来创建一
个子进程,然后子进程负责将快照写人硬盘,而父进程则继续处理命令请求。
(2)客户端还可以通过向Redis发送SAVE命令来创建一个快照,接到SAVE命令的Redi:服
务器在快照创建完毕之前将不再响应任何其他命令。SAVE命令并不常用,我们通常
只会在没有足够内存去执行BGSAVE命令的情况下,又或者即使等待持久化操作执
行完毕也无所谓的情况下,才会使用这个命令。
(3)如果用户设置Tsave配置选项,比如save 60 10000,那么从Redis最近一次创建
快照之后开始算起,当“60秒之内有10000次写人”这个条件被满足时,Redis就会自动
触发BGSAVE命令。如果用户设置了多个save配置选项,那么当任意一个save配置
选项所设置的条件被满足时,Redis就会触发一次BGSAVE命令。
(4)当Redis通过SHUTDOWN命令接收到关闭服务器的请求时,或者接收到标准TERM信号
时,会执行一个SAVE命令,阻塞所有客户端,不再执行客户端发送的任何命令,并在
SAVE命令执行完毕之后关闭服务器。
(5)当一个Redis服务器连接另一个Redis服务器,并向对方发送SYNC命令来开始一次复
制操作的时候,如果主服务器目前没有在执行BGSAVE操作,或者主服务器并非刚刚执
行完BGSAVE操作,那么主服务器就会执行BGSAVE命令。更多有关复制的信息
(1)BGSAVE和SAVE的优缺点(针对于大数据量)
BGSAVE的缺点:
如果Redis的内存占用量达到数十个GB,并且剩余的空闲内存并不多,或者Redis运
行在虚拟机(virtual machine)上面,那么执行BGSAVE可能会导致系统长时间地停顿,也
可能引发系统大量地使用虚拟内存(virtual memory),从而导致Redis的性能降低至无法使
用的程度。
执行BGSAVE而导致的停顿时间有多长取决于Redis所在的系统:对于真实的硬件、VMWare
虚拟机或者KVM虚拟机来说,Redis进程每占用一个GB的内存,创建该进程的子进程所需的
时间就要增加10-20毫秒;而对于Xen虚拟机来说,根据配置的不同,Redis进程每占用一个
GB的内存,创建该进程的子进程所需的时间就要增加200^-300毫秒。因此,如果我们的Redis
进程占用了20 GB的内存,那么在标准硬件上运行BGSAVE所创建的子进程将导致Redi:停顿
200^400毫秒;
解决办法,通过手动发送BGSAVE命令来控制停顿出现的时间。
SAVE的优点:
另一方面,虽然SAVE会一直阻塞Redis
直到快照生成完毕,但是因为它不需要创建子进程,所以就不会像BGSAVE一样因为创建子进程
而导致Redi:停顿;并且因为没有子进程在争抢资源,所以SAVE创建快照的速度会比BGSAVE
创建快照的速度要来得更快一些。
(2)配置文件中默认的快照配置
(3)冷拷贝后重新使用
⑥如何恢复
⑦优势
⑧劣势
⑨如何停止
⑩小总结
AOF(Append Only File)
①是什么
②文件保存类型
Aof保存的是appendonly.aof文件
③配置位置
④AOF启动/修复/恢复ge
修复:
⑤Rewrite
⑥优势
⑦劣势
⑧小总结
大总结
性能建议
(1)性能建议1
因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。
如果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值。
如果不Enable AOF ,仅靠Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构
(2)性能建议2
重写/压缩AOF文件
为了解决AOF文件体积不鳅曾大的问题,用户可以向Redis发送BGREWR工TEAOF命令,这
个命令会通过移除AOF文件中的冗余命令来重写(rewrite) AOF文件,使AOF文件的体积变得
尽可能地小。BGREWR工TEAOF的工作原理和BGSAVE创建决照的工作原理非常相似:Redis会创
建一个子进程,然后由子进程负责对AOF文件进行重写。因为AOF文件重写也需要用到子进程,
所以快照持久化因为创建子进程而导致的性能问题和内存占用问题,在AOF持久化中也同样存
在。更糟糕的是,如果不加以控制的话,AOF文件的体积可能会比决照文件的体积大好几倍,
在进行AOF重写并删除旧AOF文件的时候,删除一个体积达到数十GB大的旧AOF文件可能
会导致操作系统挂起(hang)数秒。
跟快照持久化可以通过设置save选项来自动执行BGSAVE一样,AOF持久化也可以通过
设置auto-aof-rewr主七e-percen七age选项和au七。-aof-rewrite-min-size选项来自动衫甫
BGREWR工TEAOFo胃到、例子,假设用户对Redis设置了酉己誊选项auto-aof-rewrite-percentage
100和auto-aof-rewrite-min-size 64mb,并且启用了AOF持久化,那么当AOF文件
的体积大于64 MB,并且AOF文件的体积比上一次重写之后的体积大了至少一倍(100%)的时
候,Redis将执行BGREWR工TEAOF命令。如果AOF重写执行得过于频繁的话,用户可以考虑将
auto-aof-rewrite-percentage选项的值设置为100以上,这种做法可以让Redis在AOF
文件的体积变得更大之后才执行重写操作,不过也会让Redis在启动时还原数据集所需的时间变
得更长。
无论是使用AOF持久化还是快照持久化,将数据持久化到硬盘上都是非常有必要的,但除
了进行持久化之外,用户还必须对持久化所得的文件进行备份(最好是备份到多个不同的地方),
这样才能尽量避免数据丢失事故发生。如果条件允许的话,最好能将快照文件和最新重写的AOF
文件备份到不同的服务器上面。
(3)性能建议3
使用事务的其中一个好处就是底层的客户端会通过使用流水线来提高事务执行时的性能。使用非事务型流水线(non-transactional pipeline)同样可以获得相似的性能提升,并且可以让用户同时执行多个不同的命令。
MULTI和EXEC也会消耗资源,并且可能会导致其他重要的命令被延迟执行。但也可以在不使用MULTI和EXEC的情况下,获得流水线带来的所有好处。
pipe = conn.pipeline()
在执行pipeline()时传入True作为参数,或者不传入任何参数,那么客户端将使用MULTI和EXEC包裹起用户要执行的所有命令。如果传入False为参数,那么客户端同样会像执行事务那样收集用户要执行的所有命令,只是不再使用MULTI和EXEC包裹这些命令。如果用户需要向Redis发送多个命令,并且对于这些命令来说,一个命令的执行结果并不会影响另一个命令的输入,而且这些命令也不需要以事务的方式来执行的话,那么我们可以通过向pipeline()方法传入False来进一步提升Redis的整体性能。
十三、Redis的事务
①是什么
②能干嘛
因为延迟执行事务有助于提升性能:
延迟执行事务有助于提升性能因为Redis在执行事务的过程中,会延迟执行已入队的命令直到客户端发送EXEC命令为止.因此,包括本书使用的Python客户端在内的很多Redis客户端都会等到
事务包含的所有命令都出现了之后,才一次性地将MULT工命令、要在事务中执行的一系列命令,
以及EXEC命令全部发送给Redis,然后等待直到接收到所有命令的回复为止。这种“一次性发送
多个命令,然后等待所有回复出现”的做法通常被称为流水线(pipelining ),它可以通过减少客户
端与Redis服务器之间的网络通信次数来提升Redis在执行多个命令时的性能.
③怎么玩
(1)常用命令
什么是DISCARD:
UNWATCH命令可以在WATCH命令执行之后、MULT工命令执行之前对连接进
行重置(reset );同样地,D工SCARD命令也可以在MULT工命令执行之后、EXEC命令执行之前对连接
进行重置.这也就是说,用户在使用WATCH监视一个或多个键,接着使用MULT工开始一个新的事务,
并将多个命令入队到事务队列之后,仍然可以通过发送D工SCARD命令来取消WATCH命令并清空所
有已入队命令.本章展示的例子都没有用到D工SCARD,主要原因在于我们已经清楚地知道自已是否
想要执行哪LT工/EXEC或者UNWATCH,所以没有必要在这些例子里面使用D工SCARD。
(2)Case1:正常执行
(3)Case2:放弃事务
(4)Case3:全体连坐
(5)Case4:冤头债主
(6)Case5:watch监控
有加塞篡改:监控了key,如果key被修改了,后面一个事务的执行失效
unwatch:
④3阶段
⑤3特性
十四、Redis的复制(Master/Slave)
①是什么
②能干嘛
③怎么玩
具体操作步骤:
1、拷贝多个redis.conf文件
2、开启daemonize yes、Pid文件名字、指定端口
3、Log文件名字
4.dump.rdb的名字
5.查看机器是主还是从的信息info replication
6、设置主从
④复制原理
⑤哨兵模式(sentinel)
详细步骤:
1、调整结构,6379带着80、81
2、自定义的/myredis目录下新建sentinel.conf文件,名字绝不能错
3、配置哨兵
4、启动哨兵
5、主机挂了
6、投票新选
7、重新主从继续开工,info replication查查看
8、如果宕掉的主机重新回来,和原来的从机就没有关系了,成为自己 的主机
9、一组sentinel能同时监控多个Master
⑥复制的缺点
十五、Redis的Java客户端Jedis
常用操作
①测试连通性
public class Demo01 {
public static void main(String[] args) {
//连接本地的 Redis 服务
Jedis jedis = new Jedis("127.0.0.1",6379);
//查看服务是否运行,打出pong表示OK
System.out.println("connection is OK==========>: "+jedis.ping());
}
}
②5+1种数据类型
package com.atguigu.redis.test;
import java.util.*;
import redis.clients.jedis.Jedis;
public class Test02
{
public static void main(String[] args)
{
Jedis jedis = new Jedis("127.0.0.1",6379);
//key
Set<String> keys = jedis.keys("*");
for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
System.out.println(key);
}
System.out.println("jedis.exists====>"+jedis.exists("k2"));
System.out.println(jedis.ttl("k1"));
//String
//jedis.append("k1","myreids");
System.out.println(jedis.get("k1"));
jedis.set("k4","k4_redis");
System.out.println("----------------------------------------");
jedis.mset("str1","v1","str2","v2","str3","v3");
System.out.println(jedis.mget("str1","str2","str3"));
//list
System.out.println("----------------------------------------");
//jedis.lpush("mylist","v1","v2","v3","v4","v5");
List<String> list = jedis.lrange("mylist",0,-1);
for (String element : list) {
System.out.println(element);
}
//set
jedis.sadd("orders","jd001");
jedis.sadd("orders","jd002");
jedis.sadd("orders","jd003");
Set<String> set1 = jedis.smembers("orders");
for (Iterator iterator = set1.iterator(); iterator.hasNext();) {
String string = (String) iterator.next();
System.out.println(string);
}
jedis.srem("orders","jd002");
System.out.println(jedis.smembers("orders").size());
//hash
jedis.hset("hash1","userName","lisi");
System.out.println(jedis.hget("hash1","userName"));
Map<String,String> map = new HashMap<String,String>();
map.put("telphone","13811814763");
map.put("address","atguigu");
map.put("email","[email protected]");
jedis.hmset("hash2",map);
List<String> result = jedis.hmget("hash2", "telphone","email");
for (String element : result) {
System.out.println(element);
}
//zset
jedis.zadd("zset01",60d,"v1");
jedis.zadd("zset01",70d,"v2");
jedis.zadd("zset01",80d,"v3");
jedis.zadd("zset01",90d,"v4");
Set<String> s1 = jedis.zrange("zset01",0,-1);
for (Iterator iterator = s1.iterator(); iterator.hasNext();) {
String string = (String) iterator.next();
System.out.println(string);
}
}
}
③日常事务
package com.atguigu.redis.test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Response;
import redis.clients.jedis.Transaction;
public class Test03
{
public static void main(String[] args)
{
Jedis jedis = new Jedis("127.0.0.1",6379);
//监控key,如果该动了事务就被放弃
/*3
jedis.watch("serialNum");
jedis.set("serialNum","s#####################");
jedis.unwatch();*/
Transaction transaction = jedis.multi();//被当作一个命令进行执行
Response<String> response = transaction.get("serialNum");
transaction.set("serialNum","s002");
response = transaction.get("serialNum");
transaction.lpush("list3","a");
transaction.lpush("list3","b");
transaction.lpush("list3","c");
transaction.exec();
//2 transaction.discard();
System.out.println("serialNum***********"+response.get());
}
}
④加锁事务
public class TestTransaction {
public boolean transMethod() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
int balance;// 可用余额
int debt;// 欠额
int amtToSubtract = 10;// 实刷额度
jedis.watch("balance");
//jedis.set("balance","5");//此句不该出现,讲课方便。模拟其他程序已经修改了该条目
balance = Integer.parseInt(jedis.get("balance"));
if (balance < amtToSubtract) {
jedis.unwatch();
System.out.println("modify");
return false;
} else {
System.out.println("***********transaction");
Transaction transaction = jedis.multi();
transaction.decrBy("balance", amtToSubtract);
transaction.incrBy("debt", amtToSubtract);
transaction.exec();
balance = Integer.parseInt(jedis.get("balance"));
debt = Integer.parseInt(jedis.get("debt"));
System.out.println("*******" + balance);
System.out.println("*******" + debt);
return true;
}
}
/**
* 通俗点讲,watch命令就是标记一个键,如果标记了一个键, 在提交事务前如果该键被别人修改过,那事务就会失败,这种情况通常可以在程序中
* 重新再尝试一次。
* 首先标记了键balance,然后检查余额是否足够,不足就取消标记,并不做扣减; 足够的话,就启动事务进行更新操作,
* 如果在此期间键balance被其它人修改, 那在提交事务(执行exec)时就会报错, 程序中通常可以捕获这类错误再重新执行一次,直到成功。
*/
public static void main(String[] args) {
TestTransaction test = new TestTransaction();
boolean retValue = test.transMethod();
System.out.println("main retValue-------: " + retValue);
}
}
⑤主从复制
public static void main(String[] args) throws InterruptedException
{
Jedis jedis_M = new Jedis("127.0.0.1",6379);
Jedis jedis_S = new Jedis("127.0.0.1",6380);
jedis_S.slaveof("127.0.0.1",6379);
jedis_M.set("k6","v6");
Thread.sleep(500);
System.out.println(jedis_S.get("k6"));
}
JedisPool
JedisPoolUtil
package com.atguigu.redis.test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;//被volatile修饰的变量不会被本地线程缓存,对该变量的读写都是直接操作共享内存。
private JedisPoolUtil() {}
public static JedisPool getJedisPoolInstance()
{
if(null == jedisPool)
{
synchronized (JedisPoolUtil.class)
{
if(null == jedisPool)
{
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxActive(1000);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWait(100*1000);
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig,"127.0.0.1");
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool,Jedis jedis)
{
if(null != jedis)
{
jedisPool.returnResourceObject(jedis);
}
}
}
Demo5
package com.atguigu.redis.test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class Test01 {
public static void main(String[] args) {
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = null;
try
{
jedis = jedisPool.getResource();
jedis.set("k18","v183");
} catch (Exception e) {
e.printStackTrace();
}finally{
JedisPoolUtil.release(jedisPool, jedis);
}
}
}
配置总结all
JedisPool的配置参数大部分是由JedisPoolConfig的对应项来赋值的。
maxActive:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted。
maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
whenExhaustedAction:表示当pool中的jedis实例都被allocated完时,pool要采取的操作;默认有三种。
WHEN_EXHAUSTED_FAIL --> 表示无jedis实例时,直接抛出NoSuchElementException;
WHEN_EXHAUSTED_BLOCK --> 则表示阻塞住,或者达到maxWait时抛出JedisConnectionException;
WHEN_EXHAUSTED_GROW --> 则表示新建一个jedis实例,也就说设置的maxActive无用;
maxWait:表示当borrow一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛JedisConnectionException;
testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
testOnReturn:return 一个jedis实例给pool时,是否检查连接可用性(ping());
testWhileIdle:如果为true,表示有一个idle object evitor线程对idle object进行扫描,如果validate失败,此object会被从pool中drop掉;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;
timeBetweenEvictionRunsMillis:表示idle object evitor两次扫描之间要sleep的毫秒数;
numTestsPerEvictionRun:表示idle object evitor每次扫描的最多的对象数;
minEvictableIdleTimeMillis:表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;
softMinEvictableIdleTimeMillis:在minEvictableIdleTimeMillis基础上,加入了至少minIdle个对象已经在pool里面了。如果为-1,evicted不会根据idle time驱逐任何对象。如果minEvictableIdleTimeMillis>0,则此项设置无意义,且只有在timeBetweenEvictionRunsMillis大于0时才有意义;
lifo:borrowObject返回对象时,是采用DEFAULT_LIFO(last in first out,即类似cache的最频繁使用队列),如果为False,则表示FIFO队列;
==================================================================================================================
其中JedisPoolConfig对一些参数的默认设置如下:
testWhileIdle=true
minEvictableIdleTimeMills=60000
timeBetweenEvictionRunsMillis=30000
numTestsPerEvictionRun=-1
十六、性能优化
②短结构
Redis为列表、集合、散列和有序集合提供一组配置选项,这些选项可以让Redis以更节约空间
的方式存储长度较短的结构。在列表、散列和有序集合的长度较短或者体积较小的时候,Redis可以选择使用一种名
为压缩列表(ziplist)的紧凑存储方式来存储这些结构。压缩列表是列表、散列和有序集合
这3种不同类型的对象的一种非结构化(unstructured)表示:与Redis在通常情况下使用
双链表表示列表、使用散列表表示散列、使用散列表加上跳跃表(skiplist )表示有序集合
的做法不同,压缩列表会以序列化的方式存储数据,这些序列化数据每次被读取的时候都
要进行解码,每次被写人的时候也要进行局部的重新编码,并且可能需要对内存里面的数
据进行移动。
(1)压缩列表表示(列表和散列,有序集合)
为了了解压缩列表比其他数据结构更为节约内存的原因,我们需要对使用压缩列表的几种结
构当中,最为简单的列表结构进行观察。在典型的双向链表( doubly linked list)里面,链表包含
的每个值都会由一个节点(node)表示,每个节点都会带有指向链表中前一个节点和后一个节点
的指针,以及一个指向节点包含的字符串值的指针。每个节点包含的字符串值都会分为3个部分
进行存储:第一部分存储的是字符串的长度,第二部分存储的是字符串值中剩余可用的字节数
量,而最后一部分存储的则是以空字符结尾的字符串本身。图9-1展示了一个比较长的双向链表
的其中一部分,通过这个图可以看到”one",”七wo",”七en”这3个字符串是如何存储在双向链
表里面的。
为了让图片保持简洁,图9-1省略了链表的某些细节。图中展示的3个3字符长的字符串,
每个都需要空间来存储3个指针、2个整数(一个是字符串的长度,另一个是字符串值的剩余可
用空间)、字符串本身以及一个额外的字节。在32位平台上,每存储一个这样的3字节长的字符
串,就需要付出21字节的额外开销(overhead),而这还只是保守的估计值,实际的额外开销还
会更多一些。
另一方面,压缩列表是由节点组成的序列(sequence),每个节点都由两个长度值和一个字符
串组成。第一个长度值记录的是前一个节点的长度,这个长度值将被用于对压缩列表进行从后向
前的遍历,第二个长度值记录了当前节点的长度,而位于节点最后的则是被存储的字符串值。尽
管压缩列表节点的长度值在实际中还有一些其他的含义,但是对于我们例子中的”one" ,"two"、
”ten”这3个3字节长的字符串来说,它们每个的长度都可以用1字节来存储,所以在使用压缩
列表存储这3个字符串的时候,每个节点只会有2字节的额外开销。通过避免存储额外的指针和
元数据,使用压缩列表可以将存储示例中的3个字符串所需的额外开销从原来的21字节降低至
2字节。
使用压缩列表编码
为了确保压缩列表只会在有需要降低内存占用的情况下使用,Redis引人了代码清
单9-1展示的配置选项,这些选项决定了列表、散列和有序集合会在什么情况下使用压缩列
表表示。
当列表的元素长度都小于64字节并且列表元素数量小于512时,使用压缩列表,反之使用linkedlist
当散列的元素的键和值都小于64字节并且键值对的数量小于512时,使用压缩列表,反之使用hashtable
当有序集合的元素都小于64字节并且元素数量小于128个的时候,使用压缩列表,反之使用skiplist https://www.cnblogs.com/a8457013/p/8251967.html
列表、散列和有序集合的基本配置选项都很相似,它们都由一max-ziplist-entries选
项和一max-ziplist-value选项组成,并且这3组选项的语义也基本相同:entries选项说
明列表、散列和有序集合在被编码为压缩列表的情况下,允许包含的最大元素数量;而value
选项则说明了压缩列表每个节点的最大体积是多少个字节。当这些选项设置的限制条件中的任意
一个被突破的时候,Redis就会将相应的列表、散列或是有序集合从压缩列表编码转换为其他结
构,而内存占用也会因此而增加。
如果用户是以默认配置方式安装Redis 2.6的话,那么Redis提供的默认配置将与代码清
单9-1中展示的配置相同。代码清单9一展示了如何通过添加元素和检查表示方式等手段,调试
一个压缩列表表示的列表对象。
(2)集合的整数集合编码
跟列表、散列和有序集合不同,集合并没有使用压缩列表表示,而是使用了另外一种具有不
同语义和限制的紧凑表示,接下来的一节就会对这种表示进行介绍。
9.1.2集合的整数集合编码
跟列表、散列和有序集合一样,体积较小的集合也有自己的紧凑表示:如果整数包含的所有
成员都可以被解释为十进制整数,而这些整数又处于平台的有符号整数范围之内,并且集合成员
的数量又足够少的话(具体的限制大小稍后就会说明),那么Redis就会以有序整数数组的方式
存储集合,这种存储方式又被称为整数集合(intset )o
以有序数组的方式存储集合不仅可以降低内存消耗,还可以提升所有标准集合操作的执行速
度。那么一个集合要符合什么条件才能被存储为整数集合呢?代码清单9-3展示了定义整数集合
最大元素数量的配置选项。
只要集合存储的整数数量没有超过配置设定的大小,Redis就会使用整数集合表示以减少数
据的体积。代码清单9-4展示了当整数集合包含的元素数量超过配置选项设定的限制时,集合发
生的一系列变化。
对一个压缩列表表示的对象的其中一部分进行读取或者
更新,可能会需要对整个压缩列表进行解码,甚至还需要对内存里面的数据进行移动,因此读写
一个长度较大的压缩列表可能会给性能带来负面的影响。使用整数集合编码的集合结构也有类似
的问题,不过整数集合的问题并非来源于编码和解码数据,而在于它在执行插人操作或者删除操
作时需要对数据进行移动。
(3)常压缩列表和大整数集合带来的性能问题
问题:当一个结构突破了用户为压缩列表或者整数集合设置的限制条件时,Redis就会自动将它转
换为更为典型的底层结构类型。这样做的主要原因在于,随着紧凑结构的体积变得越来越大,操
作这些结构的速度也会变得越来越慢。
解决方案:只要将压缩列表的长度限制在500^-2000个元素之内,并将每个元素的体积限制在128字节
或以下,那么压缩列表的性能就会处于合理范围之内。笔者的做法是将压缩列表的长度限制
在1024个元素之内,并且每个元素的体积不能超过64字节,对于大多数散列应用来说,这种配
置可以同时兼顾低内存占用和高性能这两方面优点。