Redis最佳实践
Redis键值设计
优雅的key结构
- 遵循基本格式:[业务名称]:[数据名]:[id] eg:login:user:10
- 长度不超过44字节:key为string类型,底层编码为int,embstr和raw(>44)三种。embstr在小于44字节使用,采用连续内存空间,占用小。
- 不包含特殊字符
拒绝BigKey
BigKey通常以key的大小和key中成员的数量来综合判断,例如:
- key本身的数据量过大:一个string类型的key,它的值为5MB
- key中的成员数过多:一个ZSET类型的key,他的成员数量为10000个
- ke0y中成员的数据量过大:一个Hash类型的key,它的成员数量虽然只有1000个,但value总大小为100MB
扩展:查看key大小:MEMORY USAGE key/value 简单:STRLEN key/LLEN key
推荐:1.单个key的value小于10KB 2.对于集合类型的key,建议元素数量小于1000
BigKey危害
- 网络阻塞:对BigKey执行读,少量QPS就可能导致贷款使用率被占满,导致Redis变慢
- 数据倾斜:BigKey所在的内存使用率远超其他实例,无法使数据分片的内存资源达到平衡
- Redis阻塞:对元素较多的hash,list,zset等做运算会耗时较久,使主线程阻塞
- CPU压力:对BigKey的数据序列化和反序列化会导致CPU飙升,影响Redis实例和本机其他应用。
如何发现BigKey:
- redis-cli --bigkeys:可遍历所有key,返回key整体统计信息与每个数据的Top1的bigkey
- scan扫描:自己编程,利用strlen,hlen等命令判断key长度(不建议用MEMORY USAGE)
- 第三方工具:Reids-Rdb-Tools分析RDB快照文件
- 网络监控:自定义工具,监控进出Reids的网络数据,超出预警值主动警告
如何删除BigKey:
- 小于redis3:集合类型遍历所有key,先逐个删除子元素,最后删除BigKey
- 大于redis4:异步删除命令:unlink
恰当的数据结构
推荐hash按照key打散,底层使用zipList,空间占用小,可灵活访问对象的任意字段,不推荐直接存jsonstring,hash和单个key数据
注意:hash的Entry数量超过500,会使用hash表而不是ZipList,可以调整hash-max-ziplist-entries配置entry上限,设置太大也会BigKey。
总结
key:固定格式,简短,不包含特殊字符
value:合理拆分,不BigKey。合适的数据结构。hash结构entry数量不超过1000。设置合理的超时时间。
批处理优化(如何优雅的处理海量数据)
Pipeline-大数据导入方式
MSET,HSET方式:
N条命令的批处理行:1次网络传输时间+N次Redis执行耗时
注意:不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网路阻塞。
//按照奇偶数批量插入数据
void test{
String[] arr = new String[2000];
int j;
for(int i = 0; i <= 10000; i++){
j = (i%1000) << 1;
arr[j] = "test:key_" + i;
arr[j + 1] = "value_" + i;
if(j == 0){ //当j=0时表示一组数据(1000)满了
jedis.mset(arr);
}
}
}
缺点:格式固定只能操作基础数据类型,不能扩展复杂数据,建议使用Pipeline
Pipeline:
void testPipeline(){
//创建管道
Pipeline pipeline = jedis.pipelined();
for(int i = 1; i<= 100000; i++){
//放入命令到管道
pipeline.set("test:key_" + i, "value_" + i);
//pipeline.hset("test:key_" + i, "value_" + i);
if(i % 1000 = 0){
//每放入1000条命令,批量执行
pipeline.sync();
}
}
}
总结:
批处理方案:原生M操作。Pipeline批处理
注意事项:批处理时不建议一次携带太多命令。Pipeline的多个命令之间不具备原子性
集群下的批处理
如MSET或Pipeline这样的操作需在一次请求中携带多条命令,而redis是集群那么批处理命令的多个key必须落在一个插槽内,否则就会执行失败。
spring底层采用并行slot,lettuce。springRedisTemplate.opsForValue().multiSet(map);:底层实现异步分区asynslot根据key计算插槽,把插槽一样的放在一个集合。
服务端优化
持久化配置
Redis持久化虽然可以保证数据安全,但会带来额外开销,需遵循以下建议:
- 用来做缓存的Redis实例尽量不要开启持久化
- 建议关闭RDB,使用AOF
- 利用脚本定期在salve节点做RDB,实现数据备份
- 设置合理的rewrite阈值,避免频繁bgrewrite
- 配置no-appendfsync-no-rewrite=yes(追求安全性配置no,性能yes),禁止在rewrite期间做aof,避免AOF引起的阻塞。
部署建议:
- Redis实例的物理机要预留足够内存,应对fork和rewrite
- 单个Redis实例内存上限不要太大,eg:4/8G,可以加快fork的速度,减少主从同步,数据迁移的压力。
- 不要与CPU密集型应用部署在一起。
- 不要与高硬盘负载应用一起部署。eg:数据库,消息队列。
慢查询
在Redis执行时耗时超过某个阈值的命令,成为慢查询。由于Redis单线程,客户端发送指令后会在服务端入队等待执行,超过阈值则报错。
慢查询的阈值配置:slowlog-log-slower-than:慢查询阈值,微秒。默认10000,建议1000
慢查询会被存入日志,日志长度配置:slowlog-max-len:默认128,建议1000
查询慢查询:slowlog get[n]
清空慢查询列表:slowlog reset
命令及安全配置
Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露在公网,而redis没做身份认证,会出现严重的安全漏洞。
Redis未授权访问配合SSH key:https://cloud.tencent.com/developer/article/1039000
漏洞原因:
- Redis暴露到公网,且没有密码
- 利用了Redis的config set命令动态修改Redis配置
- 使用了Root账号权限启动Redis
建议:
- 一定要设置密码,开启防火墙,不使用默认端口,不使用root账户启动redis
- 禁止线上使用:keys,flushall,flushdb,config set等命令。可利用rename-command禁用。
- bind:限制网卡,禁止外网网卡访问。
内存配置
当redis内存不足,会导致key频繁被删除,响应时间变长,QPS不稳定。内存占用超过90%就需要注意。
- 数据内存:BigKey,内存碎片 主要
- 进程内存:redis进程占用内存(代码,常量池) 可忽略
- 缓冲区内存:客户端缓冲区(输入,输出),AOF缓冲区,复制缓冲区。 波动大
Redis查看内存分配命令:info memory;memory xxx
内存缓冲区配置:
- 复制缓冲区:主从复制的repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过repl-backlog-size来设置。默认1mb
- AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区,无法设置容量上限。
- 客户端缓冲区:分为输入输出缓冲区,输入缓冲区最大1G且不能设置。输出可以。
集群最佳实践
集群还是主从?集群存在一些问题:
- 集群完整性问题-reids默认配置中,如果发现一个插槽不可用,整个集群都会停止。建议设置为false
- 集群带宽问题-集群节点之间会不断的ping来确定集群中其他节点的状态,每次ping都会包含插槽信息集群状态信息
集群节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽非常高。
解决:
① 避免大集群,最好小于1000
② 避免在单个物理机中运行太多的Redis实例
③ 配置合适的cluster-node-timeout值
- 数据倾斜问题-bigkey或者批处理时使用相同的hashkey从而导致部分节点负担较重
- 客户端性能问题-节点的选择,读写分离的判断,插槽的判断
- 命令的集群兼容性问题-批处理问题方案
- lua和事务问题-集群下不好保证
注意:单体Redis(主从)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需要的情况下,尽量不要搭建Reids集群。