关于java:redis原理及相关缓存问题

36次阅读

共计 5699 个字符,预计需要花费 15 分钟才能阅读完成。

1 什么是 redis

redis 是 nosql(也是个微小的 map) 单线程,然而可解决 1 秒 10w 的并发(数据都在内存中)

应用 java 对 redis 进行操作相似 jdbc 接口标准对 mysql,有各类实现他的实现类,咱们罕用的是druid

其中对 redis,咱们通常用 Jedis(也为咱们提供了连接池 JedisPool)

在 redis 中,key 就是 byte[](string)

redis 的数据结构(value): Stringlistsetordersethash

2 redis 的应用

先装置好 redis,而后运行,在 pom 文件中引入依赖,在要应用 redis 缓存的类的 mapper.xml 文件配置 redis 的全限定名。引入 redis 的 redis.properties 文件(如果要更改配置就能够应用)

利用场景:

String :

1 存储 json 类型对象,
2 计数器,
3 优酷视频点赞等

list(双向链表)

1 能够应用 redis 的 list 模仿队列, 堆, 栈
2 敌人圈点赞(一条朋友圈内容语句, 若干点赞语句)
规定: 朋友圈内容的格局:
1, 内容: userpost:x content 来存储;
2, 点赞: postgood list 来存储;(把相应头像取出来显示)

hash(hashmap)
1 保留对象
2 分组

3 string 与 hash 的数据差异

在网路传输时候,必须要进行进行序列化,才能够进行网路传输,那么在应用 string 类型的类型的时候须要进行相干序列化,

hash 也是要进行相干的系列化,所以会存在很多序列化,在存储的时候 hash 是能够存储的更加丰盛,然而在反序列化的时候,string 的反序列化绝对较低,而 hash 的序列化和返序列化是绝对 hash 类更加简单,所以看业务场景,如果是数据常常批改的那种,为了性能能够应用 string,如果是数据不是常常改的那种就能够应用 hash,因为 hash,存储数据时比拟丰盛,能够存储多种数据类型 

4 redis 的长久化形式:

能,将内存中的数据异步写入硬盘中,两种形式:RDB(默认)和AOF

RDB 长久化原理:通过 bgsave 命令触发,而后父过程执行 fork 操作创立子过程,子过程创立 RDB 文件,依据父过程内存生成长期快照文件,实现后对原有文件进行原子替换(定时一次性将所有数据进行快照生成一份正本存储在硬盘中)

长处:是一个紧凑压缩的二进制文件,Redis 加载 RDB 复原数据远远快于 AOF 的形式。

毛病:因为每次生成 RDB 开销较大,非实时长久化,

AOF 长久化原理:开启后,Redis 每执行一个批改数据的命令,都会把这个命令增加到 AOF 文件中。

长处:实时长久化。

毛病:所以 AOF 文件体积逐步变大,须要定期执行重写操作来升高文件体积,加载慢

5 redis 单线程为什么这么快

redis 是单线程的,然而为什么还是这么快呢,
起因 1:单线程,防止线程之间的竞争
起因 2:是内存中的,应用内存的,能够缩小磁盘的 io
起因 3:多路复用模型,用了缓冲区的概念,selector 模型来进行的

6 redis 主挂了怎么操作

redis 提供了哨兵模式,当主挂了,能够选举其余的进行代替,哨兵模式的实现原理,就是三个定时工作监控,

6.1 每隔 10s,每个 S 节点(哨兵节点) 会向主节点和从节点发送 info 命令获取最新的拓扑构造

6.2 每隔 2s,每个 S 节点(哨兵节点) 会向某频道上发送该 S 节点(哨兵节点)对于主节点的判断以及 以后 S 节点(哨兵节点) 的信息,
同时每个 Sentinel 节点也会订阅该频道,来理解其余 S 节点(哨兵节点) 以及它们对主节点的判断(做主观下线根据)

6.3 每隔 1s,每个 S 节点(哨兵节点) 会向主节点、从节点、其余 S 节点(哨兵节点) 发送一条 ping 命令做一次心跳检测(心跳检测机制),来确认这些节点以后是否可达

