当前位置: 首页 > 科技观察

21个Redis致命雷区,赶紧救命!

时间:2023-03-20 01:44:31 科技观察

最近在学习Redis,看了阿里的redis开发规范,还有Redis开发运维这本书。分为使用规范、坑命令、项目实际操作、运维配置四个方向,梳理了使用Redis的21个注意事项。希望对大家有所帮助,一起学习吧。一、Redis使用规范1、关键规范要点在设计Redis的键时,需要注意以下几点:以业务名称作为键的前缀,并用冒号隔开,防止键冲突被覆盖。例如,live:rank:1;为确保密钥语义清晰,密钥长度应小于30个字符;key不能包含特殊字符,例如空格、换行符、单引号和双引号以及其他转义字符;Redis的key尽量设置ttl,保证不用的key能被及时清除或淘汰。2.value规范的要点Redis的value是不能任意设置的。“第一点”,如果存储了大量的bigKey,就会出现问题,导致查询缓慢,内存增长过快等。如果是String类型,单个值的大小控制在10k以内;如果是hash、list、set、zset类型,元素个数一般不超过5000个。“第二点”是选择合适的数据类型。很多小伙伴只使用Redis的String类型,就是set和get。事实上,Redis提供了“丰富的数据结构类型”。在某些业务场景下,更适合hash、zset等其他数据结果。《反例:》setuser:666:namejaysetuser:666:age18《正例》hmsetuser:666namejayage183,设置key的过期时间,注意不同业务的key,尽量分散过期时间,因为Redis数据是存储在内存中,内存资源很宝贵。我们一般使用Redis作为缓存,而不是数据库,所以key的生命周期不能太长。所以一般建议你的key使用“expire来设置过期时间”。如果大量key在某个时间点集体过期,Redis可能会在那个时间点卡住甚至出现“缓存雪崩”现象。因此,一般情况下,不同业务的密钥过期时间应该是分散的。有时候,对于同一个业务,也可以在时间上加上一个随机值,来分散过期时间。4、建议使用批量操作,提高效率。我们每天写SQL的时候,都知道批量操作会更高效。一次更新50个项目比循环50次一次更新一个项目更有效。其实对于Redis的操作命令也是一样的。Redis客户端执行一个命令可以分为四个过程:1.发送命令->2.命令排队->3.命令执行->4.返回结果。1和4称为RRT(命令执行往返时间)。Redis提供了“批量操作命令,如mget、mset”等,可以有效的节省RRT。但是大部分命令不支持批处理操作,比如hgetall,mhgetall是不存在的。“流水线”可以解决这个问题。什么是流水线?它可以组装一组Redis命令,通过RTT传输给Redis,然后将这组Redis命令的执行结果按顺序返回给客户端。我们先看一下没有使用PipelineCommand模型执行的n项:使用Pipeline执行n次命令,整个过程需要1个RTT。模型如下:二、Redis中那些有陷阱的命令1、谨慎使用O(n)复杂度的命令,比如hgetall、smember、lrange等,因为Redis是单线程执行命令。hgetall、smember等命令的时间复杂度为O(n)。当n继续增加时,RedisCPU会持续飙升,阻塞其他命令的执行。hgetall、smember、lrange等命令不一定不能用。需要综合评估数据量,明确n的取值,再做决策。比如hgetall,如果hash元素n多,可以优先使用“hscan”。2、谨慎使用Redis的monitor命令。RedisMonitor命令用于实时打印出Redis服务器接收到的命令。如果我们想知道客户端对redis服务器做了什么命令,可以使用Monitor命令查看,但一般只是“Debugging”而已,生产中尽量不要使用!因为“monitor命令可能会导致redis的内存持续飙升”。监控模型是紫红色的,它会输出在Redis服务器上执行的所有命令。一般来说,Redis服务器的QPS是很高的,也就是说,如果执行monitor命令,Redis服务器会在Monitor客户端的输出缓冲区中有大量的“库存”,也占用了大量的资源Redis内存量。3.生产环境不能使用keys命令。RedisKeys命令用于查找与给定模式匹配的所有键。如果要查看Redis中某类key的个数,很多朋友会想到使用keys命令,如下:keyskeyprefix*但是redis的key是通过遍历来匹配的,复杂度为O(n).数据库数据越多,越慢。我们知道redis是单线程的。如果数据很多,keys命令会导致redis线程阻塞,在线服务也会停止。在命令执行之前,服务不会恢复。Therefore,"generallyinaproductionenvironment,donotusethekeyscommand".官方文档也有声明:Warning:considerKEYSasacommandthatshouldonlybeusedinproductionenvironmentswithextremecare.Itmayruinperformancewhenitisexecutedagainstlargedatabases.Thiscommandisintendedfordebuggingandspecialoperations,suchaschangingyourkeyspacelayout.Don'tuseKEYSinyourregularapplicationcode.Ifyou'relookingforawaytofindkeysinasubsetofyourkeyspace,considerusingsets.其实,可以使用scan指令,它它的复杂度也是O(n),但是它是通过游标一步步执行的,“不会阻塞redis线程”;但会有一定的“重复概率”,需要“客户端去重一次”。scan支持增量迭代命令,增量迭代命令也有缺点:比如使用SMEMBERS命令可以返回集合key中当前包含的所有元素,但是对于增量迭代命令如SCAN,因为在增量迭代时inKeys可能会被修改通过键,因此增量迭代命令只能提供有关返回元素的有限保证。4、禁止使用flushall和flushdbFlushall命令清除整个Redis服务器的数据(删除所有数据库中的所有key)。Flushdb命令用于清除当前数据库中的所有键。这两个命令是原子的,不会终止执行。一旦执行,它不会失败。5.注意使用del命令删除key。你通常使用什么命令?是直接del吗?如果删除一个key,直接用del命令当然没问题。但是,大家有没有想过del的时间复杂度呢?分情况来讨论:如果删除一个String类型的key,时间复杂度是O(1),“直接del就可以”;如果删除一个List/Hash/Set/ZSet类型,其复杂度为O(n),n代表元素个数。所以如果删除一个List/Hash/Set/ZSet类型的key,元素越多越慢。“当n很大时,要特别注意”,会阻塞主线程。那么,如果没有使用del,我们应该如何删除呢?如果是List类型,可以执行lpop或者rpop,直到所有元素都被删除;如果是Hash/Set/ZSet类型,可以先执行hscan/sscan/scan查询,再执行hdel/srem/zrem依次删除每个元素。6.避免使用SORT、SINTER等复杂度高的命令。执行复杂的命令会消耗更多的CPU资源,阻塞主线程。所以应该避免执行SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE等聚合命令,一般建议放在客户端执行。三、项目实战避坑操作1、使用分布式锁的注意事项分布式锁实际上是一种控制分布式系统中不同进程共同访问共享资源的锁的实现。秒下单、抢红包等业务场景需要分布式锁。我们经常使用Redis作为分布式锁。主要有以下几点需要注意:3.1.1SETNX+EXPIRE两条命令分开写(典型错误实现示例)if(jedis.setnx(key_resource_id,lock_value)==1){//添加Lockexpire(key_resource_id,100);//设置过期时间try{dosomething//业务请求}catch(){}finally{jedis.del(key_resource_id);//释放锁}}如果执行了setnx锁,即将执行expire时设置过期时间,进程崩溃或者需要重启维护,那么锁就会“永生”,“其他线程永远无法获取锁”,所以一般分布式锁不能这样实现.3.1.2SETNX+value为过期时间(有些小伙伴是这样实现的,有坑)longexpires=System.currentTimeMillis()+expireTime;//系统时间+设置过期时间StringexpiresStr=String.valueOf(expires);//如果当前锁不存在,返回锁成功if(jedis.setnx(key_resource_id,expiresStr)==1){returntrue;}//如果锁已经存在,获取锁的过期时间StringcurrentValueStr=jedis.get(key_resource_id);//如果获取到的过期时间小于系统当前时间,则表示已经过期获取上一个锁的过期时间,并设置当前锁的过期时间(不知道redis的getSet命令的可以去官网看看)StringoldValueStr=jedis.getSet(key_resource_id,expiresStr);if(oldValueStr!=null&&oldValueStr.equals(currentValueStr)){//考虑到多线程并发的情况,只有一个线程的设置值与当前值相同,才可以加锁returntrue;}}//其他情况会返回锁失败returnfalse;}该方案的“缺点”:Expired时间由客户端自己产生。在分布式环境中,每个客户端的时间必须同步;持有人的唯一标识不保存,可能被其他客户端释放/解锁;当锁过期,多个客户端同时请求到来时,执行jedis.getSet()。最终只能成功锁定一个client,但是client锁的过期时间可能会被其他client覆盖。3.1.3:SET扩展命令(SETEXPXNX)(注意可能存在的问题)if(jedis.set(key_resource_id,lock_value,"NX","EX",100s)==1){//locktry{dosomething//业务处理}catch(){}finally{jedis.del(key_resource_id);//释放锁}}这个方案可能还是有问题:锁过期释放,业务还没有执行;锁被其他人使用Thread被误删。3.1.4SETEXPXNX+验证唯一随机值,然后删除(误删问题解决,但仍然存在锁过期和业务执行未完成的问题)if(jedis.set(key_resource_id,uni_request_id,"NX","EX",100s)==1){//加锁try{dosomething//业务处理}catch(){}finally{//判断是不是当前线程加的锁,如果是则释放(uni_request_id.equals(jedis.get(key_resource_id))){jedis.del(lockKey);//释放锁}}}这里判断锁是否是当前线程加的,不是原子操作释放锁。如果调用jedis.del()释放锁,这个锁可能已经不属于当前客户端了,别人加的锁就会被释放。一般使用lua脚本代替。lua脚本如下:ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0end;3.1.5Redisson框架+Redlock算法解决锁过期释放,业务执行未完成+单机问题。Redisson使用看门狗来解决锁过期释放问题,以及业务执行未完成的问题。Redisson的示意图如下:上面的分布式锁还有一个单机问题:如果线程一在Redis的master节点上获取到锁,但是锁上的key还没有同步到slave节点.此时,如果主节点出现故障,一个从节点将升级为主节点。线程2可以获取同一个key的锁,但是线程1已经获取了锁,锁的安全性就失去了。对于单机问题,可以使用Redlock算法。有兴趣的朋友可以看看我的文章,有七种解决方法!浅谈Redis分布式锁的正确使用姿势2.缓存一致性注意事项如果是读请求,先读缓存再读数据库;如果是写请求,先更新数据库再写缓存;每次更新数据后,需要清空缓存;缓存一般需要设置一定的有效期;如果一致性要求高,可以使用biglog+MQ保证。有兴趣的朋友可以看看我的文章:并发环境下,应该先操作数据库还是先操作缓存?3、合理评估Redis的容量,避免因为频繁的set覆盖导致之前设置的过期时间失效。我们知道Redis所有的数据结构类型都可以设置过期时间。假设一个字符串设置了过期时间,如果你重新设置,之前的过期时间就失效了。RedissetKey源码如下:voidsetKey(redisDb*db,robj*key,robj*val){if(lookupKeyWrite(db,key)==NULL){dbAdd(db,key,val);}else{dbOverwrite(db,key,val);}incrRefCount(val);removeExpire(db,key);//去除过期时间signalModifiedKey(db,key);}在实际业务开发中,我们要合理评估Redis的容量同时避免频繁设置覆盖,导致设置了过期时间的key失效。新手小白很容易犯这个错误。4.缓存穿透问题首先我们来看一个常见的缓存使用方式:当有读请求到来时,首先检查缓存,如果缓存中有值则直接返回;如果缓存没有命中,则检查数据库,然后将数据库的值更新到缓存中,然后再返回。“缓存穿透”:是指查询一个一定不存在的数据。由于缓存未命中,需要从数据库中查询。如果找不到数据,则不会写入缓存。这将导致每次都请求不存在的数据。查询数据库,进而给数据库带来压力。通俗的说,当一个读请求被访问时,无论是缓存还是数据库都没有某个值,这会导致每次查询这个值的请求都会穿透到数据库,这就是缓存穿透。缓存穿透一般是以下几种情况造成的:“业务设计不合理”,比如大部分用户没有开启守卫,但是你的每一个请求都被缓存了,你可以查看某个userid查询是否被守卫。“业务/运维/开发错误操作”,如缓存、数据库数据被误删。“黑客非法请求攻击”,例如,黑客故意编造大量非法请求,读取不存在的业务数据。“如何避免缓存穿透?”一般有以下三种方法。如果是非法请求,我们在API入口处校验参数,过滤掉非法值;如果查询数据库为空,我们可以为缓存设置空值或默认值。但是如果有写请求进来,就需要更新缓存,保证缓存的一致性。同时,最终为缓存设置一个合适的过期时间。(业务中常用,简单有效);使用布隆过滤器快速判断数据是否存在。即当有查询请求进来时,首先通过布隆过滤器判断该值是否存在,然后继续检查是否存在。布隆过滤器原理:它由一个初值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法得到N个值,将位数组中的N个值进行hash并置为1,然后检查具体位置是否都为1,则Bloomfilter设备判断该key存在.5、缓存sledding的问题“Cachingsledding:”是指缓存中有大量数据直到过期时间,但是查询数据量巨大,而且所有的请求都直接访问数据库,导致数据库压力过大或者甚至下机。缓存学本一般是大量数据同时过期造成的。为此,可以通过均匀设置过期时间来解决,也就是让过期时间相对离散。例如,如果使用较大的固定值+较小的随机值,则需要5小时+0到1800秒;Redis故障和宕机也可能导致缓存降雪。这就需要搭建Redis高可用集群。6、缓存击穿问题“Cachebreakdown:”指的是当某个热点key在某个时间点过期,而这个时间点有大量并发请求这个key,从而导致大量请求命中分贝。缓存击穿看起来有点相似,但两者的区别在于,缓存击穿意味着数据库压力过大甚至宕机,而缓存击穿只是对DB数据库级别的大量并发请求。可以认为细分是缓存的学本的一个子集。有文章认为两者的区别在于击穿是针对某个热key缓存,而学本是针对很多key。有两种解决方案:“使用互斥解决方案”。当缓存失败时,不是立即加载db数据,而是使用一些有成功返回的原子操作命令,比如(Redis的setnx)来操作,成功后再加载db数据库数据并设置缓存。否则,重试获取缓存;""Neverexpire""表示不设置过期时间,但是当热点数据即将过期时,异步线程更新并设置过期时间。7、缓存热键的问题在Redis中,我们把访问频率高的键称为热键。如果对热键的请求到达服务器主机,由于请求量极大,可能会导致主机资源不足甚至崩溃,从而影响正常服务。热键是如何生成的?主要有两个原因:用户消费的数据远大于产生的数据,比如秒杀、热点新闻等场景,读多写少;请求分片集中,超过了单个Redi服务器的性能,比如固定名称key,hash落入同一个服务器,即时访问量巨大,超出机器瓶颈,导致热点Key问题。那么在日常开发中,如何识别热键呢?根据经验判断哪些是热键;客户统计报告;服务代理层上报如何解决热键问题?Redis集群扩容:增加分片副本,平衡读流量;对热键进行哈希,比如备份一个key为key1,key2...keyN,相同数据的N个备份,N个备份分布到不同的分片。访问时,可以随机访问N个备份中的一个,进一步分担读流量;使用二级缓存,即JVM本地缓存,减少Redis的读请求。四、Redis配置运维1、使用长连接代替短连接,合理配置客户端的连接池。如果使用短连接,每次都需要经过TCP三次握手和四次握手,会增加耗时。但是在长连接的情况下,它建立一次连接,一直可以使用redis命令。酱子可以减少建立redis连接的时间。连接池可以在客户端建立多个连接并且不释放它们。当需要连接时,不需要每次都创建连接,节省了时间。但是需要合理设置参数,在长时间不操作Redis时需要及时释放连接资源。2、只使用db0Redis-standalone架构,禁止使用非db0。一个连接有两个原因。Redis执行命令select0和select1进行切换,会消耗新的能量;Redis集群只支持db0。如果要迁移,成本高3.设置maxmemory+适当的驱逐策略。防止内存积压膨胀。比如有的时候,业务量大了,redis的key使用量大,内存根本就不够用,运维小哥也忘了加内存。难道redis就这么挂了?因此需要根据实际业务选择maxmemory-policy(最大内存淘汰策略)并设置过期时间。一共有8种内存淘汰策略:volatile-lru:当内存不足以容纳新写入的数据时,使用LRU(leastrecentlyused)算法从设置过期时间的key中淘汰;allkeys-lru:当内存不足容纳新写入的数据时,使用LRU(leastrecentlyused)算法从所有key中淘汰;volatile-lfu:4.0版本新增,当内存不足以容纳新写入的数据时,在过期key中,使用LFU算法删除key;allkeys-lfu:4.0版本新增,当内存不足以容纳新写入的数据时,使用LFU算法从所有key中剔除;volatile-random:当内存不足以容纳新写入的数据时,从设置过期时间的key中随机淘汰数据;allkeys-random:当内存不足以容纳新写入的数据时,随机淘汰所有key的数据;volatile-ttl:当内存不足以容纳新写入的数据时,在设置了过期时间的key中,会根据过期时间进行淘汰,先过期的先淘汰;noeviction:默认策略,当内存不足以容纳新写入的数据时,新的写操作会报错。4.开启lazy-free机制Redis4.0+版本支持lazy-free机制,如果你的Redis还有bigKey之类的东西,建议开启lazy-free。启用后,如果Redis删除一个bigkey,释放内存的耗时操作将在后台线程执行,减少对主线程的阻塞影响。