在我们的日常的Java Web开发过程中,都是使用数据库来对数据进行存储,提供给系统中的业务进行CRUD的一系列操作。可在我们涉及到大数据量和高并发的场景下时,只是使用数据库来处理数据的性能弊端暴露无遗,甚至极其容易就能造成数据库瘫痪,继而导致系统停止服务,从而对服务群体造成严重影响。为了克服以上问题,我们通常使用Redis来保证系统的稳定性和可用性。

一、Redis简介

Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源Key-Value数据库,并提供多种语言的API。
Redis与其他的Key-Value数据库(如Memcached)相比,有以下几个优势:

  • 读写高性能:Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 多数据类型:Redis不仅仅支持Key-Value类型的数据,同时还支持List、Set、Hash、Zset等数据结构。
  • 数据持久化:Redis可将内存中的数据持久化至硬盘上,对数据的备份和恢复起到良好的作用。
  • 原子性操作:Redis的所有操作都是原子性的。
  • 多语言支持:Redis可以和多种语言进行集成。
    ......

二、应用场景

Redis一般用作系统缓存、限流以及保证数据的一致性。

缓存

使用 Redis 作为缓存的读取逻辑如图所示(图源来自:Redis【入门】就这一篇!):
Redis缓存操作流程.webp
读取过程:

  1. 用户查询数据时,先去查询redis中的数据。
  2. 如果redis中存在该数据,直接返回给用户。
  3. 如果redis中不存在该数据,后台业务查询数据库。
  4. 将从数据库中查询到的数据写入redis中并返回给用户。

缓存穿透

缓存穿透,是指查询一个一定不存在的数据。

场景:一般情况下遭遇黑客攻击时可能会出现这种问题,以高频率的方式去查询一个不存在的数据。这些请求会全部落到数据库上,从而对数据造成极大的压力。

解决方案:通过在redis中存放空值对象(即将从数据库中查询所得到的空值对象作为value写入reids中并为其设置过期时间)可以解决这个问题。

缓存雪崩

缓存雪崩,是指在某一个时间段,缓存集中失效。

场景:大量缓存数据集中过期或者某一个redis服务节点宕机,会导致大量的请求落到数据库上,造成服务器压力。

解决方法:

  1. 对热门数据设置较长的过期时间(也可以不设置过期时间),对冷门数据设置较短的过期时间,可避免因为缓存数据过期而造成的缓存雪崩。
  2. 通过主从复制对redis中的缓存数据进行备份,可以迅速代替宕机的redis节点进行工作,在一定程度上可以解决因为redis服务节点宕机而造成的缓存雪崩。

缓存击穿

缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

场景:访问某些非常热门的数据时,该数据过期了,大量请求落到数据库上造成服务器压力。

解决方案:对这些非常热门的数据不设置过期时间。

三、数据结构

Redis支持的数据结构有String、List、Set、Hash、Zset五种。

String

String(字符串)类型是Redis中最为常用的数据类型,主要存储字符串类型的数据。
String类型最大能储存512M的数据。

List

List 存储的是list集合数据,集合中允许重复元素。

Set

Set 同 List,但是不允许重复元素。

Hash

Hash 是一个键值对集合。
Hash 存储的是类似于Map结构的数据。

Zset(Sorted Set)

Zset 和 Set 一样,不允许重复的成员。
Zset 中的每个元素都都会有一个score, 元素根据score进行排序。
Zset 可用于实现简单的延迟队列。

四、持久化

Redis提供了两种持久化方式,RDB和AOF

RDB(Redis DataBase)

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot快照,它恢复时是加载快照文件,将数据写入缓存中。
Redis会单独创建一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后产生的数据可能丢失。

RDB配置:

# 修改 RDB 文件名称
dbfilename dump.rdb

# 修改 RDB 文件保存路径
dir /redis/data_save

# 修改 RDB 保存策略
save 900 1  
save 300 10  
save 60 10000

# 当 Redis 无法持久化至磁盘时,关闭Redis的写操作
stop-writes-on-bgsave-error yes

# 进行 rdb 保存时,压缩文件
rdbcompression yes

