写点什么

面试必备:一线大厂 Redis 缓存设计规范与性能优化

作者:EquatorCoco
  • 2024-03-01
    福建
  • 本文字数:5380 字

    阅读完需:约 18 分钟

说在前面


你是否在使用 Redis 时,不清楚 Redis 应该遵循的设计规范而苦恼?

你是否在 Redis 出现性能问题时,不知道该如何优化而发愁?

你是否被面试官拷问过 Redis 的设计规范和性能优化而回答不出来

别慌,看这篇文章就行了


本文,已收录于,我的技术网站 aijiangsir.com,有大厂完整面经,工作技术,架构师成长之路,等经验分享


正文


一、Redis Key-Value 设计规范 &性能优化


1. key 名设计规范


(1)【建议】: 可读性和可管理性

以业务名(或数据库名)为前缀(防止 key 冲突),用冒号分隔,比如业务名:表名:id


(2)【建议】:简洁性

保证语义的前提下,控制 key 的长度,当 key 较多时,内存占用也不容忽视,例如:


(3)【强制】:不要包含特殊字符

反例:包含空格、换行、单双引号以及其他转义字符


2. Value 设计规范


(1)【强制】:拒绝 bigkey(防止网卡流量、慢查询)

在 Redis 中,一个字符串最大 512MB,一个二级数据结构(例如 hash、list、set、zset)可以存储大约 40 亿个(2^32-1)个元素,但实际中如果下面两种情况,我就会认为它是 bigkey。


  1. 字符串类型:它的 big 体现在单个 value 值很大,一般认为超过 10KB 就是 bigkey。

  2. 非字符串类型:哈希、列表、集合、有序集合,它们的 big 体现在元素个数太多。


一般来说,string 类型控制在 10KB 以内;

hash、list、set、zset 元素个数不要超过 5000。

反例:一个包含 200 万个元素的 list。


3. bigkey 性能优化


bigkey 的危害:


  1. 导致 redis 阻塞

  2. 网络拥塞

bigkey 也就意味着每次获取要产生的网络流量较大;

假设一个 bigkey 为 1MB,客户端每秒访问量为 1000,那么每秒产生 1000MB 的流量,对于普通的千兆网卡(按照字节算是 128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个 bigkey 可能会对其他实例也造成影响,其后果不堪设想。


  1. 过期删除

有个 bigkey,它安分守己(只执行简单的命令,例如 hget、lpop、zscore 等),但它设置了过期时间,当它过期后,会被删除,如果没有使用 Redis 4.0 的过期异步删除(lazyfree-lazy- expire yes),就会存在阻塞 Redis 的可能性。


bigkey 的产生:


一般来说,bigkey 的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:

  1. 社交类: 粉丝列表,如果某些明星或者大 v 不精心设计下,必是 bigkey。

  2. 统计类: 例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是 bigkey。

  3. 缓存类: 将数据从数据库 load 出来序列化放到 Redis 里,这个方式非常常用,但有两个地方需要注意,第一,是不是有必要把所有字段都缓存;第二,有没有相关关联的数据,有的同学为了图方便把相关数据都存一个 key 下,产生 bigkey。


如何优化 bigkey


1、拆

  1. 如果是大 List(big list),那么就可以拆成多个 List:

比如拆成:list1、list2、...listN


  1. 如果是一个大的哈希表(big hash),可以将数据分段存储:

比如一个大的 key,假设存了 1 百万的用户数据,可以拆分成 200 个 key,每个 key 下面存放 5000 个用户数据


如果 bigkey 不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要 hmget,而不是 hgetall),删除也是一样,尽量使用优雅的方式来处理。


2、选择合适的数据类型【推荐】


最好的优化方案其实是在设计阶段,所以我们在使用 Redis 时,在设计阶段就应该尽量避免 bigkey,所以选择合适的数据类型尤为重要。

例如:实体类型(要合理控制和使用数据结构,但也要注意节省内存和性能质检的平衡)

错误的做法:


set user:1:name tomset user:1:age 19set user:1:favor football
复制代码


正确的做法:


hmset user:1 name tom age 19 favor football
复制代码


3、控制 key 的生命周期,redis 不是垃圾桶,当不需要使用的数据,及时过期清理【推荐】

建议使用 Expire 设置过期时间

条件允许可以打散过期时间,防止几种过期

比如:设置 key 的过期时间时,采用固定过期时间+一定范围内的随机数


二、Redis 命令的使用规范 &性能优化


1、使用 O(N)类型的命令要注意关注 N 的数量【推荐】


比如 hgetall、lrange、smembers、zrange、sinter 等并非不能使用。

但是在使用的时候一定要明确 N 的值,不然就可能由于查询数据太大导致 redis 阻塞。

建议:有遍历的需求时可以使用 hscan、sscan、zscan 代替


