赞
踩
Redis是互联网领域使用最广泛的KV数据库之一,因为其快速及丰富的数据结构常常成为缓存的首选方案。
节约空间是第一生产力,一切以减少RT为主要目标
我们知道Redis是内存数据库,所有的数据都存在内存里面。因为内存是稀缺资源,所以省内存就是省钱,在不影响性能的情况下提高内存使用率就是最好的优化方法。
另外,内存虽然访问速度足够快,但是当单个进程内存占用大到一定的程度,或者产生碎片的情况下,速度也还是会有明显的影响。内存过大还会导致Redis数据持久化和主从复制时间过长,影响业务指令执行。所以,减少内存使用除了省钱之外,其实更大的优势是系统能力的提升。除了节约空间外,我们选择Redis目的最终只有一个,就是“快”。合理的使用和配置会最大化速度的提升。
key的设计主要关注两点:可读、简短。
可读性:KEY一般需要包含业务、模块、id信息,用冒号隔开,比如业务名:表名:ID 这样的格式。
像spring session持久化的key就是 spring:session:${sessionID}
。分隔符用冒号主要是使用惯例,而且像Redis Manager这样的图形客户端会自动将这样的key按冒号分割显示成树状结构。当然惯例只是用来参考的,如果你的Redis里面只存了一种业务,那显然业务名这段就可以去掉。比如全是手机号相关业务,那就用直接用手机号做key,更极端一点用手机号的后10位做key就可以了,反正第一位都是1。
简短性:在能充分表达意思的基础上,尽量缩减长度。
因为和关系型数据库元数据的机制不一样,Redis的key是将完整的名字存在内存里的。而且key都是用string来保存,占有空间和key有几个字符成正比。所以 messages:publish:${id}
就明显不如msg:pub:${id}
。
value的选择主要关注以下几点:
拒绝BigKey,BigKey是指value占用空间很大的Key。虽然redis对于value的大小限制放的很宽(比如字符串的value最大可以512M)。但还是不建议太大,太大的value会造成单次查询I/O时间太长,阻塞其它的命令。string最好不要超过10k,集合类型最好不要超过5000个元素。
尽量存数值,value尽量选用数值类型,能存id就不要存name,因为id通常更短。另外,针对全是数字的value,无论是value是简单的string,还是集合的list和set,redis都会做相应的优化使其占用更少的空间。
选择合适的数据类型,不要一提起redis就是字符串,redis有丰富的数据结构可以选择,比如只是要求有序,不要求自动排序,那就选list,不要选sortset,更加节省空间。这个我们下一节详细讲。
考虑使用压缩算法,如果单个value的值很大,又无法拆分,可以考虑先压缩再存储,比如使用gzip。但是压缩要经过完整的测试,不要redis的速度快了,压缩和解压缩把应用服务器CPU耗光了那就得不偿失了。
为什么要单独拿出来讲,是因为数据结构实在太重要了。不夸张的讲,选择了合适的数据结构,redis的使用就成功了一半。
Redis支持的数据结构有string, list, set, sorted set, hash,HyperLogLog,Geo和5.0新加的Streams。HyperLogLog和Geo应用于特定场景,我们只说下其它几种。
String,用的最多的结构,如果value是一个字符串,那就用它来存;如果不是字符串,比如说一个java对象也可以先序列化再存,可以用java默认的序列化,也可以用json。使用string存储对象是不能取单个属性的,只能一次性取出来,再在应用代码里反序列化。String类型同时提供了bit操作指令,可以实现如bitMap的功能。
List,类似于java中的List,多个元素有序存储首选,使用list的命令可同时实现队列,堆栈的功能。
Set,类似于java中的Set,用于多个元素的无续存储,元素自动去重。小集合的交并差操作,可在redis中直接用set相关命令完成,当然集合太大的话耗时过长,不建议这么做。
SortedSet,自排序集合,每个元素有个额外的score字段用来排序。如果没有自排序需求,则没必要用SortedSort
,毕竟每个元素多存了一个属性,空间肯定要大一点。
Hash,一个key中可以存多个键值对,非常适合存储一个对象(或者可以理解成mysql中的一行)。可以非常方便的读取、修改对象的单个属性。当然,如果没有对单个属性的操作要求,转成json再存string更加省空间。
Streams,Redis 5.0新加的数据结构,支持完整的分布式消息队列功能。在此之前,使用redis做消息队列都是使用List,提供的功能较简单。而新的Streams支持如下的特性1) 阻塞和非阻塞模式的消息消费,这个和之前List的RPOP/BRPOP
指令类似;2) 支持消息体包含多个属性;3)支持系统自动生成和用户自定义的唯一消息id;4) 支持按id和时间段范围查询消息;5) 支持ConsumerGroup
,同一条消息可发送给多个group种的某一个consumer;6) 支持消息的ACK、重发以及删除。在主流的redis客户端功能完善后,新的使用redis做消息队列可选用该结构。
以上就是对redis暴露给用户的数据结构的使用推荐,下面我们再深入一步,看看这些数据结构redis在底层是怎么存储的,寻找更大的优化空间。
String有3种底层结构,REDIS_ENCODING_RAW
(未加工), REDIS_ENCIDING_INT
(存成数值), REDIS_ENCODING_EMBSTR
(存成header+数据的结构,简称sds)。当Redis发现value能转成数值时,会选用将value存成数值。所以就像前面说的,如果能存id,就不要存name,更节省空间。
List底层结构是一个sdlist
(双向链表)和ziplist
(压缩链表)组成的quicklist,压缩链表简单说就是用数组存链表,因为少了双向指针更节省空间,但是如果元素太多会影响插入速度。双向链表用来快速的根据下标找到元素。Redis提供了两个参数来决定什么时候将压缩链表转成双向链表,list-max-ziplist-entries
和list-max-ziplist-value
。entries选项说明列表在被编码为ziplist的情况下,允许包含的最大元素数量;而value选项则说明了ziplist每个节点的最大体积是多少个字节。当这两个选项设置的限制条件中的任意一个被突破,即变换结构。
Set底层结构有dict
和intset
,dict就是hash的key-value数据结构;当set中全是数字并且数量小于一个参数值时,会使用intset来存储,更加节省空间,这个参数是set-max-intset-entities
。所以还是第一个原则,能用数字就不要用字符串
Hash的底层结构dict
和ziplist
。dict实现时,hash中的key和value的都是用string来存的。ziplist
怎么存hashtable呢?其实很简单,就是存一个key,存一个value;再存一个key,再存一个value。查找的时候只要查奇数位就知道key存不存在。Redis同样提供了参数来控制什么时候切换数据结构,hash-max-ziplist-entries
和hash-max-ziplist-value
。
SortedSet的底层结构由skiplist
和ziplist
,由于SortedSet兼具Set的快速查找功能,所以额外使用了一个dict来加速查找。zset-max-ziplist-entries
和zset-max-ziplist-value
参数来控制数据结构的切换。
底层的数据结构我们讲完了,那么问题来了,到底参数应该设置成多少呢?每个系统的情况不同,没有固定的值。《Redis实战》的作者建议是,ziplist的长度限制在500~2000个元素之内,并将每个元素的体积限制在128字节或以下,那么压缩列表的性能就会处于合理范围之内。还是强调一句,仅供参考,多测试。
Redis除了提供丰富的数据结构之外,还针对每种数据结构提供了很多有用的命令。这里说的优先选择,一定是基于数据量和并发量的,数据量太大或者并发太高一定要先测试再决定是redis来做还是程序中来处理。下面是我总结的一些redis中很好用的命令,个人理解,可能有遗漏,详细的请参考官方文档。
MGET/MSET:一个命令做多个Key的GET或者SET操作,非常有用的命令,提速利器。
APPEND:往字符串上追加值,并且返回追加后的长度。
INCR/DECR:类似于程序中的i++/i--, 如果key的值是数值,做原子的加减动作。最重要的是比i++安全,不会有线程并发的问题。并且返回加减后的值,类似于Java atom包下的工具类的功能。
GETRANGE/SETRANGE:返回截取的子字符串
GETSET:赋新值并返回旧值,也是原子操作
HMGET/HMSET, 用在Hash上的MGET/MSET
HINCRBY,如果field是数值的话,为哈希表 key 中的域 field 的值加上一个值,正负都可以。返回命令执行后的值。
BLPOP/BRPOP,从左边或者右边获取第一个元素,如果列表为空则阻塞,可以用来实现简单的分布式队列和堆栈功能
LRANGE,返回子列表
SDIFF,返回多个集合的差集
SINTER,返回多个集合的交集
SUNION,返回多个集合的并集
SMOVE,将元素从一个集合移动到另外一个集合
ZRANK/ZREVRANK,返回元素的排名/倒数排名,按元素的分数
ZREVRANGE,返回区间内成员,成员的位置按 score 值递减(从大到小)来排列
ZUNIONSTORE,返回多个有序集合的并集,并存到结果集合中。在合并过程中可以设置某一个集合的权重,和集合元素的聚合方式
ZINTERSTORE,类似ZUNIONSTORE
,返回交集
SCAN命令及其相关的 SSCAN、 HSCAN和 ZSCAN命令都用于增量地迭代一个集合的元素,类似于数据库的游标,强烈建议在遍历keys或者集合类结构时使用scan:
SCAN 命令用于迭代当前Redis中的Key。
SSCAN 命令用于迭代集合键中的元素。
HSCAN 命令用于迭代哈希键中的键值对。
ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。 例子:
redis 127.0.0.1:6379> scan 176 MATCH *11* COUNT 1000 1) "0" 2) 1) "key:611" 2) "key:711" 3) "key:118" 4) "key:117" 5) "key:311" 6) "key:112"
每次返回都会带一个下次遍历的游标值,并且这个游标是不需要关闭的。在开始一个新的迭代时, 游标必须为 0 。
SORT是另外一个有用的命令,返回列表、集合、有序集合经过排序的元素。支持使用另外的key进行排序,支持使用Hash的field进行排序。如果数据量不大,可以使用redis排序,跟先取出数据然后在程序中排序比,大大减少和Redis的交互次数。 例子(更复杂的情况请参考官方文档):
//金额列表redis> LPUSH today_cost 30 1.5 10 8(integer) 4//排序redis> SORT today_cost1) "1.5"2) "8"3) "10"4) "30"//逆序排序redis 127.0.0.1:6379> SORT today_cost DESC1) "30"2) "10"3) "8"4) "1.5"
最后还是要强调一遍,一切命令的高效使用都是基于数据量、并发数和具体存的数据决定的。所以,数据量大的情况下,一定要测试,多测试。
禁用批量操作命令 Redis提供了一些操作批量数据的命令,生产环境最好禁用它们。同时,部分命令限制使用场景
禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令
hgetall、lrange、smembers、zrange、sinter要使用,需要明确N的值,不能一次操作整个大集合
遍历数据使用SCAN命令
批量删除数据,对于数据量很大的key,不能直接将key删除,而是应该遍历+删除的方式,比如Set的删除使用sscan + srem
public void delBigSet(String host, int port, String password, String bigSetKey) { Jedis jedis = new Jedis(host, port); if (password != null && !"".equals(password)) { jedis.auth(password); } ScanParams scanParams = new ScanParams().count(100); String cursor = "0"; do { ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams); List<String> memberList = scanResult.getResult(); if (memberList != null && !memberList.isEmpty()) { for (String member : memberList) { jedis.srem(bigSetKey, member); } } cursor = scanResult.getStringCursor(); } while (!"0".equals(cursor)); //删除bigkey jedis.del(bigSetKey);}
其他的List删除用ltrim;Hash删除: hscan + hdel;SortedSet删除: zscan + zrem。
减少网络交互
Redis是内存数据库,而且数据结构经过优化,所以理论上访问Redis的数据比访问程序内存还要快。但是,实际情况是到Redis存取数据是要经过网络的,耗在网络上的时间在整个交互中占很大比例。举个例子,我要从Redis取10个Key,一般局域网一次网络请求往返需要耗时20ms-30ms,取10个Key就需要200ms-300ms,这个时间已经接近人眼睛能看出的延时了。如果我们把这10个GET操作合成一个MGET操作,那就只需要1次往返,20ms-30ms就够了。所以下面的建议适用于批量操作。
多使用mget/mset等批量操作命令,类似的还有hmget和hmset
Pipeline使用pipeline可以将多个不同的命令打包一起发给Redis, 执行完后返回所有命令的结果,将多次交互转换为一次。现在主流的Redis客户端都支持pipeline,比如论坛中新发一个帖子使用pipeline的例子:
public void create(article) { //获取一个新的博客ID,使用INCR命令 id := INCR ${seq_id_key} //使用Pipline将数据封装到一个call里面 SessionCallback<Void> sessionCallback = new SessionCallback<Void>() { public Void execute() { //保存文章基本属性,使用Hash的HMSET命令,propertis是文章的属性,可以是键值对 HMSET ${article_key} ${properties} //将文章的完整HTML单独保存一个key,使用SET命令 SET ${article_content_key}, ${content} //将文章ID放入所有文章列表,使用创建时间作为score,使用SortedSet的zAdd命令 ZADD ${id_list_key} ${id} ${time} } }; executePipelined(sessionCallback);}
事务使用事务和使用pipeline类似,都是将一批命令提交给Redis执行,Redis的事务功能较弱,只能保证ACID中的隔离性和一致性,无法保证原子性和持久性。所以不要将关键业务依赖Redis事务。
Redis提供两种持久化方式,快照(Snapshot)和追加文件(AOF),可同时使用。快照方式是指将Redis中存的所有数据导出到一个文件中,类似于MySQL的mysqldump命令。AOF是将数据的变化追加到一个日志文件里,类似于mysql的binlog。这两种方式各有优缺点和使用场景。 快照
优点:可在业务低谷时执行,不影响redis性能;保存的数据文件相对较小;从RDB文件恢复数据速度较快
缺点:快照是通过fork子进程的方式来保存数据的,如果redis中数据超过10G以上,这个时间会比较长,这段时间内redis是不能访问的;需仔细设置保存周期,保存间隔周期太长,出现问题后丢失数据较多;周期过短,频繁保存又影响业务访问。
建议:适合使用快照方式的业务系统应该具备如下特点:非关键业务,数据丢失影响不大;数据可通过其他系统恢复,比如当做缓存;Redis有明显的访问低谷期,比如半夜无人访问。 另外,如果业务有明显的低谷期,其实可以不使用Redis的自动持久化。可以设置定时任务,通过客户端向redis发送SAVE命令来保存数据。在命令执行期间,会阻塞其他命令,但是SAVE命令不需要fork子进程,所以速度要快很多。 对于自动的快照式持久化,redis支持持久化策略的设置,并支持设置多个,任何一个达到条件都会做持久化,建议根据自己可接受的周期进行设置。比如:
save 60 10 #60秒内有10次数据变化,做一次快照save 900 1 #15分钟内有1次数据变化,做一次快照
AOF
优点:数据实时备份;备份不影响写入;可设置参数让操作系统将数据强制刷到磁盘
缺点:备份文件过大,需要定时压缩;从aof文件恢复数据明显比从快照中恢复要慢;AOF文件压缩也会fork子进程,影响redis性能
建议:如果数据只存在redis中并且不能丢失,一定要打开aof;设置将数据强制刷新到磁盘,防止断电等异常情况导致数据丢失;打开AOF文件压缩功能;可将快照和AOF持久化结合起来使用,aof只保存上次快照之后的变化。 AOF的配置:
appendfsync everysec #每秒将变化数据强制刷新到磁盘,另外两个可选项always和no不建议选择auto-aof-rewrite-percentage 100 #AOF文件的体积比上一次重写之后的体积大了至少一倍(100%)的时候,Redis将再次执行重写BGREWRITEAOF命令。auto-aof-rewrite-min-size 64mb #和上一项结合使用,只有文件大于64m才做做aof重写
我们都知道Redis一个很有用的功能就是可以设置key的过期时间。Redis有两种方式来删除过期的Key:1)每次访问key时会先检查key是否过期,如果已过期会直接删除;2)后台定时任务从设置了过期时间的key中随机选一些key来检查是否过期。在redis运行过程中,这两种方式会同时起作用。
基于以上原因,为了防止大批量key在同一时间过期导致redis的负载上升,建议错开key的过期时间,比如在原来的过期时间上加一个随机值。
《Redis实战》作者的建议: 在 Redis 的实践中,众多因素限制了 Redis 单机的内存不能过大,例如:
当面对请求的暴增,需要从库扩容时,Redis 内存过大会导致扩容时间太长。
当主机宕机时,切换主机后需要挂载从库,Redis 内存过大导致挂载速度过慢。
持久化过程中的 fork 操作耗时过长。
一般来说 Redis 单机最大内存最好在 10GB 以内;不过这个数据并不是绝对的,可以通过观察线上环境 fork 的耗时来进行调整。观察的方法如下:执行命令 info stats,查看 latest_fork_usec 的值,单位为微秒。
为了减轻 fork 操作带来的阻塞问题,除了控制 Redis 单机内存的大小以外,还可以适度放宽 AOF 重写的触发条件。
另外,在虚拟机尤其是在云上安装Redis速度和在物理机上的速度可能差距很大,一定要做好基准测试。 在standalone和sentinel模式下,一个Redis实例可以支持15个库,一般情况下只需要使用0号库,可以在配置文件中禁用其它的库。如果为了业务上对数据做隔离而需要使用多库,最好还是考虑使用多个Redis实例。
最后,Redis是一个支持数据持久化的KV数据库,缓存只是它最广泛的使用方式,充分发挥它的数据结构优势,可以拓展出更多的使用场景。
参考资料:
《Redis In Action》(《Redis实战》) 作者:Josiah Carlson 翻译:黄健宏
《阿里云Redis开发规范》 作者:付磊-起扬
《Redis 命令参考》 作者:黄健宏
更多福利请关注官方订阅号“拍码场”
好内容不要独享!快告诉小伙伴们吧!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。