# 在存储快照后,还可以让Redis使用CRC64算法来进行数据校验,损耗性能,建议关闭
rdbchecksum no

RDB的优点:

  • 使用较少的磁盘空间
  • 恢复方式简单,速度较快

RDB的缺点:

  • 数据量过大时,会损耗性能。
  • 由于是每隔一段时间进行一次RDB持久化操作,服务节点意外宕机的情况下,会丢失最后一次持久化操作之后的所有数据。

AOF(Append Of File)

以日志的形式来记录每个写操作,只能追加文件但不可以修改文件内容,Redis重启时根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

AOF配置:

# 开启 aof 持久化
appendonly yes

# 指定 aof 文件名称
appendfilename "redis_data.aof"

# aof 文件存放路径同rdb
dir /redis/data_save

# 设置 aof 同步频率

# 始终同步,redis的每进行一次操作都会记录到 aof 文件中
# appendsync always

# 每秒同步一次,redis每隔一秒对这一秒内所有的redis操作记录到 aof 文件中
appendsync everysec

# 关闭 aof 同步
# appendsync no

# 配置 aof 重写机制
auto-aof-rewirte-percentage 100
auto-aof-rewrite-min-size 64mb	

AOF的优点:

  • 备份机制更稳健,丢失数据概率更低。
  • AOF是日志文件,可以通过改文件找出操作过程中的一些问题并予以解决。

AOF的缺点:

  • 占用更多的磁盘空间。
  • 备份和恢复的速度较慢。
  • 每次读写都同步的话,有一定的性能压力。

AOF和RDB的选择:

  • 对数据不敏感(允许存在数据丢失),优先选择RDB。
  • 对数据敏感的情况下,同时开启RDB和AOF。
  • 只是单纯的用作内存和缓存,可以不开启持久化。

五、Redis主从复制

主从复制,就是主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slaver以读为主。

主从关系配置

通过 info replication命令查看当前redis节点的主从复制相关信息。
通过执行 slaveof ip port命令设置当前redis节点为指定redis节点的从服务节点。

主从复制过程及原理

过程:

  1. 当 Slaver 连接上 Master后,Slaver 向 Master 发送 sync 指令。
  2. Master 接收到 Slaver 的 sync 指令后,立即进行数据持久化操作,生成RDB文件。
  3. Master 将RDB文件发送给 Slaver。
  4. Slaver 接收到 Master 发送过来的RDB文件后,进行全盘加载及复制操作。
  5. 之后再 Master 中的所有写操作都会立刻发送给 Slaver , Slaver 执行相同的操作来保证主从数据一致。

Slaver 升级为 Master :
当 Master 宕机时,可在 Slaver 节点执行 slaveof no one命令将当前 Slaver 节点升级为 Master 节点。

Redis哨兵(Sentinel)模式

能够后台监控 Master 节点是否出现故障,如果故障了通过投票机制将 Slaver 自动升级为 Master。

配置 Sentinel

在Redis安装目录下有一个sentinel.conf文件,对改文件进行如下修改

sentinel  monitor  mymaster  `masterIP`  `masterPort`  `2`
  • masterIP # Master 节点地址
  • masterPort # Master 节点端口
  • 2 # 只有两个或两个以上的哨兵认为Master不可用的时候,才会将Slaver变为Master

启动哨兵

执行 redis-sentinel /myredis/sentinel.conf
当 Master 宕机后,当前 Slaver 会直接替换为 Master, 之前的 Master 重启后会成为当前节点的 Slaver 。

六、Redis集群

Redis集群相关知识还未有过实际应用,之后完善。

七、Redis事务

Redis通过 MULTIEXECDISCARDWATCH等命令来实现事务(transaction)功能。
事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而去执行其他客户端命令。

MULTI 命令

执行该命令表示开启 redis 事务,之后的所有操作都会进入事务队列

127.0.0.1:6379>> MULTI
OK

127.0.0.1:6379>> set name Redis;
QUEUED

127.0.0.1:6379>> get name;
QUEUED

127.0.0.1:6379>> set language Java;
QUEUED

127.0.0.1:6379>> get language;
QUEUED

EXEC 命令

发送该命令后,redis 服务器会立即执行 事务队列 中保存的所有命令。