2、生产环境禁用部分高危命令【推荐】

禁止线上使用 keys、flushall、flushdb 等,通过 redis 的 rename 机制禁掉命令。

当有需要扫描的需要时,建议使用 scan 方式渐进式处理


3、合理使用 select【推荐】

redis 的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰

所以建议 redis 使用数据库只用序号 0 的数据库即可,在 0 数据库里采用 key 前缀区分业务即可


4、使用批量操作提高效率【推荐】

当我们要插入多个 key 时,可以采用一些批量命令代替单个命令,提高查询效率,例如:


1原生命令:例如mget、mset。2非原生命令:可以使用pipeline提高效率。
复制代码


但要注意控制一次批量操作的元素个数(例如 500 以内,实际也和元素字节数有关)。

注意两者不同:


11. 原生命令是原子操作,pipeline是非原子操作。22. pipeline可以打包不同的命令,原生命令做不到33. pipeline需要客户端和服务端同时支持。
复制代码


5、redis 事务功能较弱,不建议过多使用 redis 的事务命令

如果业务上有需要,可以使用 lua 替代【建议】


三、客户端使用规范 &性能优化

1、避免多个应用使用同一个 Redis 实例【推荐】


错误的做法:

多个业务线公用同一个 redis 实例,比如订单、库存、权限都用同一个 redis 实例,只要有一块业务有阻塞,所有业务都会受影响。

正确的做法:

不相干的业务拆分为独立的 redis 实例,比如订单、库存、权限拆分为 3 个 redis 实例。


2、客户端连接使用带有连接池的连接,可以有效控制连接,同时提高效率:

Jedis 使用连接池方式:


JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();jedisPoolConfig.setMaxTotal(5);jedisPoolConfig.setMaxIdle(2);jedisPoolConfig.setTestOnBorrow(true);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null);Jedis jedis = null;
复制代码


使用连接池执行命令:


try {    jedis = jedisPool.getResource();    //执行具体的命令    jedis.executeCommand()} catch (Exception e) {    logger.error("op key {} error: " + e.getMessage(), key, e);} finally {//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。if (jedis != null)    jedis.close();}
复制代码



3、连接池配置参数优化建议

1、maxTotal 优化:最大连接数(早期版本叫 maxActive)【建议】


实际上最大连接数该如何优化,是一个很难回答的问题,考虑的因素有很多:

比如:

  1. 业务希望的 Redis 并发量

  2. 客户端执行命令时间

  3. Redis 资源:例如 nodes(实例应用个数)* maxTotal 是不能超过 Redis 的最大连接数 maxClients

  4. 资源开销:例如虽然希望控制空闲连接(连接池此刻可马上使用的连接),但是又不希望因为连接池的频繁释放、创建连接造成不必要的开销。


以一个例子说明,假设:


一次命令时间(borrow|return resource + Jedis 执行命令(含网络) )的平均耗时约为 1ms,一个连接的 QPS 大约是 1000

业务期望的 QPS 是 50000

那么理论上需要的资源池大小是 50000 / 1000 = 50 个。但事实上这是个理论值,还要考虑到要比理论值预留一些资源,通常来讲 maxTotal 可以比理论值大一些。

但这个值不是越大越好,一方面连接太多占用客户端和服务端资源,另一方面对于 Redis 这种高 QPS 的服务器,一个大命令的阻塞即使设置再大资源池仍然会无济于事。


2、maxldle 和 minldle 优化:(资源池允许最大空闲连接数和资源池确保最少空闲连接数)【建议】


maxldle(最大空闲连接数):


maxIdle 实际上才是业务需要的最大连接数,maxTotal 是为了给出余量,所以 maxIdle 不要设置过小,否则会有 new Jedis(新连接)开销。


连接池的最佳性能是 maxTotal = maxIdle,这样就避免连接池伸缩带来的性能干扰。但是如果并发量不大或者 maxTotal 设置过高,会导致不必要的连接资源浪费。一般推荐 maxIdle 可以设置为按上面的业务期望 QPS 计算出来的理论连接,maxTotal 可以再放大一倍。


minIdle(最小空闲连接数):


minIdle 与其说是最小空闲连接数,不如说是"至少需要保持的空闲连接数",在使用连接的过程中,如果连接数超过了 minIdle,那么继续建立连接,如果超过了 maxIdle,当超过的连接执行完业务后会慢慢被移出连接池释放掉


所以最小空闲连接数需要根据自己的业务规模和客户端规模自行评估配置


【建议】:


如果你的系统 QPS 很高,系统启动完马上就会有很多的请求过来,那么可以给 redis 连接池做预热,比如快速的创建一些 redis 连接,执行简单命令,类似 ping(),快速的将连接池里的空闲连接提升到 minIdle 的数量。


连接池预热示例代码:


List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {    Jedis jedis = null;    try {            jedis = pool.getResource();            minIdleJedisList.add(jedis);            jedis.ping();        } catch (Exception e) {            logger.error(e.getMessage(), e);        } finally {        //注意,这里不能马上close将连接还回连接池,否则最后连接池里只会建立1个连接。。        //jedis.close();        }    }    //统一将预热的连接还回连接池for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {    Jedis jedis = null;    try {        jedis = minIdleJedisList.get(i);        //将连接归还回连接池        jedis.close();      } catch (Exception e) {        logger.error(e.getMessage(), e);      } finally {
}}
复制代码


总之,要根据实际系统的 QPS 和调用 redis 客户端的规模整体评估每个节点所使用的连接池大小


3、【建议】高并发下,建议客户端添加熔断功能

(例如接入 sentinel、hystrix)


4、【推荐】设置合理的密码

有必要可以使用 SSL 加密访问


5、【建议】设置合适的缓存淘汰策略

LRU 算法(Least Recently Used,最近最少使用)

淘汰很久没被访问过的数据,以最近一次访问时间作为参考。

LFU 算法(Least Frequently Used,最不经常使用)

淘汰最近一段时间被访问次数最少的数据,以次数作为参考。


当存在热点数据时,LRU 的效率很好,但偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降,缓存污染情况比较严重。这时使用 LFU 可能更好点。

根据自身业务类型,配置好 maxmemory-policy(默认是 noeviction),推荐使用 volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。

当 Redis 运行在主从模式时,只有主结点才会执行过期删除策略,然后把删除操作”del key”同

步到从结点删除数据。


四、系统内核参数优化

1、vm.swapiness 配置,根据 linux 版本选择配置(默认 0)


swap 对于操作系统来说比较重要,当物理内存不足时,可以将一部分内存页进行 swap 到硬盘上,以解燃眉之急。


但世界上没有免费午餐,swap 空间由硬盘提供,对于需要高并发、高吞吐的应用来说,磁盘 IO 通常会成为系统瓶颈。


在 Linux 中,并不是要等到所有物理内存都使用完才会使用到 swap,系统参数 swppiness 会决定操作系统使用 swap 的倾向程度。swappiness 的取值范围是 0~100,swappiness 的值越大,说明操作系统可能使用 swap 的概率越高,swappiness 值越低,表示操作系统更加倾向于使用物理内存。


swappiness 的取值越大,说明操作系统可能使用 swap 的概率越高,越低则越倾向于使用物理内存。


如果 linux 内核版本<3.5,那么 swapiness 设置为 0,这样系统宁愿 swap 也不会 oom kille(杀掉进程)

如果 linux 内核版本>=3.5,那么 swapiness 设置为 1,这样系统宁愿 swap 也不会 oom killer


一般需要保证 redis 不会被 kill 掉:


cat /proc/version #查看linux内核版本echo 1 > /proc/sys/vm/swappinessecho vm.swapiness=1 >> /etc/sysctl.conf
复制代码


PS:OOM killer 机制是指 Linux 操作系统发现可用内存不足时,强制杀死一些用户进程(非内核进程),来保证系统有足够的可用内存进行分配。


2、vm.overcommit_memory 配置改为 1(默认 0)


0:表示内核将检查是否有足够的可用物理内存(实际不一定用满)供应用进程使用;

  • 如果有足够的可用物理内存,内存申请允许;

  • 否则,内存申请失败,并把错误返回给应用进程

1:表示内核允许分配所有的物理内存,而不管当前的内存状态如何;

如果是 0 的话,可能导致类似 fork 等操作执行失败,申请不到足够的内存空间

Redis 建议把这个值设置为 1,就是为了让 fork 操作能够在低内存下也执行成功。


cat /proc/sys/vm/overcommit_memoryecho "vm.overcommit_memory=1" >> /etc/sysctl.confsysctl vm.overcommit_memory=1
复制代码


3、合理设置文件句柄数


操作系统进程试图打开一个文件(或者叫句柄),但是现在进程打开的句柄数已经达到了上限,继续打开会报错:“Too many open files”


ulimit ‐a #查看系统文件句柄数,看open files那项ulimit ‐n 65535 #设置系统文件句柄数
复制代码

总结


本文梳理了在使用 Redis 过程需要遵循的一些最佳实践,包括针对架构维度的一些深入性能优化的知识,如果面试官问你:"说下在使用 Redis 的过程中,需要注意哪些规范?",如果你按照本文的思路回答,肯定能让面试官眼前一亮,offer 自然就到手了。


文章转载自:江-小北

原文链接:https://www.cnblogs.com/jiang-xiao-bei/p/18045759

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
面试必备:一线大厂Redis缓存设计规范与性能优化_redis_EquatorCoco_InfoQ写作社区