一)优雅的key结构:
redis中的key虽然可以自定义,但是最好遵循下面的几个最佳实践约定:
1)遵循基本格式:业务名称:数据名字:ID;
2)长度不要超过44字节,key所占的字节数越小,占用空间越小,越短越好;
3)不要包含特殊字符;
例如当设计登录业务的时候,保存用户信息,其实可以这样:login:user:10
优点:
1)可读性强
2)避免key的冲突:没有直接使用用户的ID,防止不同的业务之间都使用用户的ID造成key冲突,加上业务名字可以避免业务冲突;
3)方便Key的管理,使用冒号分离,在redis中形成层级结构;
4)更节省内存:key是String类型,底层编码包括int,embstr和raw三种,embstr在小于44字节的时候进行使用,采用连续的内存空间,所占用的内存更小;
查看底层编码:object encoding+key的名字;
int编码:当key是数值的时候进行存储,占用内存空间是非常小的
embstr:是一段连续空间,编码会更加紧凑,占用空间比较小,想要使用ambstr的编码格式,key就需要小于44字节
raw:空间不是连续的,访问性能下降,内存占用会更高,还会产生内存碎片
二)BigKey
Big Key就是某个key对应的value很大,占用的redis空间很大,本质上是大value问题,key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大;
在Redis中,一个字符串最大是512MB,一个二级数据结构比如说哈希,list,set,zset可以存储大约40亿个元素,但是实际上如果出现以下几种情况,就被认为是bigKey
1)字符串类型:它的big体现在单个value置很大,一般认为超过10KB就是BigKey
2)非字符串类型:哈希,列表,集合它们的bigKey取决于元素个数太多
3)memory usage+Key的名字,查看key的大小,但是消耗的CPU资源比较多
针对于String类型来说:strlen key查看key的长度
针对于list集合来说:llen key,查看list集合的长度
BigKey产生的场景:
1)Redis数据结构使用不恰当:将redis用在并不适合的场景下,造成Key的value过大,比如使用String类型的key存放大体积的二进制数据文件
2)未能及时清理垃圾数据:没有针对无效数据做定期的清理,造成比如Hash类型中的Key成员在不断的增加,就是一直向value中塞数据,但是没有删除机制,value只会越来越大
3)明星、网红的粉丝列表、某条热点新闻的评论列表
假设使用List数据结构保存某个明星/网红的粉丝,或者保存热点新闻的评论列表,因为粉丝数量巨大,热点新闻因为点击率、评论数会很多,这样List集合中存放的元素就会很多,可能导致value过大,进而产生Big Key问题
在Redis配置文件中禁用一些命令:这样以后在对应的客户端就无法使用了
三)BigKey的危害:
有时也可以考虑对 Bigkey 进行拆分,具体方法如下:对于 string 类型的 Bigkey,可以考虑拆分成多个 key-value。对于 hash 或者 list 类型,可以考虑拆分成多个 hash 或者 list
1)网络阻塞:针对于BigKey进行网络请求的时候,假设每秒钟对这个bigKey的请求达到了20个,少量的并发就很有可能导致带宽被占满,导致Redis实例乃至所在的物理机变慢,假设这太物理机除了部署redis以外还部署了一些其他的应用,那么会导致其他网络请求被阻塞
Big Key对应的value较大,我们对其进行读写的时候,需要耗费较长的时间,这样就可能阻塞后续的请求处理,Redis的核心线程是单线程,单线程中请求任务的处理是串行的,前面的任务完不成,后面的任务就处理不了
2)数据倾斜:BigKey所在的Redis实例内存利用率(数据量)远远超过其他实例,导致无法使数据分片的内存资源达到均衡;
3)Redis阻塞:针对元素比较多的哈希,list,zset等做运算的时候会耗时较久,redis还是单线程的,是主线程被阻塞,读取单value较大时会占用服务器网卡较多带宽,自身变慢的同时可能会影响该服务器上的其他Redis实例或者应用
4)CPU压力过高:针对BigKey的数据做序列化和反序列化会导致CPU的使用率飙升,影响Redis和本机其他实例的使用
5)内存溢出:读取Big Key耗费的内存比正常Key会有所增大,如果不断变大,可能会引发OOM,达到redis的最大内存maxmemory设置值引发写阻塞或重要Key被逐出
四)排查BigKey:
1)redis-cli --bigKeys,利用redis-cli提供的bigKeys参数,可以遍历分析所有的key,并返回Key的整体统计信息和每一种数据的Top1的big key;
2)scan扫描,利用scan扫描Redis中的所有key,利用strlen或者是hlen等命令来判断key的长度,此处不建议使用memory usage或者是keys *;
2.1)第1个参数是游标,也就是你从第几个位置开始进行扫描,最终redis会返回一个游标,下一次会继续从这个位置进行扫描;
2.2)第二个参数你扫描哪一种类型,第三个参数是你要扫描几个
@Controller public class UserController { @Autowired private Jedis jedis; private final int StringMax=10*1024; private final int HashMax=500; @RequestMapping("/Java100") @ResponseBody public void scan(){ System.out.println("1"); long MaxLen=0; long KeyLen=0; String cur="0"; do{ ScanResult
result= jedis.scan(cur); //1.记录游标 cur=result.getCursor(); //2.获取到扫描的key List list=result.getResult(); if(list==null||list.isEmpty()){ break; } for(String key:list){ System.out.println(key); switch(jedis.type(key)){ case "string": KeyLen=jedis.strlen(key); MaxLen=StringMax; break; case "hash": KeyLen= jedis.hlen(key); MaxLen=HashMax; break; case "list": KeyLen= jedis.llen(key); MaxLen=HashMax; break; case "set": KeyLen= jedis.scard(key); MaxLen=HashMax; break; case "zset": KeyLen= jedis.zcard(key); MaxLen=HashMax; break; } if(KeyLen>=MaxLen){ System.out.println("当前key是一个bigKey"); } } } while(!cur.equals("0")); } } 3)debug object key
根据传入的对象(Key的名称)来对Key进行分析并返回大量数据,其中serializedlength的值为该Key的序列化长度,需要注意的是,Key的序列化长度并不等同于它在内存空间中的真实长度,此外,debug object属于调试命令,运行代价较大,并且在其运行时,进入Redis的其余请求将会被阻塞直到其执行完毕,并且每次只能查找单个key的信息,官方不推荐使用
4)这种方式是在redis实例上执行bgsave,bgsave会触发redis的快照备份,生成rdb持久化文件,然后对dump出来的rdb文件进行分析,找到其中的大key
1)针对 BigKey 进行拆分
通过将 BigKey 拆分成多个小 Key 的键值对,并且拆分后的对应的 value 大小和拆分成的成员数量比较合理,然后进行存储即可,在获取的时候通过 get 不同的 key 或是用 mget 批量获取存储的键值对
2)清理无效的数据
这个主要是针对像是 list 和 set 这种类型,在使用的过程中,list 和 set 中对应的内容不断增加,但是由于之前存储的已经是无效的了,需要定时的对 list 和 set 进行清理。
3)压缩对应的 BigKey 的 value
可以通过序列化或者压缩的方法对 value 进行压缩,是其变为较小的 value,但是如果压缩之后如果对应的 value 还是特别大的话,就需要使用拆分的方法进行解决了。
4)监控 Redis 中内存,带宽,增长率
通过监控系统,监控 redis 中的内存占用大小和网络带宽的占用大小,以及固定时间内的内存占用增长率,当超过设定的阈值的时候,进行报警通知处理
5)删除BigKey
如何删除BigKey
因为BigKey所占用的内存比较多,那么即便即使删除这样的Key也是很消耗时间的,这样还会导致Redis的主线程阻塞,从而引发一系列问题
1)redis3.0以下版本,比如说hash类型,先借用hdel来删除一个一个的子元素,最后删除key,如果是集合类型,那么先遍历BigKey的子元素,先逐个删除子元素,最后在删除BigKey,还是不能使用keys *,还是使用scan,还是指定key返回一个游标;
2)redis4.0以后提供了异步删除的命令,就是unlink命令
1)针对于String类型来说,一般使用del,如果过于庞大可以使用unlink;
2)针对于hash类型来说,可以使用hscan来每一次获取少量field-value,再使用hdel来删除每一个field;
public class DelHash { public static void start(String host,int port,String password,String bigHashKey){ Jedis jedis=new Jedis(host,port); jedis.auth(password); //1.先进行记录每一次返回的BigHashKey中的field的个数 ScanParams params=new ScanParams().count(10); //2.初始游标设置成0 String cursor="0"; do{ //3.先得到所有的bigHashKey中的field ScanResult
> scanResult=jedis.hscan(bigHashKey,cursor,params); //4.遍历Map集合进行删除此次得到的所有field值和value List > entryList=scanResult.getResult(); if(entryList!=null&&!entryList.isEmpty()){ for(Map.Entry entry:entryList){ jedis.hdel(bigHashKey,entry.getKey()); } } }while(!"0".equals(cursor)); //3.删除bigHashKey中的所有field和value值之后,再来进行删除对应的key jedis.del(bigHashKey); } public static void main(String[] args) { start("124.71.136.248",6379,"12503487","student"); } } 3)针对于list结构来说,可以使用ltrim渐进式删除,直到全部删除完成为止,这个命令是对列表只保留指定区间内的元素,不在区间内的元素会全部被删除,下标0表示列表中的第一个元素,下标1代表列表中的第一个元素,以此类推,也可以使用负数下标,-1代表列表中的最后一个元素,-2代表列表中的倒数第二个元素,以此类推:
public static void start(String host,int port,String password,String bigListKey){ Jedis jedis=new Jedis(host,port); jedis.auth(password); long llen= jedis.llen(bigListKey); int counter=0; int left=100; while(counter
4)针对于set集合来说可以使用sscan命令每一次获取部分元素,再使用zrem来删除部分元素
5)对于SortedSet来说,可以先使用zscan来获取部分元素,最后使用zremrangebyrank来删除每一个元素
恰当的数据类型1:
第一种字符串对象存储方式,修改对应的字段值很不方便,新增字段值也很不方便
第二种转化成更大的key进行存储
1)占用空间比较大,有几个字段,key都是user:1:name,存储了很多相同的key,浪费空间
2)想要获取用户的所有信息比较麻烦
第三种的value又是一个键值对,但是Hash结构的Entry不要超过1000
恰当的数据类型2:
1)当hash的entry数量超过500的时候,会使用哈希表而不是使用ZipList,这样会使内存占用比较多
2)可以通过修改hash-max-ziplist-entries配置entry上限,但是如果entry过多就会导致bigKey问题,但是还是可以通过config set hash-max-ziplist-entries 1000来进行修改
解决方案:
1)转化成String类型进行存储:解决了BigKey的问题
1.1)String类型底层结构没有太多优化,内存占用比较多
1.2)想要批量获取这些数据比较麻烦
2)拆分成小的hash,将id/100作为key,将id%100作为field,这样每100个元素为一个hash,解决了BigKey的问题;
批处理优化:
一次命令的执行时间:1次往返的网络传输耗时+1次redis的执行命令的耗时,网络传输是非常耗时的,但是也不需要在一次批处理中传输太多命令,否则单次命令占用网络带宽过多导致网络阻塞
mset虽然可以进行批处理,但是却只能操作部分数据类型,如果对有复杂数据类型的批处理需要,需要使用管道来进行处理
1)m操作是redis原生提供的操作,这个操作的执行是原子性的,一次性会直接全部执行完成,中间过程中不会有其他命令来进行插队
2)但是管道是直接讲这些命令发送到管道里面,但是不会一起执行,因为管道里面命令的传输是有先后顺序的,在命令传输的过程中也是可以有其他客户端来给redis传输命令的,管道的命令会进入到redis队列中排队,redis的线程会依次取出这些命令进行执行,如果有其他命令来插队,那么实际执行时长可能会比与其执行时长要长
集群下的批处理:
批处理是在一次连接中把所有的请求全部干掉,如mset或者是Pipeline这样的批处理需要在一次请求中携带多条命令,而如果此时Redis是一个集群,那么批处理得key必须落到同一个插槽中,否则就会执行失败;
mset、mget只支持在同一个槽内的key,因为不在一个槽内的key可能存在于不同节点上
服务器端的优化:
1)用来做缓存的redis尽量不要使用持久化功能;
2)建议关闭RDB持久化功能,使用AOF持久化功能;
3)利用脚本定期在slave节点做RDB,来实现数据备份;
4)设置合理的rewrite阈值,避免频繁的重写
5)配置no-appendfsyc-on-rewrite:yes,禁止在AOF重写的过程中做AOF持久化,避免因为AOF引起的阻塞
不建议redis和做大量CPU密集型计算的应用和高磁盘负载的应用部署到同一台服务器上
慢查询:
下面的例子就是假设执行了keys *命令,接下来就可以通过showlog get 1来查询对应的命令
这个配置是把config命令替换成后面的命令
redis内存配置:
当redis内存不足的时候,可能会导致key频繁被删除,响应时间变长等问题,当redis的内存使用频率超过90%以上就需要我们警惕,并应该快速定位到内存占用的原因
1)数据内存,这是redis最主要的部分,用来存储redis的键值信息,主要的问题是BigKey问题,内存碎片的问题,是在内存分配的过程中产生的,当向redis中存储一部分的数据的时候,假设存储10个字节,就会分配16个字节,多分配给的6个字节就是内存碎片,要想解决内存碎片问题,就可以解决内存碎片的问题;
2)进程内存:Redis主进程本身运行肯定需要占用内存,比如说代码和常量池等等,这部分内存大约几兆,在大多数生产环境中和Redis数据占用的内存相比可以忽略
3)缓冲区内存:这部分内存会受到客户端的操作而受影响
info memory
memoery stats
1)复制缓冲区配置的尽量大些,防止进行频繁全量同步;
2)AOF缓冲区:每一秒钟执行一次刷盘策略,刷盘就从AOF缓冲区中刷,AOF缓冲区不会是内存有较大的波动,况且AOF缓冲区中存放的都是用户的命令;
3)客户端缓冲区(所有和redis建立连接的客户端)的输入缓冲区,如果主线程被阻塞(慢查询导致),数据量过多命令过多直接堆满输入缓冲区超过1G,那么redis会直接主动和客户端断开连接,一般情况不会出现问题,只要防止慢查询即可,所以不用担心输入缓冲区溢出的问题
4)输出缓冲区满了:redis处理完数据之后,返回的数据量太多导致客户端无法进行处理
通过info client或者client list来查看客户端信息clusterdown the cluster is down