Redis的最佳实践?看完不心动,算我输!!
一、Redis键值设计
1、优雅的key结构
Redis的key虽然可以自定义,但是最好遵循几个最佳实战约定:
-
遵循基本格式
:[业务名称]:[数据名]:[id]
login:user:10
-
不包含特殊字符
-
value长度不超过44字节
(4版本之前是39)
优点:
- 阅读性强
- 避免key冲突
- 方便管理。比如:删除该业务下所有的key
- 更节省内存
1.为什么value长度不尽量不超过44字节
key是String类型,底层编码包括:int、embstr、raw三种
:
-
int
:全部是数值的情况下,采用int编码。将字符串当成数值存储。 -
embstr
:小于44字节使用,是
连续的内存空间,内存占用更小
。 -
raw
:大于44字节使用,是通过指针,指向不同的内存空间。 由于
内存不连续,所以访问的时候性能收到影响。
# 测试value,是纯数字。底层编码是int
127.0.0.1:6379> set name 123
OK
127.0.0.1:6379> object encoding name
"int"
# 测试44个字节,底层编码是embstr
127.0.0.1:6379> set name 12345678912345678912345678912345678912345678
OK
127.0.0.1:6379> object encoding name
"embstr"
# 测试45个字节,底层编码是raw
127.0.0.1:6379> set name 123456789123456789123456789123456789123456789
OK
127.0.0.1:6379> object encoding name
"raw"
2、拒绝BigKey
1.什么是BigKey
虽然一个key,最大能存放512,但是5MB的就是很大的key了
BigKey通常以key的大小和key中成员变量来综合判定,例如:
-
key本身的数据量过大:
一个String类型的key,它的值是5MB
-
key中的成员变量过多:
一个ZSET类型的key,他的成员变量10,000个
-
key中的成员数据量过大:
一个Hash类型的key,它的成员变量虽然只有1000个,但是这些成员变量的value总大小为100MB
推荐值:
- 对于String类型的单个key,建议value小于10KB
- 对于集合类型的key,建议元素数量小于1000
2.BigKey的危害
-
网络阻塞
对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致redis实例,乃至所在物理机变慢
例如一个key的大小5MB,并发20次请求,那么就是100MB的带宽。如果服务器只有100MB的带宽,那么就是全部占用了。
-
数据倾斜
BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡 -
Redis阻塞
对元素较多的hash、list、zset等做运算会比较耗时,从而主线程阻塞 -
CPU压力
对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其他应用
3.如何发现BigKey
-
redis-cli bigkeys
利用Redis-cli提供的–bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的TOP1的bigkey。
缺点:
只能看到第一名,有可能第一名并不是bigkey。也有可能第一第二第三都是bigkey。 -
scan扫描
自己编程,利用
scan
扫描Redis中的所有key,利用strlen和hlen等命令判断key的长度。(不建议使用memory usage,因为是主线程操作)
1.并不是主线程去操作
2.并且用的是迭代器逐步扫描
3.每次扫描一小部分 -
第三方工具
利用第三方工具,如Redis-Rdb-Tools分析RDB快照文件,全面分析内存使用情况 -
网络监控
自定义工具,监控进出Redis的网络数据,超出预警值时主动告警
4.如何删除BigKey
BigKey由于
内存占用较多
,即使删除这样的key也需要耗时很长时间,导致Redis
主线程阻塞
,引发一系列问题。
-
Redis3.0及以下版本
如果是集合类型,则用(
扫描的方法
)遍历Bigkey的元素,先逐个删除子元素,最后删除BigKey -
Redis 4.0以后
Redis在4.0后提供了异步删除的命令:
unlik
3、恰当的数据类型
string类型存储了一个123,会占用48个字节。有很多源信息。
1.存储User对象
可以使用Hash类型。
-
hash的
entry数量小于500
时,底层使用ziplist,空间占用小,可以灵活访问对象的任意字段。 -
hash的
entry数量超过500
时,会使用哈希表,内存占用比较多。一百万数据,占用内存大小是62.23M
方法一:导致BigKey问题
可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会
导致BigKey问题
。`
方法二:
拆分为小的hash,将id/100作为key,将id%100作为field,这样每100个元素为一个hash
例如:有100万条数据,不拆分是1个哈希,拆分成1万个哈希。将100万数据放到1万个哈希里面,每个哈希只有100个数据。
内存占用:
二、批处理优化
单个命令执行的流程:
N次命令的响应时间=N次往返的网络传输耗时+N次Redis执行命令耗时。
- 测试案例一:每次执行一次set命令,十万条数据,大概需要44秒。
1、N条命令批处理执行
N次命令的响应时间=1次往返的网络传输耗时+N次Redis执行命令耗时。
-
测试案例二:批处理MSET命令,十万条数据,大概需要182毫秒
优点:速度最快,具有原子性
缺点:数据类型受限制
不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞。
2、Pipeline
MSET虽然可以批处理,但是却只能操作
部分数据类型
(String和Hash类型),因此如果有对复杂数据类型的批处理需要,建议使用Pipeline功能:
优点:数据类型不收限制
缺点:速度略微低于M操作,并不是原子操作。
- 测试案例三:批处理Pipeline,十万条数据,大概需要250毫秒
@Resource
private Jedis jedis;
@Test
void testPipeline() {
// 创建管道
Pipeline pipelined = jedis.pipelined();
// 获取当前时间
long l = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
//放入命令到管道
pipelined.set("test_key:"+i,"value_"+i);
if (i % 1000 == 0 ){
//每放入一千条命令,批量执行一次
pipelined.sync();
}
long l1 = System.currentTimeMillis();
System.out.println("time====>"+(l1-l));
}
}
3、集群下的批处理
如MSET或Pipeline这样的批处理需要再一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则会导致执行失败。
mset name jack age12 sex male
(error)CROSSLOT Keys in request don't hash to the same solt
# 翻译:在这次请求中是:跨域多次槽的key,没有办法去做hash。没有办法到一个槽内。
四种处理方式:
并行solt(推荐使用)
计算每个key的插槽,将存在相同插槽的key,存放在同一组中。从而分出多个组。 在通过多
线程的方式
同时执行各组的命令。
@Test
void testMSetInCluster(){
HashMap<String, String> map = new HashMap<>(3);
map.put("name","zhangsan");
map.put("age","21");
map.put("sex","male");
stringRedisTemplate.opsForValue().multiSet(map);
}
三、服务端优化
1、持久化配置
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:
-
用来做
缓存的Redis实例
尽量不要开启持久化功能。 - 建议关闭RDB持久化功能,使用AOF持久化。
-
利用脚本定期在
slave从节点
做RDB,实现数据备份。 - 设置合理的rewrite阈值,避免频繁的bgrewrite
超过上一次rewrite的大小的百分百。并且超过64兆
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
- 配置no-appendfsync-on-rewrite=yes,进制在rewrit期间做aof,避免因aof引起的阻塞
部署有关建议:
- Redis实例的物理机要预留足够内存,应对fork和rewrite
- 单个Redis实例内存上限不要太大,例如4G或者8G。可以加快fork速度、减少主从同步、数据迁移的压力。
- Redis实例不要和CPU密集型应用部署在一起。例如ES
- 不要与高硬盘负载应用一起部署。例如数据库、消息队列。
2、慢查询
慢查询:在redis执行时耗时超过某个阈值的命令。称为
慢查询
慢查询的阈值可以通过配置指定:
-
默认阈值是10毫秒,redis执行命令是微妙级别,建议是1毫秒。
slowlog-log-slower-than 10000
-
慢查询会放入慢查询日志中,日志的长度有上限,可以通过配置指定:建议长度是1000
slowlog-max-len 128
3、命令及安全配置
- Redis一定要设置密码
- 进制线上使用下面的命令:keys、flushall、flushdb、config set 等命令。可以利用rename-command禁用或者另起名字。
- bind:限制网卡,进制外网网卡访问
- 开启防火墙
- 不要使用Root账户启用Redis
- 尽量不是有默认的端口
四、集群最佳实践
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题。
- 集群完整性问题
- 集群带宽问题
- 数据倾斜问题。hash_tag
- 客户端性能问题
- 命令的集群兼容性问题
- 集群下不支持lua和事务问题。
所以要不要使用集群?
单体的Redis(主从Redis)已经达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。
1、集群完整性问题
集群要不要全部覆盖。也就说插槽数量一个都不能少。当有插槽不能使用时,整个redis集群都不可用。默认是开启的。
cluster-require-full-coverage yes
为了保证高可用性,建议将此配置改为 false
2、集群带宽问题
集群节点之间会不断的互相ping来确定集群中其他节点的状态。每次ping携带的信息至少包括:
- 插槽信息
- 集群状态信息
集群中节点越多,集群状态信息数据量也越大,10各节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。
解决途径:
- 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群
- 避免在单个物理机中运行多个redis实例
- 配置合适的cluster-nodetimeout值。 集群节点客观下线的超时时间。15秒