ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

使用Redis,你必须知道的21个注意要点

使用Redis,你必须知道的21个注意要点 最近在学习Redis相关知识看了阿里的redis开发规范以及Redis开发与运维这本书。分使用规范、有坑的命令、项目实战操作、运维配置四个方向整理了使用Redis的21个注意点希望对大家有帮助一起学习哈1、Redis的使用规范1.1、 key的规范要点我们设计Redis的key的时候要注意以下这几个点❝以业务名为key前缀用冒号隔开以防止key冲突覆盖。如live:rank:1确保key的语义清晰的情况下key的长度尽量小于30个字符。key禁止包含特殊字符如空格、换行、单双引号以及其他转义字符。Redis的key尽量设置ttl以保证不使用的Key能被及时清理或淘汰。❞1.2、value的规范要点Redis的value值不可以随意设置的哦。「第一点」如果大量存储bigKey是会有问题的会导致慢查询内存增长过快等等。如果是String类型单个value大小控制10k以内。如果是hash、list、set、zset类型元素个数一般不超过5000。「第二点」要选择适合的数据类型。不少小伙伴只用Redis的String类型上来就是set和get。实际上Redis 提供了「丰富的数据结构类型」有些业务场景更适合hash、zset等其他数据结果。「反例」set user:666:name jay set user:666:age 18「正例」hmset user:666 name jay age 181.3. 给Key设置过期时间同时注意不同业务的key尽量过期时间分散一点因为Redis的数据是存在内存中的而内存资源是很宝贵的。我们一般是把Redis当做缓存来用而「不是数据库」所以key的生命周期就不宜太长久啦。因此你的key一般建议用「expire设置过期时间」。如果大量的key在某个时间点集中过期到过期的那个时间点Redis可能会存在卡顿甚至出现「缓存雪崩」现象因此一般不同业务的key过期时间应该分散一些。有时候同业务的也可以在时间上加一个随机值让过期时间分散一些。1.4.建议使用批量操作提高效率我们日常写SQL的时候都知道批量操作效率会更高一次更新50条比循环50次每次更新一条效率更高。其实Redis操作命令也是这个道理。Redis客户端执行一次命令可分为4个过程1.发送命令- 2.命令排队- 3.命令执行- 4. 返回结果。1和4 称为RRT命令执行往返时间。Redis提供了「批量操作命令如mget、mset」等可有效节约RRT。但是呢大部分的命令是不支持批量操作的比如hgetall并没有mhgetall存在。「Pipeline」则可以解决这个问题。❝Pipeline是什么呢?它能将一组Redis命令进行组装通过一次RTT传输给Redis再将这组Redis命令的执行结果按顺序返回给客户端.❞我们先来看下没有使用Pipeline执行了n条命令的模型使用Pipeline执行了n次命令整个过程需要1次RTT模型如下2、Redis 有坑的那些命令2.1. 慎用O(n)复杂度命令如hgetall、smemberlrange等因为Redis是单线程执行命令的。hgetall、smember等命令时间复杂度为O(n)当n持续增加时会导致 Redis CPU 持续飙高阻塞其他命令的执行。❝hgetall、smemberlrange等这些命令不是一定不能使用需要综合评估数据量明确n的值再去决定。比如hgetall如果哈希元素n比较多的话可以优先考虑使用「hscan」。❞2.2 慎用Redis的monitor命令Redis Monitor 命令用于实时打印出Redis服务器接收到的命令如果我们想知道客户端对redis服务端做了哪些命令操作就可以用Monitor 命令查看但是它一般「调试」用而已尽量不要在生产上用因为「monitor命令可能导致redis的内存持续飙升。」❝monitor的模型是酱紫的它会将所有在Redis服务器执行的命令进行输出一般来讲Redis服务器的QPS是很高的也就是如果执行了monitor命令Redis服务器在Monitor这个客户端的输出缓冲区又会有大量“存货”也就占用了大量Redis内存。❞2.3、生产环境不能使用 keys指令Redis Keys 命令用于查找所有符合给定模式pattern的key。如果想查看Redis 某类型的key有多少个不少小伙伴想到用keys命令如下keys key前缀*但是redis的keys是遍历匹配的复杂度是On数据库数据越多就越慢。我们知道redis是单线程的如果数据比较多的话keys指令就会导致redis线程阻塞线上服务也会停顿了直到指令执行完服务才会恢复。因此「一般在生产环境不要使用keys指令」。官方文档也有声明❝Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout. Dont use KEYS in your regular application code. If youre looking for a way to find keys in a subset of your keyspace, consider using sets.❞其实可以使用scan指令它同keys命令一样提供模式匹配功能。它的复杂度也是 O(n)但是它通过游标分步进行「不会阻塞redis线程」;但是会有一定的「重复概率」需要在「客户端做一次去重」。❝scan支持增量式迭代命令增量式迭代命令也是有缺点的举个例子 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素 但是对于 SCAN 这类增量式迭代命令来说 因为在对键进行增量式迭代的过程中 键可能会被修改 所以增量式迭代命令只能对被返回的元素提供有限的保证 。❞2.4 禁止使用flushall、flushdb❝Flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key )。Flushdb 命令用于清空当前数据库中的所有 key。❞这两命令是原子性的不会终止执行。一旦开始执行不会执行失败的。2.5 注意使用del命令删除key你一般使用什么命令是直接del如果删除一个key直接使用del命令当然没问题。但是你想过del的时间复杂度是多少嘛我们分情况探讨一下如果删除一个String类型的key时间复杂度就是O1「可以直接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依次删除每个元素。❞2.6 避免使用SORT、SINTER等复杂度过高的命令。执行复杂度较高的命令会消耗更多的 CPU 资源会阻塞主线程。所以你要避免执行如SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE等聚合命令一般建议把它放到客户端来执行。3、项目实战避坑操作3.1 分布式锁使用的注意点分布式锁其实就是控制分布式系统不同进程共同访问共享资源的一种锁的实现。秒杀下单、抢红包等等业务场景都需要用到分布式锁。我们经常使用Redis作为分布式锁主要有这些注意点3.1.1 两个命令SETNX EXPIRE分开写典型错误实现范例ifjedis.setnx(key_resource_id,lock_value) 1{ //加锁 expirekey_resource_id100; //设置过期时间 try { do something //业务请求 }catch(){ } finally { jedis.del(key_resource_id); //释放锁 } }如果执行完setnx加锁正要执行expire设置过期时间时进程crash或者要重启维护了那么这个锁就“长生不老”了「别的线程永远获取不到锁」啦所以一般分布式锁不能这么实现。3.1.2 SETNX value值是过期时间 (有些小伙伴是这么实现有坑)long expires System.currentTimeMillis() expireTime; //系统时间设置的过期时间 String expiresStr String.valueOf(expires); // 如果当前锁不存在返回加锁成功 if (jedis.setnx(key_resource_id, expiresStr) 1) { return true; } // 如果锁已经存在获取锁的过期时间 String currentValueStr jedis.get(key_resource_id); // 如果获取到的过期时间小于系统当前时间表示已经过期 if (currentValueStr ! null Long.parseLong(currentValueStr) System.currentTimeMillis()) { // 锁已过期获取上一个锁的过期时间并设置现在锁的过期时间不了解redis的getSet命令的小伙伴可以去官网看下哈 String oldValueStr jedis.getSet(key_resource_id, expiresStr); if (oldValueStr ! null oldValueStr.equals(currentValueStr)) { // 考虑多线程并发的情况只有一个线程的设置值和当前值相同它才可以加锁 return true; } } //其他情况均返回加锁失败 return false; }这种方案的「缺点」❝过期时间是客户端自己生成的分布式环境下每个客户端的时间必须同步没有保存持有者的唯一标识可能被别的客户端释放/解锁。锁过期的时候并发多个客户端同时请求过来都执行了jedis.getSet()最终只能有一个客户端加锁成功但是该客户端锁的过期时间可能被别的客户端覆盖。❞3.1.3SET的扩展命令SET EX PX NX注意可能存在的问题ifjedis.set(key_resource_id, lock_value, NX, EX, 100s) 1{ //加锁 try { do something //业务处理 }catch(){ } finally { jedis.del(key_resource_id); //释放锁 } }这个方案还是可能存在问题锁过期释放了业务还没执行完。锁被别的线程误删。3.1.4 SET EX PX NX 校验唯一随机值,再删除解决了误删问题还是存在锁过期业务没执行完的问题ifjedis.set(key_resource_id, uni_request_id, NX, EX, 100s) 1{ //加锁 try { do something //业务处理 }catch(){ } finally { //判断是不是当前线程加的锁,是才释放 if (uni_request_id.equals(jedis.get(key_resource_id))) { jedis.del(lockKey); //释放锁 } } }在这里判断是不是当前线程加的锁和释放锁不是一个原子操作。如果调用jedis.del()释放锁的时候可能这把锁已经不属于当前客户端会解除他人加的锁。一般也是用lua脚本代替。lua脚本如下if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end;3.1.5 Redisson框架 Redlock算法 解决锁过期释放业务没执行完问题单机问题Redisson 使用了一个Watch dog解决了锁过期释放业务没执行完问题Redisson原理图如下:以上的分布式锁还存在单机问题❝如果线程一在Redis的master节点上拿到了锁但是加锁的key还没同步到slave节点。恰好这时master节点发生故障一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦但线程一也已经拿到锁了锁的安全性就没了。❞针对单机问题可以使用Redlock算法。有兴趣的朋友可以看下我这篇文章哈七种方案探讨Redis分布式锁的正确使用姿势3.2 缓存一致性注意点如果是读请求先读缓存后读数据库如果写请求先更新数据库再写缓存每次更新数据后需要清除缓存缓存一般都需要设置一定的过期失效一致性要求高的话可以使用biglogMQ保证。有兴趣的朋友可以看下我这篇文章哈并发环境下先操作数据库还是先操作缓存3.3 合理评估Redis容量避免由于频繁set覆盖导致之前设置的过期时间无效。我们知道Redis的所有数据结构类型都是可以设置过期时间的。假设一个字符串已经设置了过期时间你再去重新设置它就会导致之前的过期时间无效。RedissetKey源码如下void setKey(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的容量避免频繁set覆盖导致设置了过期时间的key失效。新手小白容易犯这个错误。3.4 缓存穿透问题先来看一个常见的缓存使用方式读请求来了先查下缓存缓存有值命中就直接返回缓存没命中就去查数据库然后把数据库的值更新到缓存再返回。「缓存穿透」指查询一个一定不存在的数据由于缓存是不命中时需要从数据库查询查不到数据则不写入缓存这将导致这个不存在的数据每次请求都要到数据库去查询进而给数据库带来压力。❝通俗点说读请求访问时缓存和数据库都没有某个值这样就会导致每次对这个值的查询请求都会穿透到数据库这就是缓存穿透。❞缓存穿透一般都是这几种情况产生的「业务不合理的设计」比如大多数用户都没开守护但是你的每个请求都去缓存查询某个userid查询有没有守护。「业务/运维/开发失误的操作」比如缓存和数据库的数据都被误删除了。「黑客非法请求攻击」比如黑客故意捏造大量非法请求以读取不存在的业务数据。「如何避免缓存穿透呢」一般有三种方法。如果是非法请求我们在API入口对参数进行校验过滤非法值。如果查询数据库为空我们可以给缓存设置个空值或者默认值。但是如有有写请求进来的话需要更新缓存哈以保证缓存一致性同时最后给缓存设置适当的过期时间。业务上比较常用简单有效使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时先通过布隆过滤器判断值是否存在存在才继续往下查。❝布隆过滤器原理它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值在比特数组中将这N个值散列后设定为1然后查的时候如果特定的这几个位置都为1那么布隆过滤器判断该key存在。❞3.5 缓存雪奔问题「缓存雪奔」指缓存中数据大批量到过期时间而查询数据量巨大请求都直接访问数据库引起数据库压力过大甚至down机。缓存雪奔一般是由于大量数据同时过期造成的对于这个原因可通过均匀设置过期时间解决即让过期时间相对离散一点。如采用一个较大固定值一个较小的随机值5小时0到1800秒酱紫。Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦。3.6 缓存击穿问题「缓存击穿」指热点key在某个时间点过期的时候而恰好在这个时间点对这个Key有大量的并发请求过来从而大量的请求打到db。缓存击穿看着有点像其实它两区别是缓存雪奔是指数据库压力过大甚至down机缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别是区别在于击穿针对某一热点key缓存雪奔则是很多key。解决方案就有两种「1.使用互斥锁方案」。缓存失效时不是立即去加载db数据而是先使用某些带成功返回的原子操作命令如(Redis的setnx去操作成功的时候再去加载db数据库数据和设置缓存。否则就去重试获取缓存。「2. “永不过期”」是指没有设置过期时间但是热点数据快要过期时异步线程去更新和设置过期时间。3.7、缓存热key问题在Redis中我们把访问频率高的key称为热点key。如果某一热点key的请求到服务器主机时由于请求量特别大可能会导致主机资源不足甚至宕机从而影响正常的服务。而热点Key是怎么产生的呢主要原因有两个❝用户消费的数据远大于生产的数据如秒杀、热点新闻等读多写少的场景。请求分片集中超过单Redi服务器的性能比如固定名称keyHash落入同一台服务器瞬间访问量极大超过机器瓶颈产生热点Key问题。❞那么在日常开发中如何识别到热点key呢❝凭经验判断哪些是热Key客户端统计上报服务代理层上报❞如何解决热key问题❝Redis集群扩容增加分片副本均衡读流量对热key进行hash散列比如将一个key备份为key1,key2……keyN同样的数据N个备份N个备份分布到不同分片访问时可随机访问N个备份中的一个进一步分担读流量使用二级缓存即JVM本地缓存,减少Redis的读请求。❞4. Redis配置运维4.1 使用长连接而不是短连接并且合理配置客户端的连接池如果使用短连接每次都需要过 TCP 三次握手、四次挥手会增加耗时。然而长连接的话它建立一次连接redis的命令就能一直使用酱紫可以减少建立redis连接时间。连接池可以实现在客户端建立多个连接并且不释放需要使用连接的时候不用每次都创建连接节省了耗时。但是需要合理设置参数长时间不操作 Redis时也需及时释放连接资源。4.2 只使用 db0Redis-standalone架构禁止使用非db0.原因有两个一个连接Redis执行命令select 0和select 1切换会损耗新能。Redis Cluster 只支持 db0要迁移的话成本高4.3 设置maxmemory 恰当的淘汰策略。为了防止内存积压膨胀。比如有些时候业务量大起来了redis的key被大量使用内存直接不够了运维小哥哥也忘记加大内存了。难道redis直接这样挂掉所以需要根据实际业务选好maxmemory-policy(最大内存淘汰策略)设置好过期时间。一共有8种内存淘汰策略volatile-lru当内存不足以容纳新写入数据时从设置了过期时间的key中使用LRU最近最少使用算法进行淘汰allkeys-lru当内存不足以容纳新写入数据时从所有key中使用LRU最近最少使用算法进行淘汰。volatile-lfu4.0版本新增当内存不足以容纳新写入数据时在过期的key中使用LFU算法进行删除key。allkeys-lfu4.0版本新增当内存不足以容纳新写入数据时从所有key中使用LFU算法进行淘汰volatile-random当内存不足以容纳新写入数据时从设置了过期时间的key中随机淘汰数据。allkeys-random当内存不足以容纳新写入数据时从所有key中随机淘汰数据。volatile-ttl当内存不足以容纳新写入数据时在设置了过期时间的key中根据过期时间进行淘汰越早过期的优先被淘汰noeviction默认策略当内存不足以容纳新写入数据时新写入操作会报错。4.4 开启 lazy-free 机制Redis4.0版本支持lazy-free机制如果你的Redis还是有bigKey这种玩意存在建议把lazy-free开启。当开启它后Redis 如果删除一个 bigkey 时释放内存的耗时操作会放到后台线程去执行减少对主线程的阻塞影响。
返回列表