当前位置: 首页 > 后端技术 > Java

Redis最佳实践(上)

时间:2023-04-01 20:30:40 Java

介绍虽然redis是一个非常优秀的NoSQL数据库,但更重要的是,作为用户,我们应该学会如何在不同的场景下更好地使用它,更好地利用它。这很值得。主要可以从这四个方面进行优化:Rediskey-value设计、批处理优化、服务端优化、集群配置优化1.Redis慢查询日志利用Redis提供了慢日志命令的统计功能,它记录了哪些命令在执行时需要很长时间。查看Redis慢日志前,需要设置慢日志阈值。例如设置慢日志的阈值为5毫秒,保留最近500条慢日志记录:#如果命令执行时间超过5毫秒,则记录慢日志CONFIGSETslowlog-log-slower-than5000#Onlykeepthelast500slowlogs设置CONFIGSETslowlog-max-len500后,如果操作时间超过5毫秒,所有执行的命令都会被Redis记录下来。此时可以执行如下命令查询最近记录的慢日志:slowloglen:查询慢查询日志长度slowlogget[n]:读取n条慢查询日志slowlogreset:清空慢查询列表127.0.0.0.00.1:6379>SLOWLOGget51)1)(integer)12691#SlowlogID2)(integer)16027264377#Executiontimestamp3)(integer)6989#Executiontime(microseconds)4)1)"LRANGE"#SpecificExecutedcommands和参数2)"goods_list:100"3)"0"4)"-1"2)1)(integer)126922)(integer)160282542473)(integer)54544)1)"GET"2)"good_info:100”可能会导致操作延迟:经常使用复杂度在O(N)以上的命令,比如SORT、SUNION、ZUNIONSTORE聚合命令,使用O(N)复杂命令会消耗更多的CPU资源,但是N的值很大,Redis一次需要返回给客户端的数据太多,更多的时间花在了数据协议的组装和网络传输上。您可以使用以下方法来优化您的业务:尽量不要使用复杂度高的O(N)命令。对于数据聚合操作,在客户端执行O(N)条命令,保证N尽可能小(推荐N<=300),每次获取尽可能少的数据,以便Redis及时处理和返回2.Redis键值设计2.1优雅的键结构虽然Redis的键可以自定义,但最好遵循以下最佳实践约定:遵循基本格式:[业务名]:[数据名]:[id]长度不不超过44个字节且不包含特殊字符。例如:我们的登录业务保存用户信息,它的key可以设计成如下格式:这样设计好处:可读性强,避免key冲突,方便管理,节省内存:key为string类型,底层编码包括int,embstr和原始。embstr用在44字节以内,使用连续的内存空间,占用内存少。当字节数大于44字节时,会转为原始模式存储。在raw模式下,内存空间是不连续的,而是用一个指针指向另一个内存空间,SDS的内容存放在这个空间,这样如果空间不连续,访问时会影响性能,并且可能会出现内存碎片。2.2拒绝BigKey2.2.1什么是BigKey?.同样,删除这个key时,释放内存也是需要时间的。我们一般称这种类型的密钥为bigkey。BigKey通常根据Key的大小和Key中的成员数量综合判断。例如:Key自身数据量过大:String类型的Key,值为5。MBKey中的成员数过大:ZSET类型的Key,其成员数为10000。Key的成员数据量过大:一个Hash类型的Key只有1000个成员,但是这些成员的总值为100MB。那么如何判断元素呢绒的大小呢?Redis也给我们提供了命令MEMORYUSAGEKEY的推荐值:单个key的值小于10KB。对于集合类型的key,建议元素个数小于1000。2.2.2BigKey危险网络阻塞在对BigKey进行读请求时,少量的QPS可能会导致带宽占用,导致Redis实例甚至物理机都会变慢。数据是倾斜的。BigKey所在Redis实例内存占用远高于其他实例,无法平衡数据分片的内存资源。Redisblockshash,List,zset等会花很长时间做计算,主线程会阻塞。CPU压力会导致BigKey数据序列化和反序列化时CPU使用率激增,影响Redis实例和其他本地应用程序。2.2.3如何找出BigKeyredis-cli--bigkeys-a`Password`使用redis-cli提供的--bigkeys参数,可以遍历分析所有的key,返回Key和Top1big的整体统计信息每种数据类型的键。该命令的原理是Redis内部执行SCAN命令遍历整个实例中的所有key,然后针对key的类型执行STRLEN、LLEN、HLEN、SCARD、ZCARD命令获取String的长度类型和容器类型(List、Hash、Set、ZSet)元素的数量。这里需要提醒大家的是,在执行这条命令的时候,需要注意两个问题:对在线实例进行bigkey扫描时,Redis的OPS会突然增加。为了减少扫描过程中对Redis的影响,最好控制扫描的频率,可以指定-i参数,该参数表示扫描过程中每次扫描后休息的时间间隔,单位为秒。在扫描结果中,对于容器类型(List、Hash、Set、ZSet)的key,只能扫描key。元素最多的键。但是一个key有很多元素并不一定代表它占用的内存很多。需要根据业务情况进一步评估内存使用情况。自己扫描cursorcountn程序,使用scan扫描Redis中所有的key,使用strlen,hlen等命令判断key的长度(这里不推荐使用MEMORYUSAGE)。调用scan命令后,每次会返回2个元素。第一个是下一次迭代的光标。第一个游标会被设置为0,当最后一次扫描返回游标等于0时,表示整个扫描遍历结束,第二次返回的是List,一个匹配键的数组publicclassJedisTest{privateJedis绝地武士;@BeforeEachvoidsetUp(){//1.建立连接//jedis=newJedis("192.168.150.101",6379);jedis=JedisConnectionFactory.getJedis();//2.设置密码jedis.auth("123321");//3.选择库jedis.select(0);}finalstaticintSTR_MAX_LEN=10*1024;最终静态intHASH_MAX_LEN=500;@TestvoidtestScan(){intmaxLen=0;长len=0;字符串游标="0";do{//扫描并获取键的一部分ScanResultresult=jedis.scan(cursor);//记录游标cursor=result.getCursor();清单<斯特里ng>list=result.getResult();如果(list==null||list.isEmpty()){break;}//遍历for(Stringkey:list){//判定key的类型Stringtype=jedis.type(key);switch(type){case"string":len=jedis.strlen(key);maxLen=STR_MAX_LEN;休息;案例“哈希”:len=jedis.hlen(key);maxLen=HASH_MAX_LEN;休息;案例“列表”:len=jedis.llen(key);maxLen=HASH_MAX_LEN;休息;案例“设置”:len=jedis.scard(key);maxLen=HASH_MAX_LEN;休息;案件&quot;zset":len=jedis.zcard(key);maxLen=HASH_MAX_LEN;break;default:break;}if(len>=maxLen){System.out.printf("Foundbigkey:%s,type:%s,长度或大小:%d%n",key,type,len);}}}while(!cursor.equals("0"));}@AfterEachvoidtearDown(){if(jedis!=null){jedis.close();}}}第三方工具利用Redis-Rdb-Tools等第三方工具分析RDB快照文件,综合分析内存使用情况https://github.com/sripathikr...网络监控自定义工具,监控进出Redis的网络数据,超过警告值时主动报警,一般阿里云搭建的云服务器都有相关的监控页面,如果你的Redis版本在4.0以上,使用UNLINK命令而不是DEL,这个命令可以在后台线程执行释放key内存的操作,从而减少对Redis的影响,如果你使用的是6.0以上的Redis,可以启用lazy-free机制(lazyfree-lazy-user-del=yes),执行DEL命令时,释放的内存也会在后台线程执行bigkey,很多场景下会出现性能问题比如在shardingcluster模式下,bigkey也会对数据迁移产生性能影响,后面讲到的数据过期、数据淘汰、透明大页都会受到bigkey的影响。所以,即使是reids6.0之后,仍然不建议使用BigKey2.3,总结一下Key固定格式的最佳实践:[业务名称]:[数据名称]:[id]足够短:不超过44字节不包含特殊字符Value最佳实践:合理拆分数据,拒绝BigKey,选择合适的数据结构,Hash结构的条目数不超过1000。设置合理的超时时间。N条命令的执行流程Redis处理指令的速度非常快,主要耗费的时间是网络传输。于是很容易想到将多条指令批量传输到redis3.1.2。MSetRedis提供了很多类似Mxxx的命令,可以批量插入数据。例如:msethmset使用mset批量插入10万条数据@TestvoidtestMxx(){String[]arr=newString[2000];诠释j;longb=System.currentTimeMillis();对于(inti=1;i<=100000;i++){j=(i%1000)<<1;arr[j]="test:key_"+i;arr[j+1]="value_"+i;如果(j==0){jedis.mset(arr);}}longe=System.currentTimeMillis();.out.println("time:"+(e-b));}3.1.3PipelineMSET虽然可以批量处理,但是只能对部分数据类型进行操作。因此,如果有复杂数据类型的批处理需求,建议使用Pipeline@TestvoidtestPipeline(){//创建管道Pipelinepipeline=jedis.pipelined();longb=System.currentTimeMillis();for(inti=1;i<=100000;i++){//将命令放入管道pipeline.set("test:key_"+i,"value_"+i);if(i%1000==0){//每输入1000条命令,分批执行pipeline.sync();}}长的e=System.currentTimeMillis();System.out.println("time:"+(e-b));}3.2集群下的批处理MSET或Pipeline等批处理需要在一次请求中携带多条命令,此时如果Redis是一个cluster,批处理命令的多个key必须落在一个slot中,否则会导致执行失败。你可以考虑一下。这时候可能会一次性插入很多条数据,而这些数据不一定都落在同一个节点上,这样就会出错。这时候,我们可以找到4种解法。第一种方案:串行执行,所以这个方法没有意义。当然实现起来很简单,缺点是时间太长。第二种解决方案:串行插槽。简单来说,在执行之前,客户端先计算出对应key的slot。同一个slot的key放在一组,不同slot的key放在不同的group。然后对每个组执行流水线的批处理,他可以串行执行每个组的命令。这种方法比第一种方法耗时少,缺点是相对复杂,所以这个方案还是需要优化第三种方案:parallelslot。相比方案二,分组完成后串行执行。第三种方案变成了每条命令并行执行,所以耗时很短。但是实现起来也比较复杂。第四种:hash_tag,redis在计算key的slot时,实际上是根据key的有效部分计算的。这样就可以一次处理所有的key。这种方式耗时最短,实现简单,但是如果通过操作key的有效部分,所有的key都会落在一个节点上,造成数据倾斜,所以我们推荐使用第三种方式。3.2.1串行化执行代码实现publicclassJedisClusterTest{privateJedisClusterjedisCluster;@BeforeEachvoidsetUp(){//配置连接池JedisPoolConfigpoolConfig=newJedisPoolConfig();poolConfig.setMaxTotal(8);poolConfig.setMaxIdle(8);poolConfig.setMinIdle(0);poolConfig.setMaxWaitMillis(1000);HashSetnodes=newHashSet<>();nodes.add(newHostAndPort("192.168.150.101",7001));nodes.add(newHostAndPort("192.168.150.101",7002));nodes.add(newHostAndPort("192.168.150.101",7003));nodes.add(newHostAndPort("192.168.150.101",8001));nodes.add(newHostAndPort("192.168.150.101",8002));nodes.add(newHostAndPort("192.168.150.101",8003));jedisCluster=newJedisCluster(nodes,poolConfig);}@TestvoidtestMSet(){jedisCluster.mset("name","Jack","age","21","sex","male");}@TestvoidtestMSet2(){Mapmap=newHashMap<>(3);map.put("name","Jack");map.put("age","21");map.put("sex","Male");//分组地图数据按照同一个slot放入group//key为slot,value为一组Map>>result=map.entrySet().stream().collect(Collectors.groupingBy(entry->ClusterSlotHashUtil.calculateSlot(entry.getKey())));//串行执行mset逻辑for(List>list:result.values()){String[]arr=newString[list.size()*2];整数j=0;对于(inti=0;ie=list.get(0);arr[j]=e.getKey();arr[j+1]=e.getValue();}jedisCluster.mset(arr);}}@AfterEachvoidtearDown(){if(jedisCluster!=null){jedisCluster.close();}}}3.2.2Spring集群环境批处理代码@TestvoidtestMSetInCluster(){Mapmap=newHashMap<>(3);map.put("姓名","玫瑰");map.put("年龄","21");map.put("性别","女");stringRedisTemplate.opsForValue().multiSet(map);Liststrings=stringRedisTemplate.opsForValue().multiGet(Arrays.asList("name","age","sex"));字符串。forEach(System.out::println);}如果本文对您有帮助,请关注点赞`,您的支持是我坚持创作的动力,转载请注明出处!