redis实践经验总结
Redis内存配置
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到80%以上时就需要我们警惕,并快速定位到内存占用的原因。
一般来说,会有以下几种占用内存的情况:
- 数据内存
是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题 - 进程内存
Redis主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略。 - 缓冲区内存
包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出。
查看内存分配状态
info memory
memory xxx
查看最大内存大小
获取Redis能使用的最大内存大小:config get maxmemory
如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器限制最大 3 GB 的可用内存
设置Redis最大占用内存大小:config set maxmemory 1000mb #设置Redis最大占用内存大小为1000M
查看内存淘汰策略
config get maxmemory-policy
关于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且不能设置,输出缓冲区可以设置
默认的配置如下:
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 命令:
查看慢查询日志列表:
slowlog len:查询慢查询日志的长度
slowlog get [n]:读取n条慢查询日志
slowlog reset:清空慢查询列表
持久化配置
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引起的阻塞
集群最佳实践
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
- 集群完整性问题(插槽)
- 集群带宽问题(心跳机制)
- 数据倾斜问题(BigKey)
- 命令的集群兼容性问题
- lua和事务问题
集群完整性问题(插槽)
在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务。因此建议将 cluster-require-full-coverage 配置为false。
集群带宽问题(心跳机制)
集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:插槽信息、集群状态信息
集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。如果单机部署多个节点,那么带宽就会倍增。
解决建议:
- 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则拆分成多个集群
- 避免在单个物理机中运行太多redis实例
- 配置合适的 cluster-node-timeout :节点心跳失败的超时时间(默认cluster-node-timeout 15000)
- 提高带宽:可以通过添加更多的带宽或升级网络设备来提高Redis集群的带宽,以满足高并发场景下的需求。
数据倾斜问题(BigKey)
在 Redis 集群模式下,数据倾斜问题往往是由以下几个原因导致的:
-
哈希槽分配不均
Redis 将所有的键映射到哈希槽中,然后将哈希槽分布到各个节点上。如果某些节点上的哈希槽分配过多,就会导致某些节点存储的数据比其他节点多很多。 -
热点键(概率性)集中在某些节点上
在 Redis 集群模式下,对于某些被频繁地访问的热点键,它们有可能会被存储在同一个节点上,从而导致该节点的负载较大。 -
新节点加入不平衡
当新节点加入 Redis 集群时,Redis 会自动将部分哈希槽分配到新节点上。如果新节点的加入不均衡,就会导致数据倾斜的问题。 -
节点故障恢复不平衡
当某个节点故障时,Redis 会自动将该节点上的哈希槽重新分配给其他节点。如果故障节点的负载很高,重新分配的哈希槽就会集中到少数几个节点上,从而导致数据倾斜的问题。
以下是一些解决 Redis 集群数据倾斜问题的方法:
- 调整哈希槽分配:Redis 将所有的键映射到哈希槽中,然后将哈希槽分布到各个节点上。如果某些节点上的哈希槽分配过多,可以通过手动调整哈希槽分配来解决数据倾斜的问题:使用 redis-cli 工具的 reshard 命令或者第三方工具 Redis-trib 来进行哈希槽的迁移。
- 增加节点数量:增加节点数量可以扩容 Redis 集群,从而解决数据倾斜的问题。在增加节点时,Redis 会自动将部分哈希槽分配到新节点上。
- 使用虚拟节点:虚拟节点是指将一个物理节点划分为多个虚拟节点,每个虚拟节点负责一部分哈希槽。这样可以避免某个物理节点上的哈希槽分配过多的情况发生。
- 优化键的设计: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万对字段和值
存在问题:hash的entry数量超过500时,会使用哈希表而不是ZipList,内存占用较多。虽然可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题
这个方式存储的内存占用情况如下:
方案二:拆分为string类型
存在问题:string结构底层没有太多内存优化,内存占用较多,想要批量获取这些数据比较麻烦。
内存占用情况如下:
方案三:拆分为小的hash,将id/100作为key,将id%100作为字段,这样每100个元素为hash
内存占用如下:
总结:方案三的内存占用最小,应该使用方案三的存储方式。
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批处理,并行执行各组命令
避免并发set key
问题来源:同时有client端去set一个key。
解决方案:
- 分布式锁
准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,该方法较为通用、常见 - redis的事务机制:
不推荐使用redis的事务机制,因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。 - 队列
使set操作串行化
redis作为数据库缓存
加强与数据库的一致性
一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的情况,只能做到最终一致性。MySQL 和 redis 数据一致性是一个复杂的课题,通常是多种策略同时使用,例如:延迟双删、redis 过期淘汰、通过路由策略串行处理同类型数据、分布式锁等等。
下面举例常见的解决方案:
-
延时双删,参考下面伪代码:
def update_data(key, obj):
del_cache(key) # 删除 redis 缓存数据。
update_db(obj) # 更新数据库数据(若涉及主从同步可能会有ms级延时)
logic_sleep(_time) # 删除延时;一定要大于其他请求将数据库旧数据写入redis的时间,以便能够把其他并发线程更新上去的老数据删除;此外还需要考虑读MySQL从库过程的主从同步耗时;因此可设置几百毫秒~几秒。
del_cache(key) # 删除 redis 缓存数据。 -
删除+补偿
方案一:先更新数据库,再删缓存,再加异步删除事件(例如利用消息队列)
方案二:上述的延时双删,再加异步删除事件
实际上,上述方案只能降低不一致发生的概率,无法完全避免。因此,有强一致性要求且频繁修改的数据,不建议放缓存。
避免缓存穿透
缓存穿透:故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
解决方案:
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
- 采用异步更新策略,无论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