redis实践经验总结

发布时间 2023-12-18 17:53:45作者: 鱼007

Redis内存配置

当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到80%以上时就需要我们警惕,并快速定位到内存占用的原因。
一般来说,会有以下几种占用内存的情况:

  • 数据内存
    是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题
  • 进程内存
    Redis主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略。
  • 缓冲区内存
    包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出。

查看内存分配状态

info memory

drawing

memory xxx

drawing

查看最大内存大小

获取Redis能使用的最大内存大小:config get maxmemory
drawing
如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器限制最大 3 GB 的可用内存
设置Redis最大占用内存大小:config set maxmemory 1000mb #设置Redis最大占用内存大小为1000M

查看内存淘汰策略

config get maxmemory-policy
drawing

关于redis的内存淘汰策略,在redis.conf中的配置为:maxmemory-policy noeviction
下面大概讲一下redis六种淘汰策略:
1.noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
2.allkeys-lru:从所有key中使用LRU算法进行淘汰(LRU算法:即最近最少使用算法)
3.volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
4.allkeys-random:从所有key中随机淘汰数据
5.volatile-random:从设置了过期时间的key中随机淘汰
6.volatile-ttl:在设置了过期时间的key中,淘汰过期时间剩余最短的
当使用volatile-lru、volatile-random、volatile-ttl这三种策略时,如果没有key可以被淘汰,则和noeviction一样返回错误
修改淘汰策略:config set maxmemory-policy allkeys-lru

内存缓冲区配置

内存缓冲区常见的有三种:

  • 复制缓冲区:主从复制的 repl_backlog_buf ,如果太小可能导致频繁的全量复制,影响性能。通过 repl_backlog_size 来设置,默认1mb
  • AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区,无法设置容量上限
  • 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置,输出缓冲区可以设置
    drawing
    默认的配置如下:
    drawing

Redis服务端优化

命令及安全配置

Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网上,而Redis如果没有做身份认证,会出现严重的安全漏洞。
因此,以下操作会存在较高风险:

  • Redis未设置密码
  • 利用了Redis的 config set 命令动态修改了Redis配置
  • Root账号权限启动Redis

这里给出一些建议:

  • Redis一定要设置密码
  • 不要使用Root账号启动Redis
  • 尽量不要使用默认端口启动(6379)
  • 开启防火墙;限制网卡,禁止外网网卡访问
  • 禁止在线上使用这些命令:keys、flushall、flushdb、config set等命令。可以利用rename-command来给这些命令重命名达到禁用的目的

慢查询

慢查询:在Redis执行中耗时超过某个阈值的命令,称为慢查询。慢读和慢写统称慢查询。
慢查询的阈值可以通过配置指定:
lslowlog-log-slower-than:慢查询阈值,单位是微秒。默认是10000,建议1000
慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:
lslowlog-max-len:慢查询日志(本质是一个队列)的长度。默认是128,建议1000
修改这两个配置可以使用 config set 命令:
drawing

查看慢查询日志列表:
slowlog len:查询慢查询日志的长度
slowlog get [n]:读取n条慢查询日志
slowlog reset:清空慢查询列表
drawing

持久化配置

Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化要遵循以下建议:

  • 用来做临时缓存的Redis实例尽量不要开启持久化功能
  • 建议关闭RDB持久化功能,使用AOF持久化
  • 利用脚本定期在slave节点做RDB,实现数据备份
  • 设置合理的rewrite阈值,避免频繁的bgrewrite
    AOF文件膨胀到需要rewrite时又或者接收到客户端的bgrewriteaof命令会fork出一个子进程进行rewrite,而父进程继续接受命令,现在的写操作命令都会被额外添加到一个aof_rewrite_buf_blocks缓冲中)
  • 配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞

集群最佳实践

集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:

  1. 集群完整性问题(插槽)
  2. 集群带宽问题(心跳机制)
  3. 数据倾斜问题(BigKey)
  4. 命令的集群兼容性问题
  5. lua和事务问题
