节约空间是第一生产力,一切以减少 RT 为主要目标
Redis 是内存数据库,所有的数据都存在内存里面。因为内存是稀缺资源,所以省内存就是省钱,在不影响性能的情况下,提高内存使用率就是最好的优化方法。
另外,内存虽然访问速度足够快,但是当内存大到一定的程度,或者产生碎片的情况下,速度也还是会有明显的影响。内存过大还会导致数据持久化和主从复制时间过长,影响业务查询。所以,减少内存使用除了省钱之外,其实更大的优势是系统能力的提升。
除了节约空间,我们选择 Redis 目的最终只有一个,就是「快」。合理的使用和配置会最大化速度的提升。
KEY 的选择
key 的设计主要关注两点:可读、简短。
可读性
KEY 一般包含业务、模块、id 信息,用冒号隔开,比如业务名:表名:ID 这样的格式。比如 spring session 持久化的 key 就是 spring:session:${sessionID}。分隔符用冒号主要是使用惯例,而且像 Redis Manager 这样的图形客户端会自动将这样的 key 按冒号分割显示成树状结构。
当然惯例只是用来参考的,如果你 Redis 里面只存了一种业务,那显然业务名这段就可以去掉。比如全是手机号相关业务,那就用直接用手机号做 key,更极端一点用手机号的后 10 位做 key 就可以了,反正第一位都是 1。
简短性
在能充分表达意思的基础上,尽量缩减长度。 和关系型数据库元数据的机制不一样,Redis 的 key 是将完整的名字存在内存里的。而且 key 都是用 SDS 结构来保存,所以占有空间和 key 有几个字符成正比。比如 messages:publish:${id} 就明显不如 msg:pub:${id}。
VALUE 的选择
value 的选择主要关注以下几点:
拒绝BigKey
虽然 Redis 对于 value 的大小限制放的很宽,比如字符串的 value 最大可以是 512M。但还是不建议存太大的 value,太大的 value 会造成单次查询 I/O 太大,阻塞其他命令。string 最好不要超过 10k,集合类型最好不要超过 5000 个元素。
尽量存数值
能存 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。后面两种应用于特定场景,我们只说下前面几种。
**String,**用的最多的结构,如果 value 是一个字符串,那就用它来存;如果不是字符串,比如说一个 java 对象也可以先序列化再存,可以用 java 默认的序列化,也可以用 json。这样存储的对象是不能取单个属性的,只能一次性取出来,再在应用代码里反序列化。
List,类似于 java 中的 List,多个元素有序存储首选,可同时实现队列,堆栈的功能。
Set,类似于 java 中的 Set,多个元素的无续存储,元素自动去重。小集合的交并差操作,可在 Redis 中直接用 set 相关命令完成,当然集合太大的话不建议这么做。
SortedSet,自排序集合,每个元素有个额外的 score 字段用来排序。如果没有自排序需求,则没必要用 SortedSort,毕竟每个元素多存了一个属性,空间肯定要大一点。
Hash,一个 key 中可以存多个键值对,非常适合存储一个对象(或者可以理解成 mysql 中的一行)。可以非常方便的操作对象的单个属性。当然如果没有对单个属性的操作要求,转成 json 再存 string 更加省空间。 以上就是对 Redis 对外的数据结构的使用推荐,下面我们再深入一步,看看这些数据结构 Redis 在底层是怎么存储的,寻找更大的优化空间。
控制底层数据结构
String 有3种底层结构,Redis_ENCODING_RAW(未加工), Redis_ENCIDING_INT(存成数值), Redis_ENCODING_EMBSTR(存成 header+数据的结构)。当 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 来做还是程序中来处理。
下面是我总结的一些 Redis 中很好用的命令,个人理解,可能有遗漏,详细的请参考官方文档。
String
- MGET/MSET:一个命令做多个 Key 的 GET 或者 SET 操作,非常有用的命令,提速利器。
- APPEND:往字符串上追加值,并且返回追加后的长度。
- INCR/DECR:类似于程序中的 i++, 如果 key 的值是数值,做原子的加减动作。最重要的是比 i++安全,不会有线程并发的问题。并且返回加减后的值。
- GETRANGE/SETRANGE:返回截取的子字符串
- GETSET:赋新值并返回旧值
Hash
- HMGET/HMSET, 用在 Hash 上的 MGET/MSET
- HINCRBY,如果 field 是数值的话,为哈希表 key 中的域 field 的值加上一个值,正负都可以。返回命令执行后的值。
List
- BLPOP/BRPOP,从左边或者右边获取第一个元素,如果列表为空则阻塞
- LRANGE,返回子列表
Set
- SDIFF,返回多个集合的差集
- SINTER,返回多个集合的交集
- SUNION,返回多个集合的并集
- SMOVE,将元素从一个集合移动到另外一个集合
SortedSet
- ZRANK/ZREVRANK,返回元素的排名/倒数排名,按元素的分数
- ZREVRANGE,返回区间内成员,成员的位置按 score 值递减(从大到小)来排列
- ZUNIONSTORE,返回多个有序集合的并集,并存到结果集合中。在合并过程中可以设置某一个集合的权重,和集合元素的聚合方式
- ZINTERSTORE,类似 ZUNIONSTORE,返回交集
SCAN
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
SORT 是另外一个有用的命令,返回列表、集合、有序集合经过排序的元素。支持使用另外的 key 进行排序,支持使用 Hash 的 field 进行排序。如果数据量不大,建议使用 Redis 排序,跟先取出数据然后在程序中排序比,大大减少和 Redis 的交互次数。
举个例子(更复杂的情况请参考官方文档):
# 开销金额列表
Redis > LPUSH today_cost 30 1.5 10 8
(integer) 4
# 排序
Redis > SORT today_cost
1) "1.5"
2) "8"
3) "10"
4) "30"
# 逆序排序
Redis 127.0.0.1:6379 > SORT today_cost DESC
1) "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 的速度。
举个例子,我要从 Redis 取 10 个 Key,一般局域网一次网络请求往返需要耗时 3ms-5ms,取 10 个Key就需要 30ms-50ms,这个时间已经接近一次普通的数据库查询的时间了。如果我们把这 10 个 GET 操作合成一个 MGET 操作,那就只需要 1 次往返,3ms-5ms 就够了。
所以下面的建议适用于批量操作。
- 多使用 mget/mset 等批量操作命令,类似的还有 hmget 和 hmset
- 使用 pipeline 可以将多个不同的命令打包一起发给 Redis, 执行完后返回所有命令的结果,将多次交互转换为一次。现在主流的 Redis 客户端都支持 pipeline。
举个例子,把一篇文章存到 Redis 中:
public void create(ArticleDto article) {
//获取一个新的博客ID
article.setId(seqSupport.nextValue(ARTICLE_SEQ));
...
//使用Pipline将数据保存到Redis
SessionCallback<Void> sessionCallback = new SessionCallback<Void>() {
@Override
public <K, V> Void execute(RedisOperations<K, V> RedisOperations) throws DataAccessException {
//保存文章基本属性,使用Hash的HMSET命令,BeanUtils.beanToMap这个方法是将POJO转成Map
RedisOperations.opsForHash().putAll((K)("article:" + article.getId()), BeanUtils.beanToMap(article, "userLike"));
//将文章的完整HTML单独保存一个key,使用SET命令
RedisOperations.opsForValue().set((K)("article:content:" + article.getId()), (V)content);
//将文章ID放入所有文章列表,使用创建时间作为score,使用SortedSet的zAdd命令
String score = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
RedisOperations.opsForZSet().add((K)"article:ids", (V)article.getId(), Double.parseDouble(score));
return null;
}
};
RedisSupport.executePipelined(sessionCallback);
log.debug("Save activle article:{} success", JSON.toJSONString(article));
}
- 事务 使用事务和使用 pipeline 类似,都是将一批命令提交给 Redis 执行,Redis 的事务功能较弱,只能保证 ACID 中的隔离性和一致性,无法保证原子性和持久性。所以不要将非常关键的业务操作依赖 Redis 事务。
持久化的选择
Redis 提供两种持久化方式,快照(Snapshot)和追加文件(AOF),可同时使用。快照方式是指将 Redi s中存的所有数据导出到一个文件中,类似于 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重写
KEY的过期机制
我们都知道 Redis 一个很有用的功能就是可以设置 key 的过期时间。
Redis 有两种方式来删除过期的 Key:
1)每次访问 key 时会先检查 key 是否过期,如果已过期会直接删除;
2)后台定时任务从设置了过期时间的 key 中随机选一些 key 来检查是否过期。
基于以上原因,为了防止大批量key在同一时间过期导致 Redis 的负载上升,建议错开 key 的过期时间,比如在原来的过期时间上加一个随机值。
写在最后
《 Redis 实战》作者的建议: 在 Redis 的实践中,众多因素限制了 Redis 单机的内存不能过大,例如:
- 当面对请求的暴增,需要从库扩容时,Redis 内存过大会导致扩容时间太长。
- 当主机宕机时,切换主机后需要挂载从库,Redis 内存过大导致挂载速度过慢。
- 持久化过程中的 fork 操作。一般来说 Redis 单机最大内存最好在 10GB 以内;不过这个数据并不是绝对的,可以通过观察线上环境 fork 的耗时来进行调整。
观察的方法如下:执行命令 info stats,查看 latest_fork_usec 的值,单位为微秒。 为了减轻 fork 操作带来的阻塞问题,除了控制 Redis 单机内存的大小以外,还可以适度放宽 AOF 重写的触发条件。 另外,在虚拟机尤其是在云上安装Redis速度和在物理机上的速度可能差距很大,一定要做好基准测试。
如果要在你的日常工作中用好 Redis,以上内容是必须要掌握的。Redis 的作用远远不至于缓存,在架构实战篇,我们会设计一个使用 Redis 做后端存储的项目,看看如何最大化发挥 Redis 的作用。
以上有启发,左下角告诉我呀,点我跳转专栏目录
🚀 掌握这些技巧,让你的 Redis 飞起来! 🚀
Redis 作为内存数据库,性能优化是关键!想要最大化 Redis 的速度和效率?以下这些技巧你必须掌握:
🔑 Key 设计:简短、可读是关键!使用冒号分隔业务、模块和 ID,既清晰又高效。
💾 Value 选择:拒绝 BigKey!字符串不超过 10k,集合元素不超过 5000,避免阻塞其他命令。
📊 数据结构:选择合适的数据结构是成功的一半!String、List、Set、SortedSet、Hash,各有其用武之地。
⚙️ 底层优化:控制底层数据结构,合理设置参数,进一步提升性能。
🔧 命令优先:使用 Redis 提供的批量操作命令,如 MGET/MSET,减少网络交互,提升效率。
📂 持久化策略:根据业务需求选择快照或 AOF,确保数据安全的同时不影响性能。
⏰ 过期机制:错开 key 的过期时间,避免大批量 key 同时过期导致的负载上升。
💡 最后建议:单机内存控制在 10GB 以内,定期观察 fork 操作耗时,做好基准测试。
掌握这些技巧,让你的 Redis 性能飙升!💪 更多详细内容,点击链接查看完整文章:点我跳转专栏目录
#Redis #性能优化 #数据库 #技术分享