当三次心跳检测之后,就会进行投票,当超过半数以上的时候就会将该节点当做主

7 redis 集群

redis 集群在 3.0 当前提供了 ruby 脚本进行搭建,引入了糙的概念,

Redis 集群内节点通过 ping/pong 音讯实现节点通信,音讯岂但能够流传节点槽信息,还能够流传其余状态如:主从状态、节点故障等。因而故障发现也是通过音讯流传机制实现的,次要环节包含:主观下线(pfail)和主观下线(fail)

主客观下线:

主观下线:集群中每个节点都会定期向其余节点发送 ping 音讯,接管节点回复 pong 音讯作为响应。如果通信始终失败,则发送节点会把接管节点标记为主观下线(pfail)状态。

主观下线:超过半数,对该主节点做主观下线

主节点选举出某一主节点作为领导者,来进行故障转移。

故障转移(选举从节点作为新主节点)

8 内存淘汰策略

Redis 的内存淘汰策略是指在 Redis 的用于缓存的内存不足时,怎么解决须要新写入且须要申请额定空间的数据。

noeviction:当内存不足以包容新写入数据时,新写入操作会报错。

allkeys-lru:当内存不足以包容新写入数据时,在键空间中,移除最近起码应用的 key。

allkeys-random:当内存不足以包容新写入数据时,在键空间中,随机移除某个 key。

volatile-lru:当内存不足以包容新写入数据时,在设置了过期工夫的键空间中,移除最近起码应用的 key。

volatile-random:当内存不足以包容新写入数据时,在设置了过期工夫的键空间中,随机移除某个 key。

volatile-ttl:当内存不足以包容新写入数据时,在设置了过期工夫的键空间中,有更早过期工夫的 key 优先移除。

9 REDIS 缓存穿透,缓存击穿,缓存雪崩起因 + 解决方案

粗体

  • 缓存穿透:key 对应的数据在数据源并不存在,每次针对此 key 的申请从缓存获取不到,申请都会到数据源,从而可能压垮数据源。比方用一个不存在的用户 id 获取用户信息,不管缓存还是数据库都没有,若黑客利用此破绽进行攻打可能压垮数据库。
  • 缓存击穿:key 对应的数据存在,但在 redis 中过期,此时若有大量并发申请过去,这些申请发现缓存过期个别都会从后端 DB 加载数据并回设到缓存,这个时候大并发的申请可能会霎时把后端 DB 压垮。
  • 缓存雪崩 :当缓存服务器重启或者大量缓存集中在某一个时间段生效,这样在生效的时候,也会给后端系统(比方 DB) 带来很大压力。

缓存穿透解决方案

一个肯定不存在缓存及查问不到的数据,因为缓存是不命中时被动写的,并且出于容错思考,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次申请都要到存储层去查问,失去了缓存的意义。

有很多种办法能够无效地解决缓存穿透问题,最常见 的则是采纳布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个肯定不存在的数据会被 这个 bitmap 拦挡掉,从而防止了对底层存储系统的查问压力。另外也有一个更为简略粗犷的办法(咱们采纳的就是这种),如果一个查问返回的数据为空(不论是数据不存在,还是系统故障),咱们依然把这个空后果进行缓存,但它的过期工夫会很短,最长不超过五分钟。

粗犷形式伪代码:

// 伪代码
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";

    String cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {return cacheValue;}

    cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {return cacheValue;} else {
        // 数据库查问不到,为空
        cacheValue = GetProductListFromDB();
        if (cacheValue == null) {
            // 如果发现为空,设置个默认值,也缓存起来
            cacheValue = string.Empty;
        }
        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        return cacheValue;
    }
}

缓存击穿解决方案

key 可能会在某些工夫点被超高并发地拜访,是一种十分“热点”的数据。这个时候,须要思考一个问题:缓存被“击穿”的问题。

应用互斥锁(mutex key)

