首页 > 数据库 >redis实践经验总结

redis实践经验总结

时间:2023-12-18 17:55:06浏览次数:45  
标签:缓存 redis Redis 实践 集群 内存 key 节点 经验总结

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

标签:缓存,redis,Redis,实践,集群,内存,key,节点,经验总结
From: https://www.cnblogs.com/yu007/p/17900854.html

相关文章

  • redis中的hash tag
    在集群模式下,如果lua脚本同时操作多个key,可能会出现:CROSSSLOTKeysinrequestdon'thashtothesameslot的错误。这种情况下,可以通过{...},来指定多个key使用相同的内容进行hash,例如:user:{123}:username和user:{123}:email就会用123去hash,保证落到同一个slot,也就是可以在单个......
  • 10倍提升-TiCDC性能调优实践
    作者:Jellybean结论先行针对v5.3.0版本的TiCDC,优化Sorter算子内存参数和Sink同步并发可以极大改善同步的性能,测试验证实时同步的QPS从5k优化到60k,提升12倍以上。测试验证,在满负载追数据同步场景,TiCDC的相关参数per-table-memory-quota设为800MB和同步任务......
  • Redis上层数据类型设计
    StringString即字符串对象,是Redis使用最多的数据类型,其使用key-value结构,key为唯一标识,value为存储内容。value不仅可以是字符串,也可以是数字,包括整数或者浮点数。value最多可以容纳的大小为512MB。>SETnameErickRenOK>SETage19OK>GETname"ErickRen">GETage......
  • Java | 多线程并发编程CountDownLatch实践
    关注:CodingTechWork引言  在一次数据割接需求中,数据需要通过编程的方式进行转移割接到新平台,此时若串行化方式,无疑会拉锯此次战斗,所以首当其冲要使用并发编程来降低割接时长。  本次主要考虑使用CountDownLatch工具类进行并发编程的控制。CountDownLatch概述  在并发编程过程......
  • Docker部署Node.js应用简单实践
    前言本文将从零至一,介绍如何在云服务器上通过Docker容器运行一个简单的Node应用。本文假设读者已经掌握基本的Linux,Docker,Node,Express知识。基本步骤本地写好nodejs应用,放到github。云服务器安装docker,配置国内镜像加速。拉取一个node镜像。从github拉取项目代码。编写Dockerfil......
  • nginx+lua+redis实现灰度发布
    前言:授人以鱼不如授人以渔.先学会用,在学原理,在学创造,可能一辈子用不到这种能力,但是不能不具备这种能力。这篇文章主要是沉淀使用nginx+lua+redis实现灰度,当我们具备了这种能力,随时可以基于这种能力和思想调整实现方案:比如nginx+lua+(其他数据源)、nginx+(其他脚本语言)一、灰度......
  • OpenSergo & Dubbo 微服务治理最佳实践
    *作者:何家欢,阿里云MSE研发工程师Why微服务治理?现代的微服务架构里,我们通过将系统分解成一系列的服务并通过远程过程调用联接在一起,在带来一些优势的同时也为我们带来了一些挑战。如上图所示,可以看到词云中所展示的都是目前微服务架构在生产上所遇到的挑战。比如,最常见的流......
  • 软件工程读后感10-代码阅读方法与实践4
    最近,我阅读了代码阅读方法与实践的下一部分。意义重大的编码工作,或大型、有组织体制之下的项目,比如GNU和BSD,都会采纳一套编码规范、指导原则或约定。计算机语言和编程系统为程序员如何表达一个给定的算法提供了大量的余地。代码规范提供风格上的指导,目标是增强代码的可靠性、易读......
  • Wireshark 实践(选做)
    Wireshark实践(选做)参考https://www.cnblogs.com/mq0036/p/11187138.html,访问一个网站,抓包分析一次TCP三次握手,四次分手的过程。TCP三次握手过程:客户端向服务器发送一个带有SYN(同步)标志位的数据包,请求建立连接。服务器收到请求后,返回一个带有SYN和ACK(确认)标志位的数据包,表示......
  • 阿里云AnalyticDB基于Flink CDC+Hudi实现多表全增量入湖实践
    湖仓一体(LakeHouse)是大数据领域的重要发展方向,提供了流批一体和湖仓结合的新场景。阿里云AnalyticDB for MySQL基于 Apache Hudi 构建了新一代的湖仓平台,提供日志、CDC等多种数据源一键入湖,在离线计算引擎融合分析等能力。本文将主要介绍AnalyticDB for MySQL基于Apache ......