127.0.0.1:6379> EXEC
OK
"Redis"
OK
"Java"

WATCH 命令

WATCH命令是一个乐观锁,它可以在 EXEC命令执行前,监视任意数量的数据库键,并在 EXEC命令执行时,检查被监视的键是否存在被修改过的,如果存在,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

127.0.0.1:6379> WATCH name
OK

127.0.0.1:6379> MULTI
OK

127.0.0.1:6379> set name HelloWorld
QUEUED

127.0.0.1:6379> EXEC
(nil)

八、Redis内存淘汰策略

当 Redis 内存不足时,会采用淘汰策略删除部分缓存。从 Redis4.0 版本之后,Redis有以下8中内存淘汰策略。

  • volatile-lru:从设置了过期时间的键集合中淘汰最久没有使用的键
  • volatile-lfu:从设置了过期时间的键中淘汰使用频率最少的键
  • volatile-random:从设置了过期键的集合中随机淘汰
  • volatile-ttl:从设置了过期时间的键中淘汰马上就要过期的键
  • allkeys-lru:从所有键中淘汰最久没有使用的键
  • allkeys-lfu:从所有键中淘汰使用频率最少的键
  • allkeys-random:从所有键中随机淘汰
  • noeviction:不会淘汰任何键,但是会报错

LRU(Least Recently Used)

最近最少使用算法:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)

LRU 算法简述:

  • 新数据插入到链表头部
  • 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
  • 当链表满的时候,将链表尾部的数据丢弃

Java 实现 LRU 算法的方式

logback中的 LRUMessageCache使用 LinkedHashMap进行了实现 ,查看 LinkedHashMap中的 get()方法也能看到LRU的影子。

/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.classic.turbo;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Clients of this class should only use the  {@link #getMessageCountAndThenIncrement} method. Other methods inherited
 * via LinkedHashMap are not thread safe.
 */
class LRUMessageCache extends LinkedHashMap<String, Integer> {

    private static final long serialVersionUID = 1L;
    final int cacheSize;

    LRUMessageCache(int cacheSize) {
        super((int) (cacheSize * (4.0f / 3)), 0.75f, true);
        if (cacheSize < 1) {
            throw new IllegalArgumentException("Cache size cannot be smaller than 1");
        }
        this.cacheSize = cacheSize;
    }

    int getMessageCountAndThenIncrement(String msg) {
        // don't insert null elements
        if (msg == null) {
            return 0;
        }

        Integer i;
        // LinkedHashMap is not LinkedHashMap. See also LBCLASSIC-255
        synchronized (this) {
            i = super.get(msg);
            if (i == null) {
                i = 0;
            } else {
                i = i + 1;
            }
            super.put(msg, i);
        }
        return i;
    }

    // called indirectly by get() or put() which are already supposed to be
    // called from within a synchronized block
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return (size() > cacheSize);
    }

    @Override
    synchronized public void clear() {
        super.clear();
    }
}

Redis中LRU的实现

  • Redis操作数据的时候会带上时间戳,当Redis内存不足时,会将数据的时间戳与当前时间进行对比,将距离当前时间较为久远的数据进行淘汰。
  • Redis中的LRU与常规的LRU实现并不相同,常规LRU会准确的淘汰掉队头的元素,但是Redis的LRU并不维护队列,只是根据配置的策略要么从所有的key中随机选择N个(N可以配置)要么从所有的设置了过期时间的key中选出N个键,然后再从这N个键中选出最久没有使用的一个key进行淘汰。

LFU(Least Frequently Used)

最近最不常用算法:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰

参考资料

Redis 集群
Redis事务
深入剖析Redis - Redis集群模式搭建与原理详解
实例解读什么是Redis缓存穿透、缓存雪崩和缓存击穿
缓存算法(FIFO 、LRU、LFU三种算法的区别)


关于作者:NekoChips
本文地址:https://chenyangjie.com.cn/articles/2019/11/11/1573458713627.html
版权声明:本篇所有文章仅用于学习和技术交流,本作品采用 BY-NC-SA 4.0 许可协议,如需转载请注明出处!
许可协议:知识共享许可协议