业界比拟罕用的做法,是应用 mutex。简略地来说,就是在缓存生效的时候(判断拿进去的值为空),不是立刻去 load db,而是先应用缓存工具的某些带胜利操作返回值的操作(比方 Redis 的 SETNX 或者 Memcache 的 ADD)去 set 一个 mutex key,当操作返回胜利时,再进行 load db 的操作并回设缓存;否则,就重试整个 get 缓存的办法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,能够利用它来实现锁的成果。

public String get(key) {String value = redis.get(key);
      if (value == null) { // 代表缓存值过期
          // 设置 3min 的超时,避免 del 操作失败的时候,下次缓存过期始终不能 load db
      if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  // 代表设置胜利
               value = db.get(key);
                      redis.set(key, value, expire_secs);
                      redis.del(key_mutex);
              } else {  // 这个时候代表同时候的其余线程曾经 load db 并回设到缓存了,这时候重试获取缓存值即可
                      sleep(50);
                      get(key);  // 重试
              }
          } else {return value;}
 }

memcache 代码:

if (memcache.get(key) == null) {  
    // 3 min timeout to avoid mutex holder crash  
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {value = db.get(key);  
        memcache.set(key, value);  
        memcache.delete(key_mutex);  
    } else {sleep(50);  
        retry();}  
}

其它计划:待各位补充。

缓存雪崩解决方案

与缓存击穿的区别在于这里针对很多 key 缓存,前者则是某一个 key。缓存失常从 Redis 中获取,示意图如下:

缓存生效霎时示意图如下:

缓存生效时的雪崩效应对底层零碎的冲击十分可怕!大多数零碎设计者思考用加锁或者队列的形式保障来保障不会有大量的线程对数据库一次性进行读写,从而防止生效时大量的并发申请落到底层存储系统上。还有一个简略计划就时讲缓存生效工夫扩散开,比方咱们能够在原有的生效工夫根底上减少一个随机值,比方 1 - 5 分钟随机,这样每一个缓存的过期工夫的反复率就会升高,就很难引发个体生效的事件。

加锁排队,伪代码如下:

// 伪代码
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    String lockKey = cacheKey;

    String cacheValue = CacheHelper.get(cacheKey);
    if (cacheValue != null) {return cacheValue;} else {synchronized(lockKey) {cacheValue = CacheHelper.get(cacheKey);
            if (cacheValue != null) {return cacheValue;} else {
              // 这里个别是 sql 查问数据
                cacheValue = GetProductListFromDB(); 
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            }
        }
        return cacheValue;
    }
}

加锁排队只是为了加重数据库的压力,并没有进步零碎吞吐量。假如在高并发下,缓存重建期间 key 是锁着的,这是过去 1000 个申请 999 个都在阻塞的。同样会导致用户期待超时,这是个治标不治本的办法!

留神:加锁排队的解决形式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因而,在真正的高并发场景下很少应用!

随机值伪代码:

// 伪代码
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    // 缓存标记
    String cacheSign = cacheKey + "_sign";

    String sign = CacheHelper.Get(cacheSign);
    // 获取缓存值
    String cacheValue = CacheHelper.Get(cacheKey);
    if (sign != null) {return cacheValue; // 未过期,间接返回} else {CacheHelper.Add(cacheSign, "1", cacheTime);
        ThreadPool.QueueUserWorkItem((arg) -> {
      // 这里个别是 sql 查问数据
            cacheValue = GetProductListFromDB(); 
          // 日期设缓存工夫的 2 倍,用于脏读
          CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                 
        });
        return cacheValue;
    }
} 

解释阐明:

  • 缓存标记:记录缓存数据是否过期,如果过期会触发告诉另外的线程在后盾去更新理论 key 的缓存;
  • 缓存数据:它的过期工夫比缓存标记的工夫缩短 1 倍,例:标记缓存工夫 30 分钟,数据缓存设置为 60 分钟。这样,当缓存标记 key 过期后,理论缓存还能把旧数据返回给调用端,直到另外的线程在后盾更新实现后,才会返回新缓存。

对于缓存解体的解决办法,这里提出了三种计划:应用锁或队列、设置过期标记更新缓存、为 key 设置不同的缓存生效工夫,还有一种被称为“二级缓存”的解决办法。

正文完
 0