集群完整性问题(插槽)

在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务。因此建议将 cluster-require-full-coverage 配置为false。
drawing

集群带宽问题(心跳机制)

集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:插槽信息、集群状态信息
集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。如果单机部署多个节点,那么带宽就会倍增。
解决建议:

  1. 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则拆分成多个集群
  2. 避免在单个物理机中运行太多redis实例
  3. 配置合适的 cluster-node-timeout :节点心跳失败的超时时间(默认cluster-node-timeout 15000)
  4. 提高带宽:可以通过添加更多的带宽或升级网络设备来提高Redis集群的带宽,以满足高并发场景下的需求。
数据倾斜问题(BigKey)

在 Redis 集群模式下,数据倾斜问题往往是由以下几个原因导致的:

  • 哈希槽分配不均
    Redis 将所有的键映射到哈希槽中,然后将哈希槽分布到各个节点上。如果某些节点上的哈希槽分配过多,就会导致某些节点存储的数据比其他节点多很多。

  • 热点键(概率性)集中在某些节点上
    在 Redis 集群模式下,对于某些被频繁地访问的热点键,它们有可能会被存储在同一个节点上,从而导致该节点的负载较大。

  • 新节点加入不平衡
    当新节点加入 Redis 集群时,Redis 会自动将部分哈希槽分配到新节点上。如果新节点的加入不均衡,就会导致数据倾斜的问题。

  • 节点故障恢复不平衡
    当某个节点故障时,Redis 会自动将该节点上的哈希槽重新分配给其他节点。如果故障节点的负载很高,重新分配的哈希槽就会集中到少数几个节点上,从而导致数据倾斜的问题。

以下是一些解决 Redis 集群数据倾斜问题的方法:

  1. 调整哈希槽分配:Redis 将所有的键映射到哈希槽中,然后将哈希槽分布到各个节点上。如果某些节点上的哈希槽分配过多,可以通过手动调整哈希槽分配来解决数据倾斜的问题:使用 redis-cli 工具的 reshard 命令或者第三方工具 Redis-trib 来进行哈希槽的迁移。
  2. 增加节点数量:增加节点数量可以扩容 Redis 集群,从而解决数据倾斜的问题。在增加节点时,Redis 会自动将部分哈希槽分配到新节点上。
  3. 使用虚拟节点:虚拟节点是指将一个物理节点划分为多个虚拟节点,每个虚拟节点负责一部分哈希槽。这样可以避免某个物理节点上的哈希槽分配过多的情况发生。
  4. 优化键的设计:Redis 集群中的数据倾斜往往是由于一些热点键导致的。可以优化热点键的设计,比如将一个热点键拆分成多个键或者使用哈希表来存储数据,从而减少某些节点上的负载。
命令的集群兼容性问题

在Redis集群中,有一些命令是不支持的或者在使用时需要注意兼容性问题。以下是一些常见的命令和它们的集群兼容性问题:

  • KEYS命令:在Redis集群中,KEYS命令会遍历整个集群,这可能会影响整个集群的性能。
  • MIGRATE命令:MIGRATE命令需要使用迁移槽来指定目标节点,但是在Redis 3.x版本之前,迁移槽并不是动态的,所以在使用MIGRATE命令时需要特别小心。
  • FLUSHDB和FLUSHALL命令:在Redis集群中,FLUSHDB和FLUSHALL只会清空当前节点的数据,而不是整个集群。
  • SORT命令:SORT命令在Redis集群中只能用于单个节点,因为它需要对整个集合进行排序,而不是分散到多个节点。
  • PUBLISH命令:由于Redis集群中没有中心节点,因此PUBLISH命令无法直接用于广播消息。取而代之的是,可以使用Lua脚本实现广播功能。
  • 总之,在使用Redis集群时,需要了解每个命令的集群兼容性问题,并采取相应的措施来确保集群的稳定性和性能。
lua和事务问题

