关于java:使用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 18 
复制代码

1.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)复杂度命令,如hgetallsmemberlrange

因为Redis是单线程执行命令的。hgetall、smember等命令工夫复杂度为O(n),当n继续减少时,会导致 Redis CPU 继续飙高,阻塞其余命令的执行。

hgetall、smember,lrange等这些命令不是肯定不能应用,须要综合评估数据量,明确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是遍历匹配的,复杂度是O(n),数据库数据越多就越慢。咱们晓得,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. Don’t use KEYS in your regular application code. If you’re 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,工夫复杂度就是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顺次删除每个元素。

2.6 防止应用SORT、SINTER等简单度过高的命令。

执行复杂度较高的命令,会耗费更多的 CPU 资源,会阻塞主线程。所以你要防止执行如SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE等聚合命令,个别倡议把它放到客户端来执行。

3、我的项目实战避坑操作

3.1 分布式锁应用的留神点

分布式锁其实就是,管制分布式系统不同过程独特访问共享资源的一种锁的实现。秒杀下单、抢红包等等业务场景,都须要用到分布式锁。咱们常常应用Redis作为分布式锁,次要有这些留神点:

3.1.1 两个命令SETNX + EXPIRE离开写(典型谬误实现范例)

if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
    expire(key_resource_id,100); //设置过期工夫
    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.3: SET的扩大命令(SET EX PX NX)(留神可能存在的问题)

if(jedis.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 + 校验惟一随机值,再删除(解决了误删问题,还是存在锁过期,业务没执行完的问题)

if(jedis.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 缓存一致性留神点

  • 如果是读申请,先读缓存,后读数据库
  • 如果写申请,先更新数据库,再写缓存
  • 每次更新数据后,须要革除缓存
  • 缓存个别都须要设置肯定的过期生效
  • 一致性要求高的话,能够应用biglog+MQ保障。

有趣味的敌人,能够看下我这篇文章哈:并发环境下,先操作数据库还是先操作缓存?

3.3 正当评估Redis容量,防止因为频繁set笼罩,导致之前设置的过期工夫有效。

咱们晓得,Redis的所有数据结构类型,都是能够设置过期工夫的。假如一个字符串,曾经设置了过期工夫,你再去从新设置它,就会导致之前的过期工夫有效。

Redis setKey源码如下:

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查问有没有守护。
  • 业务/运维/开发失误的操作,比方缓存和数据库的数据都被误删除了。
  • 黑客非法申请攻打,比方黑客成心捏造大量非法申请,以读取不存在的业务数据。

如何防止缓存穿透呢? 个别有三种办法。

    1. 如果是非法申请,咱们在API入口,对参数进行校验,过滤非法值。
    1. 如果查询数据库为空,咱们能够给缓存设置个空值,或者默认值。然而如有有写申请进来的话,须要更新缓存哈,以保障缓存一致性,同时,最初给缓存设置适当的过期工夫。(业务上比拟罕用,简略无效)
    1. 应用布隆过滤器疾速判断数据是否存在。即一个查问申请过去时,先通过布隆过滤器判断值是否存在,存在才持续往下查。

布隆过滤器原理:它由初始值为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服务器的性能,比方固定名称key,Hash落入同一台服务器,霎时访问量极大,超过机器瓶颈,产生热点Key问题。

那么在日常开发中,如何辨认到热点key呢?

  • 凭教训判断哪些是热Key;
  • 客户端统计上报;
  • 服务代理层上报

如何解决热key问题?

  • Redis集群扩容:减少分片正本,平衡读流量;
  • 对热key进行hash散列,比方将一个key备份为key1,key2……keyN,同样的数据N个备份,N个备份散布到不同分片,拜访时可随机拜访N个备份中的一个,进一步分担读流量;
  • 应用二级缓存,即JVM本地缓存,缩小Redis的读申请。
  1. Redis配置运维

4.1 应用长连贯而不是短连贯,并且合理配置客户端的连接池

  • 如果应用短连贯,每次都须要过 TCP 三次握手、四次挥手,会减少耗时。然而长连贯的话,它建设一次连贯,redis的命令就能始终应用,酱紫能够缩小建设redis连接时间。
  • 连接池能够实现在客户端建设多个连贯并且不开释,须要应用连贯的时候,不必每次都创立连贯,节俭了耗时。然而须要正当设置参数,长时间不操作 Redis时,也需及时开释连贯资源。

4.2 只应用 db0

Redis-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-lfu:4.0版本新增,当内存不足以包容新写入数据时,在过期的key中,应用LFU算法进行删除key。
  • allkeys-lfu:4.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 时,开释内存的耗时操作,会放到后盾线程去执行,缩小对主线程的阻塞影响。

参考:《2020最新Java根底精讲视频教程和学习路线!》
链接:https://juejin.cn/post/694264…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理