在 Redis 集群模式下,由于数据被分散存储在不同的节点中,因此对于使用 Lua 脚本或事务进行操作的情况,需要注意以下几点:

  • 使用 Lua 脚本:在 Redis 集群模式下,Lua 脚本可以在任何一个节点上执行。但是,如果脚本需要访问多个键,那么这些键可能分布在不同的节点上,这时候就需要在脚本中使用 redis.call 或 redis.pcall 函数来显式地指定要访问哪个节点的键。否则,如果脚本中涉及到的键分布在不同的节点上,Redis 就会抛出 MOVED 错误。
  • 使用事务:Redis 事务的实现依赖于单个节点上的原子性和一致性。在集群模式下,由于数据被分散存储,当一个事务需要访问多个键时,这些键可能分布在不同的节点上,这就导致了事务的原子性和一致性不能得到保证。因此,Redis 集群模式下不建议使用事务。
    总的来说,当 Redis 集群模式下需要使用 Lua 脚本或事务时,需要特别注意键的分布情况,以确保操作的正确性。

Redis键值设计

优雅的key结构

redis的key索引使用了跳表算法,因此在保证key唯一的同时也要注意key的大小、格式
推荐:

  • 遵循格式-> [业务名称]:[数据名]:[id] -> 例:login:user:1
  • 不包含特殊字符
  • key的长度不超过44字节,务必避免BigKey
    此外,还设置合理的超时时间,否则往后有可能变成死数据(尤其是redis内存淘汰策略不是allkeys-lru时)

数据的合理聚合

上面提到BigKey问题,需要尽量控制value大小。相对地,实际中也存在key粒度过小、属性数据过于分散的情况,建议注意做好聚合。
例如要存储一个User对象,存储方案如下:

  • 字段打散(key为 user:[userid]:[属性])
    优点:可以灵活访问对象任意属性KV
    缺点:占用空间大,没办法做统一控制

  • json字符串(key为 user:[userid])
    优点:实现简单
    缺点:数据耦合,不够灵活

  • hash(key为 user:[userid])
    优点:底层使用 ziplist、ht(hash的entry数量超过500时) 存储,空间占用相对小,可以灵活访问对象的任意字段。
    缺点:代码会相对复杂些

避免BigKey

BigKey通常是指占用内存空间比较大的key,例如包含大量元素的hash、list、set、zset,或者字符串类型的value比较大的key。
BigKey的危害包括:
网络阻塞——对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
数据倾斜——BigKey所在的redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
Redis阻塞——对元素较多的hash,list,zset等做运算会耗时较久,使主线程被阻塞。
CPU压力——对BigKey的数据序列化、反序列化、过期删除等操作都会导致CPU的使用率飙升,影响Redis实例和本机其他应用。
推荐:

  • 单个key的value值小于10KB
  • 对于集合类型的key,或者Hash结构的entry数量,建议元素数量小于1000
  • 对于超大集合类型的key,建议设计拆分逻辑

假如有hash类型的key,其中有100万对字段和值,字段是自增id,这个key存在什么问题?如何优化?

方案一:使用hash存储100万对字段和值
drawing
存在问题:hash的entry数量超过500时,会使用哈希表而不是ZipList,内存占用较多。虽然可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题
这个方式存储的内存占用情况如下:
drawing

方案二:拆分为string类型
drawing
存在问题:string结构底层没有太多内存优化,内存占用较多,想要批量获取这些数据比较麻烦。
内存占用情况如下:
drawing

方案三:拆分为小的hash,将id/100作为key,将id%100作为字段,这样每100个元素为hash
drawing
内存占用如下:
drawing
总结:方案三的内存占用最小,应该使用方案三的存储方式。

BigKey发现与删除

  • redis-cli --bigkeys
    利用redis-cli提供的–bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key

  • 自行scan扫描
    编程利用scan命令扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)

  • 第三方工具
    如 RESP客户端(类似navicat的可视化工具)分析RDB快照文件,全面分析内存使用情况

  • 网络监控
    自定义工具,监控进出Redis的网络数据,超出预警值时主动告警

由于BigKey内存占用较多,即便时删除这样的key也需要耗费很长时间,导致Redis主线程阻塞。针对不同版本有对应的删除方法。
Redis 3.0及以下版本:如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey
Redis 4.0以后:Redis在4.0后提供了异步删除的命令:unlink

redis的client端

命令批量执行

当redis操作串行化时:
一次命令的响应时间 = 1次往返的网络传输耗时 + 1次Redis执行命令耗时
N次命令的响应时间 = N次往返的网络传输耗时 + N次Redis执行命令耗时
对此,Redis提供了很多Mxxx这样的命令,可以实现批量插入数据,如:mset、hmset,可以把N次网络请求合并到1次。
mset示例如下:

// 定义要设置的key-value对
Map<String, String> keyValueMap = new HashMap<>();
keyValueMap.put("key1", "value1");
keyValueMap.put("key2", "value2");

// 使用mset方法设置key-value对
redisTemplate.opsForValue().multiSet(keyValueMap);

如果有对复杂数据类型的批处理需要,建议使用Pipeline功能:

List<Object> results = redisTemplate.executePipelined(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
    connection.set("key1".getBytes(), "value1".getBytes());
    connection.get("key1".getBytes());
    connection.set("key2".getBytes(), "value2".getBytes());
    connection.get("key2".getBytes());
    return null;
  }
});

for (Object result : results) {
  System.out.println(result);
}

注意:

  • 批处理时不建议一次携带太多命令
  • Pipeline的多个命令之间不具备原子性
  • 对于集群下的批处理,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败
    注:spring环境下默认使用并行slot,即在客户端计算每个key的slot,将slot一致分为一组,每组都利用Pipeline批处理,并行执行各组命令
    drawing

避免并发set key

问题来源:同时有client端去set一个key。
解决方案:

  • 分布式锁
    准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,该方法较为通用、常见
  • redis的事务机制:
    不推荐使用redis的事务机制,因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。
  • 队列
    使set操作串行化

redis作为数据库缓存

加强与数据库的一致性

一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的情况,只能做到最终一致性。MySQL 和 redis 数据一致性是一个复杂的课题,通常是多种策略同时使用,例如:延迟双删、redis 过期淘汰、通过路由策略串行处理同类型数据、分布式锁等等。
下面举例常见的解决方案:

  1. 延时双删,参考下面伪代码:
    def update_data(key, obj):
    del_cache(key) # 删除 redis 缓存数据。
    update_db(obj) # 更新数据库数据(若涉及主从同步可能会有ms级延时)
    logic_sleep(_time) # 删除延时;一定要大于其他请求将数据库旧数据写入redis的时间,以便能够把其他并发线程更新上去的老数据删除;此外还需要考虑读MySQL从库过程的主从同步耗时;因此可设置几百毫秒~几秒。
    del_cache(key) # 删除 redis 缓存数据。

  2. 删除+补偿
    方案一:先更新数据库,再删缓存,再加异步删除事件(例如利用消息队列)
    方案二:上述的延时双删,再加异步删除事件

实际上,上述方案只能降低不一致发生的概率,无法完全避免。因此,有强一致性要求且频繁修改的数据,不建议放缓存。

避免缓存穿透

缓存穿透:故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
解决方案:

  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
  • 采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
  • 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。

避免缓存雪崩

缓存雪崩:大面积的缓存同时失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。
解决方案:

  • 给缓存的失效时间,加上一个随机值,避免集体失效。该方案较为常见。
  • 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。具体操作为:
    I 从缓存A读数据库,有则直接返回
    II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
    III 更新线程同时更新缓存A和缓存B。

备注:参考、引用博文列表
Redis最佳实践/经验总结 —— https://blog.csdn.net/Decade_Faiz/article/details/131346119
Redis原理和机制详解 —— https://zhuanlan.zhihu.com/p/222697530