Redis分布式缓存剖析及大厂面试精髓v6.2.6

Posted itxiaoshen博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis分布式缓存剖析及大厂面试精髓v6.2.6相关的知识,希望对你有一定的参考价值。

概述

**本人博客网站 **IT小神 www.itxiaoshen.com

官方说明

Redis官网 https://redis.io/ 最新版本6.2.6

Redis中文官网 http://www.redis.cn/ 不过中文官网的同步更新维护相对要滞后不少时间,但对于我们基础学习完成足够了

Redis是一个开源(BSD许可)的内存数据结构存储,用作数据库、缓存和消息代理。Redis提供丰富的数据结构,如字符串、哈希、列表、集合、带范围查询、位图、超对数、地理空间索引和流的排序集。Redis具有内置的复制、Lua脚本、LRU驱逐、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster的自动分区提供高可用性。

Redis使用场景有哪些?

计数器、分布式ID生成器、海量数据统计bitmap、会话缓存、分布式阻塞队列、分布式锁、热点数据、社交需求好友推荐、延迟队列(sortset)等。

Redis与mysql的部分场景比较

  • 高性能读写访问,解决mysql读写慢的问题、缓解mysql压力
  • 具有较丰富可描述性数据结构和可扩展性。
  • Redis有更高优势应对访问热度问题,存储热点数据。

安装

单机源码安装

#Redis单机源码安装非常简单的,先下载,提取和编译就可以拉起来使用,Redis单机一般用于开发和学习环境,生产使用的话一般都是使用Redis Sentinel或者Redis Cluster保证高可用性
wget https://download.redis.io/releases/redis-6.2.6.tar.gz
tar xzf redis-6.2.6.tar.gz
cd redis-6.2.6
make && make install

#在当前目录下有redis的配置文件redis.conf,先修改redis.conf中的daemonize值为yes让redis以后台程序方式运行
redis-server redis.conf
#使用redis自带的客户端工具redis-cli
redis-cli
#向redis写入一个key名hello,值为world
set hello world
#读取key名称为hello的值
get hello
#Redis默认配置是16个数据库,通常没有特殊指定连接操作的是0号库,可以通过select命令选择库的索引,比如可以选择1号库
select 1

Redis Cluster安装(伪集群)

我们这里采用在同一台上多个端口运行多个redis实例的伪集群安装方式(当然也可以采用之前学习的docker等容器化的方式部署redis集群),同样需要先安装redis,可参考上面单机安装步骤。

#创建集群目录,放置各集群实例的配置和数据,创建六个文件夹,分别以端口号命名7000 7001 7002 7003 7004 7005六个以端口号为名字的子目录, 稍后我们在将每个目录中运行一个 Redis 实例
mkdir rediscluster
cd rediscluster
mkdir 7000 7001 7002 7003 7004 7005
#并将redis.conf配置文件拷贝六个目录下conf文件夹中,修改六个redis.conf 最少配置内容,端口port的配置和目录文件夹名称一致,其他内容如数据文件目录dir、bind、密码等配置可以按照实际的情况需求进行修改
vi redis.conf
daemonize yes
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
#分别进入6个端口目录
cd 7000
#分别启动相应目录下配置文件的redis实例
redis-server redis.conf
#配置集群信息
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
redis-cli --cluster create 192.168.50.36:7000 192.168.50.36:7001 192.168.50.36:7002 192.168.50.36:7003 192.168.50.36:7004 192.168.50.36:7005 --cluster-replicas 1

出现下面的信息则代表集群的信息已经配置成功

#通过客户端登录redis集群
redis-cli -c -p 7000
#和上面一样读取键值验证redis集群是否正常

Redis功能特性

常见功能

Redis命令

官网提供非常详细信息可以查阅,对于常见命令如所有数据结构读写操作命令都是需要熟悉的

也可以通过官方提供客户端help命令查阅

Redis客户端库

Redis支持非常多种语言的运营,官方上列出54种编程语言库,待黄色星号的是对应编程语言推荐的客户端库

以我们Java开发技术栈来说,推荐使用Jedis(一个非常小和健全的Redis Java客户端)、Lettuce(先进的Redis客户端线程安全同步,异步,和反应使用。支持集群、哨兵、流水线和编解码器。后面有Lettuce官网和GitHub源码地址,目前很多整合框架如SpringBoot都是使用Lettuce库)、Redisson(基于Redis服务器的分布式协调和可扩展的Java数据结构,如封装redis的分布式锁)。后面有时间我们专门针对Lettuce、Redisson这两个库实战和原理做专门剖析。

从Lettuce官网上就可以简单示例,包括对于怎么连接单机版本和集群版本,当然实际上我们更多的是使用Spring与Redis整合作为开发方式。

#如使用Maven,pom.xml加入下面依赖
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.1.5.RELEASE</version>
</dependency>
  
#如使用Gradle,build.gradle加入下面依赖
dependencies {
  compile \'io.lettuce:lettuce-core:6.1.5.RELEASE
}
import io.lettuce.core.*;

public class ConnectToRedis {

    public static void main(String[] args) {
        #Redis分为16个库,下面使用的是0号库
        RedisClient redisClient = RedisClient.create("redis://password@localhost:6379/0");
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        RedisCommands<String, String> syncCommands = connection.sync();
        syncCommands.set("testkey", "test string value");
        connection.close();
        redisClient.shutdown();
    }
}
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;

public class ConnectToRedisCluster {

    public static void main(String[] args) {
        // Syntax: redis://[password@]host[:port]
        // Syntax: redis://[username:password@]host[:port]
        RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7000");
        StatefulRedisClusterConnection<String, String> connection = redisClient.connect();
        System.out.println("Connected to Redis");
        connection.close();
        redisClient.shutdown();
    }
}

Redis发布/订阅(Pub/Sub)

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息,客户端订阅到一个或多个频道,其他客户端发到这些频道的消息将会被推送到所有订阅的客户端;发布/订阅与key所在空间没有关系,它不会受任何级别的干扰,包括不同数据库索引, 发布在db 10,订阅可以在db 1。

  • SUBCRIBE:订阅一个或者多个频道。
  • PSUBCRIBE:订阅一个或多个符合给定模式的频道;每个模式以 * 作为匹配符,比如*itxiaoshen匹配所有以 it 开头的频道(news.itxiaoshen 、 sports.itxiaoshen 等等)。
  • Publish:命令用于将信息发送到指定的频道。
  • Pubsub:命令用于查看订阅与发布系统状态。
  • UNSUBCRIBE:退订给定的一个或多个频道的信息。
  • PUNSUBCRIBE:退订所有给定模式的频道。
#客户端订阅执行
SUBSCRIBE devchannel testchannel
PSUBSCRIBE *itxiaoshen blog*

#发布信息
PUBLISH testchannel hello
PUBLISH productchannel hello
PUBLISH devchannel hello
PUBLISH new.itxiaoshen hello
PUBLISH sports.itxiaoshen hello
PUBLISH sports.xiaoshen hello
PUBLISH blog.csdn hello

#查看所有通道列表
PUBSUB CHANNELS

管道

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务,客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。服务端处理命令,并将结果返回给客户端。

管道一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。而当执行的命令较多时,这样的一来一回的网络传输所消耗的时间被称为RTT(Round Trip Time),显而易见,如果可以将这些命令作为一个请求一次性发送给服务端,并一次性将结果返回客户端,会节约很多网络传输的消耗,可以大大提升响应时间。

大量 pipeline 应用场景可通过 Redis 脚本(Redis 版本 >= 2.6)得到更高效的处理,后者在服务器端执行大量工作。脚本的一大优势是可通过最小的延迟读写数据,让读、计算、写等操作变得非常快(pipeline 在这种情况下不能使用,因为客户端在写命令前需要读命令返回的结果)。 Redis 中的脚本本身也就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。

Lua脚本

使用Lua优点

使用内置的 Lua 解释器,可以对 Lua(Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能) 脚本进行求值,Redis Lua脚本适合简单快速执行的业务,如果是复杂计算业务则会阻塞Redis server端的处理业务。

  • 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延。
  • 原子性:Redis 使用单个 Lua 解释器以原子性(atomic)的方式执行脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断, 这和使用MULTI/EXEC包围的事务很类似。
  • 复用:客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

EVAL命令

  • EVAL的第一个参数是一段 Lua 5.1 脚本程序。 这段Lua脚本不需要(也不应该)定义函数。它运行在 Redis 服务器中。
  • EVAL的第二个参数是参数的个数,后面的参数(从第三个参数),表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
  • 在命令的最后,那些不是键名参数的附加参数 arg [arg …] ,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second#使用了redis为lua内置的redis.call函数EVAL "redis.call(\'SET\', KEYS[1], ARGV[1]);redis.call(\'EXPIRE\', KEYS[1], ARGV[2]); return 1;" 1 good_price 99.00 300

SCRIPT 命令

#SCRIPT LOAD将一个脚本装入脚本缓存,但并不立即运行它
SCRIPT LOAD "redis.call(\'SET\', KEYS[1], ARGV[1]);redis.call(\'EXPIRE\', KEYS[1], ARGV[2]); return 1;"
#在脚本被加入到缓存之后,在任何客户端通过EVALSHA命令,可以使用脚本的SHA1校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止
EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 good_stock 1000 600
#SCRIPT EXISTS根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存 
SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345

#SCRIPT FLUSH清除所有脚本缓存 
SCRIPT FLUSH
#SCRIPT KILL杀死当前正在运行的脚本

Lua脚本文件执行示例

创建mytest.lua脚本文件

--- 获取key
local key = KEYS[1]
--- 获取value
local val = KEYS[2]
--- 获取一个参数
local expire = ARGV[1]
--- 如果redis找不到这个key就去插入
if redis.call("get", key) == false then
    --- 如果插入成功,就去设置过期值
    if redis.call("set", key, val) then
        --- 由于lua脚本接收到参数都会转为String,所以要转成数字类型才能比较
        if tonumber(expire) > 0 then
            --- 设置过期时间
            redis.call("expire", key, expire)
        end
        return true
    end
    return false
else
    return false
end
#执行mytest.lua脚本文件
redis-cli --eval mytest.lua myKey myValue , 100

事务

定义

Redis 事务可以一次执行多个命令,将一系列的预定义命令放入队列,执行时按照添加顺序执行,redis 的事务更像是批量执行指令,有两个重要的保证:

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

加入事务的命令只是暂时存放在队列中,只有在执行了 exec 指令后才会被执行

命令

  • MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
  • EXEC:执行事务中的所有操作命令。
  • DISCARD:取消事务,放弃执行事务块中的所有命令。
  • WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH:取消WATCH对所有key的监视。

事务错误处理

使用事务时可能会遇上以下两种错误:

  • 事务在执行 EXEC 之前,入队的命令可能会出错,比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话);服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。

  • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行

为什么Redis不支持事务回滚?

多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,这些Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。

WATCH监视锁

严格的说Redis的命令是原子性的,而事务是非原子性的,Redis WATCH命令可以让事务具有回滚的能力。Redis使用WATCH命令来决定事务是继续执行还是回滚,那就需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。在WATCH之后,MULTI之前执行UNWATCH,则事务正常提交。

分布式锁

Redisson GitHub分布式锁使用示例 https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers

引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁,实际使用中我们也会基于redisson和spring框架的整合

maven pom依赖

<!-- https://mvnrepository.com/artifact/org.redisson/redisson --><dependency>    <groupId>org.redisson</groupId>    <artifactId>redisson</artifactId>    <version>3.16.3</version></dependency>
#配置Config config = new Config();config.useClusterServers()       // use "rediss://" for SSL connection      .addNodeAddress("redis://127.0.0.1:7181");#创建Redisson的实例RedissonClient redisson = Redisson.create(config);

简单锁的示例

RLock lock = redisson.getLock("myLock");// traditional lock methodlock.lock();// or acquire lock and automatically unlock it after 10 secondslock.lock(10, TimeUnit.SECONDS);// or wait for lock aquisition up to 100 seconds // and automatically unlock it after 10 secondsboolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);if (res) {   try {     ...   } finally {       lock.unlock();   }}

简单红锁的使用示例

RReadWriteLock rwlock = redisson.getReadWriteLock("myLock");RLock lock = rwlock.readLock();// orRLock lock = rwlock.writeLock();// traditional lock methodlock.lock();// or acquire lock and automatically unlock it after 10 secondslock.lock(10, TimeUnit.SECONDS);// or wait for lock aquisition up to 100 seconds // and automatically unlock it after 10 secondsboolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);if (res) {   try {     ...   } finally {       lock.unlock();   }}

Distributed locks:用Redis实现分布式锁管理器,分布式锁在很多场景中是非常有用,官方提供一个使用Redis实现分布式锁的Redlock算法,这种实现比普通的单实例实现更安全,下面为各种语言基于Redlock算法实现分布式锁。

面试题

Redis分布式锁实现思路?

  • 自旋锁:循环获取锁,类似CAS。
  • 原子性:可利用redis lua脚本的原子性。加锁过程可以利用set nx命令SET lock_key random_value NX PX 5000,判断key是否存在,不存在则设置key值并设置ttl时间。random_value是客户端生成的唯一的字符串(可使用雪花算法),NX代表只在键不存在时,才对键进行设置操作,PX设置键的过期时间为5000毫秒这里random_value取值作为客户端加锁的时间不宜过长过短。解锁的过程就是将Key键删除,但也不能乱删,不能说客户端1的请求线程里将客户端2的锁给删除掉,这时候就可以使用到random_value来实现。删除的时候可以通过lua脚本的原子性判断当前请求如果是对应客户端唯一标识字符串则将key删除。
  • 锁的延期:设置锁的时间比如为10秒,通过类似看门狗技术检查key的ttl值是否快要到期,重新设置或重置ttl的时间。
  • 上述几点主要是实现单台redis分布式锁的核心点,至于主从和集群可以参考上述红锁算法思想。

简单谈谈一致性哈希算法和Redis哈希槽?

一句话概括一致性哈希:就是普通取模哈希算法的改良版,哈希函数计算方法不变,只不过是通过构建环状的 Hash 空间代替普通的线性 Hash 空间。

数据存储的位置是沿顺时针的方向找到的环上的第一个节点,数据倾斜和节点宕机都可能会导致缓存雪崩。虚拟节点,就是对原来单一的物理节点在哈希环上虚拟出几个它的分身节点,这些分身节点称为「虚拟节点」。打到分身节点上的数据实际上也是映射到分身对应的物理节点上,这样一个物理节点可以通过虚拟节点的方式均匀分散在哈希环的各个部分,解决了数据倾斜问题。

redis 集群(cluster)并没有使用一致性哈希,而是采用了哈希槽(slot)的这种概念。主要的原因是一致性哈希算法的节点分布基于圆环,无法很好的手动控制数据分布,比如一个节点失效,把数据转移到下一个节点,容易造成缓存雪崩,而采用hash槽+副本节点失效的时候从节点自动接替,不易造成雪崩。

redis cluster 包含了16384个哈希槽,集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽,也即是每个 key 通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配,集群中的每一个节点负责处理一部分哈希槽。

Redis分区方案有哪些?

  • 客户端分区:由客户端决定数据被存储在哪个redis节点或者从哪个redis节点读取,大部分客户端都已实现了客户端分区。
  • 代理分区:客户端将请求发送给代理,有代理决定请求给哪些redis实例,然后根据Redis的响应结果返回给客户端,像Twemproxy就是redis一种代理实现。
  • 查询路由:客户端随机的请求到任意一个redis实例,然后由Redis将请求转给正确的redis实例节点。而Redis Cluster实现一种混合形式的Query routing,并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端直接存储所有实例的key存储分布信息,所以在客户端上就直接redirected到正确的redis节点。
  • Redis哨兵和codis也是redis高可用的解决方案。

Redis数据类型和底层数据结构的理解?

redis底层核心实现是一个双数组,hash 数组+链表,通过哈希冲突解决方法如链表法、再哈希。

type是约束api, object encoding是底层实现类型

数据类型:string、list、set、sortset、hash、hyperloglog、 stream 、geo

底层数据结构:哈希表、跳表、双向链表、压缩列表等

  • 简单字符串:SDS simple dynamic string 包含free len char数组 扩容为(len+addlen)*2,二进制安全、内存预分配、兼容C语言库、空间换时间 sdshdr 长度 分配空间 类型 char数组,获取到是char数组的地址,往前偏移可获取类型进而获取其他。

  • 哈希表:哈希表的制作方法一般有两种,一种是: 开放寻址法,一种是 拉链法。redis的哈希表的使用的是拉链法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ihhvn4XI-1634137212485)(image-20211013181140096.png)]

  • 双向链表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NIeAH9WR-1634137212486)(image-20211013181213164.png)]

  • 跳表:zset是一个有序集合、排行版功能,关于时间复杂度跳表少量数据的话误差比较大、大量数据的话接近olog(n),zset 的长度小于128或者key的长度小于64,ziplist压缩列表(字节数组),大于则使用skiplist。

  • 压缩列表: redis的列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的。和各种语言的数组类似,它是由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存。

Redis扩容时机?

Redis扩容是使用两个哈希表分多次渐进式rehash和动态扩容机制。当used大于size扩容,排除场景包括持久化、lua事务阻塞,如果大于5size则直接扩容,翻倍扩容如4-8-16,2指数主要方便位运算,可以将取模转为位运算,采用头插法,当used<=size*0.1时候进行缩容;redis扩容采用渐进式rehash的方式,redis CRUD每操作一次rehash一次,每毫秒100个数组槽位。

Redis同步机制?

Redis同步机制分为全量复制和增量复制。全同步是指slave启动时进行的初始化同步。 增量复制是指Redis运行过程中的修改同步。

  • 全同步过程如下:
    • 在slave启动时,会向master发送一条SYNC指令。
    • master收到这条指令后,会启动一个备份进程将所有数据写到rdb文件中去。
    • 更新master的状态(备份是否成功、备份时间等),然后将rdb文件内容发送给等待中的slave。
    • 注意,master并不会立即将rdb内容发送给slave。而是为每个等待中的slave注册写事件,当slave对应的socket可以发送数据时,再讲rdb内容发送给slave。
  • 当Redis的master/slave服务启动后,首先进行全同步。之后,所有的写操作都在master上,而所有的读操作都在slave上。因此写操作需要及时同步到所有的slave上,这种同步就是部分同步。 部分同步过程如下:
    • master收到一个操作,然后判断是否需要同步到salve。
    • 如果需要同步,则将操作记录到aof文件中。
    • 遍历所有的salve,将操作的指令和参数写入到savle的回复缓存中。
    • 一旦slave对应的socket发送缓存中有空间写入数据,即将数据通过socket发出去。

redis过期策略和淘汰策略、持久化机制?

  • 过期策略

    • 定时过期
    • 惰性过期。
    • 贪心策略:redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,间隔100ms随机抽取20个key。
  • 淘汰策略:Redis官方给的警告,当内存不足时,Redis会根据配置的缓存策略淘汰部分keys,以保证写入成功。当无淘汰策略时或没有找到适合淘汰的key时,Redis直接返回out of memory错误。

    • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
    • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
    • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
    • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。
    • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
    • no-enviction(驱逐):禁止驱逐数据。
  • 持久化机制

    • RDB快照(snapshot):在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置, 让它在“N 秒内数据集至少有 M 个改动”这一条件被满足时,自动保存一次数据集。
    • AOF(append-only file):快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。Redis 增加了一种完全耐久的持久化方式: AOF 持久化,将修改的每一条指令记录进文件你可以通过修改配置文件来打开 AOF 功能。
    appendonly yes
    
    • 混合持久化:Redis 4.0之后带来了一个新的持久化选项,混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将aof_rewrite_buf重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图

在redis重启的时候,加载 aof 文件进行恢复数据:先加载 rdb 内容再加载剩余的 aof。混合持久化配置:

aof-use-rdb-preamble yes  # yes:开启,no:关闭

说说Redis网络IO和单线程为何能支持高并发?

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。Redis采用网络IO多路复用技术来保证在多连接的时候,系统的高吞吐量。多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

Redis采用单线程为何支持高并发?

  • Redis使用的内存IO,不是磁盘IO,大大降低了IO时间
  • Redis单线程,无需去考虑多线程造成的死锁问题
  • Redis单线程,底层网络IO模型使用多路复用epoll方式(如果内核不支持epoll,可自动切换到select或者poll,看配置信息可进行修改)

Redis6实现的多线程,只是对网络IO读写处理做多线程处理,但是对命令行的操作仍然是单线程的。这样即加快了IO处理效率,又保证了原子性。

简单说说Redis协议?

Redis客户端和服务端之间使用一种名为RESP(REdis Serialization Protocol)的二进制安全文本协议进行通信,属于请求-响应模型。

#用SET命令来举例说明RESP协议的格式。
SET mykey "Hello"
#实际发送的请求数据:
*3\\r\\n$3\\r\\nSET\\r\\n$5\\r\\nmykey\\r\\n$5\\r\\nHello\\r\\n
#实际收到的响应数据:
+OK\\r\\n

RESP设计的十分精巧,下面是一张完备的协议描述图。

请说说对于缓存预热、缓存穿透、缓存雪崩、缓存击穿、缓存更新、缓存降级的理解?

  • 缓存穿透

    • 定义

      • 当查询Redis中没有的数据时,该查询会下沉到数据库层,同时数据库层也没有该数据,当这种情况大量出现或被恶意攻击时,接口的访问全部透过Redis访问数据库,而数据库中也没有这些数据,我们称这种现象为"缓存穿透"。缓存穿透会穿透Redis的保护,提升底层数据库的负载压力,同时这类穿透查询没有数据返回也造成了网络和计算资源的浪费。

    • 解决方案:

      • 在接口访问层对用户做校验,如接口传参、登陆状态、n秒内访问接口的次数;
      • 利用布隆过滤器,将数据库层有的数据key存储在位数组中,以判断访问的key在底层数据库中是否存在;核心思想是布隆过滤器,在redis里也有bitmap位图的类似实现,布隆过滤器过滤器不能实现动态删除,有时间可以研究下布谷鸟过滤器,是布隆过滤器增强版本。布隆过滤器有误判率,虽然不能完全避免数据穿透的现象,但已经可以将99.99%的穿透查询给屏蔽在Redis层了,极大的降低了底层数据库的压力,减少了资源浪费。
        • 基于布隆过滤器,我们可以先将数据库中数据的key存储在布隆过滤器的位数组中,每次客户端查询数据时先访问Redis:
        • 如果Redis内不存在该数据,则通过布隆过滤器判断数据是否在底层数据库内;
        • 如果布隆过滤器告诉我们该key在底层库内不存在,则直接返回null给客户端即可,避免了查询底层数据库的动作;
        • 如果布隆过滤器告诉我们该key极有可能在底层数据库内存在,那么将查询下推到底层数据库即可;

  • 缓存击穿

    • 定义

      • 缓存击穿和缓存穿透从名词上可能很难区分开来,它们的区别是:穿透表示底层数据库没有数据且缓存内也没有数据,击穿表示底层数据库有数据而缓存内没有数据。当热点数据key从缓存内失效时,大量访问同时请求这个数据,就会将查询下沉到数据库层,此时数据库层的负载压力会骤增,我们称这种现象为"缓存击穿"。
    • 解决方案

      • 延长热点key的过期时间或者设置永不过期,如排行榜,首页等一定会有高并发的接口;
      • 利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至Redis内,避免其他大量请求同时穿过Redis访问底层数据库;

  • 缓存雪崩

    • 定义
      • 缓存雪崩是缓存击穿的"大面积"版,缓存击穿是数据库缓存到Redis内的热点数据失效导致大量并发查询穿过redis直接击打到底层数据库,而缓存雪崩是指Redis中大量的key几乎同时过期,然后大量并发查询穿过redis击打到底层数据库上,此时数据库层的负载压力会骤增,我们称这种现象为"缓存雪崩"。事实上缓存雪崩相比于缓存击穿更容易发生,对于大多数公司来讲,同时超大并发量访问同一个过时key的场景的确太少见了,而大量key同时过期,大量用户访问这些key的几率相比缓存击穿来说明显更大。
    • 解决方案
      • 在可接受的时间范围内随机设置key的过期时间,分散key的过期时间,以防止大量的key在同一时刻过期;
      • 对于一定要在固定时间让key失效的场景(例如每日12点准时更新所有最新排名),可以在固定的失效时间时在接口服务端设置随机延时,将请求的时间打散,让一部分查询先将数据缓存起来;
      • 延长热点key的过期时间或者设置永不过期,这一点和缓存击穿中的方案一样;

  • 缓存预热

    • 如字面意思,当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至Redis内再提供出去使用,这种操作就成为"缓存预热"。
    • 缓存预热的实现方式有很多,比较通用的方式是写个批任务,在启动项目时或定时去触发将底层数据库内的热点数据加载到缓存内。
  • 缓存降级

    • 缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,即使是有损部分其他服务,仍然需要保证主服务可用。可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。
    • 降级的目的是保证核心服务可用,即使是有损的。如去年双十一的时候淘宝购物车无法修改地址只能使用默认地址,这个服务就是被降级了,这里阿里保证了订单可以正常提交和付款,但修改地址的服务可以在服务器压力降低,并发量相对减少的时候再恢复。
    • 降级可以根据实时的监控数据进行自动降级也可以配置开关人工降级。是否需要降级,哪些服务需要降级,在什么情况下再降级,取决于大家对于系统功能的取舍。
  • 缓存更新

    • 缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。

    Redis缓存数据如何保证与Mysql的一致性

    采用延时双删策略,在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:

    • 先删除缓存

    • 再写数据库

    • 休眠N毫秒(根据具体的业务时间来定)

    • 再次删除缓存。

    • 休眠N毫秒怎么确定

      • 需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒
    • 设置缓存的过期时间

      • 从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存
      • 结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时
    • 如何写完数据库后,保证再次删除缓存成功?

      • 上述的方案有一个缺点,那就是操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。这里,我们需要提供一个保障重试的方案。
      • 方案一具体流程(但该方案有一个缺点,对业务线代码造成大量的侵入)
        • 更新数据库数据;
        • 缓存因为种种问题删除失败;
        • 将需要删除的key发送至消息队列;
        • 自己消费消息,获得需要删除的key;
        • 继续重试删除操作,直到成功。
      • 方案二具体流程
        • 更新数据库数据;
        • 数据库会将操作信息写入binlog日志当中;
        • 订阅程序提取出所需要的数据以及key;
        • 另起一段非业务代码,获得该信息;
        • 尝试删除缓存操作,发现删除失败;
        • 将这些信息发送至消息队列;
        • 重新从消息队列中获得该数据,重试操作。

    以上方案都是在业务中经常会碰到的场景,可以依据业务场景的复杂和对数据一致性的要求来选择具体的方案

    redis技术点非常多,本章主要对redis有一个全局的理解,后续有时间我们再深入理解redis其他内容,包括客户端框架原理和运用。

阿里大厂面试:2亿条数据需要缓存,如何设计这个存储方案?

对于2亿条数据需要缓存,使用单机肯定是不可能了,至少是分布式存储。

而分布式存储我们可以选择的选项有很多,今天我们单独来讨论下redis的解决方案。

使用redis如何落地。

在阿里p7工程案例和场景设计中,这一类的确是必问题,我们一般有三种解决方案:

第一种: 哈希取余算法


2亿条记录假设2亿个k,v,我们单机不行必须要分布式多机,假设有3台机器构成一个集群,用户每次读写操作都是根据公式:

hash(key) % N个机器台数,计算出哈希值,用来决定数据映射到哪一个节点上。

比如0,就是最左的,1是中间的,2是最右边的。

这种方案是最常用的,也是最通用的

优点:
简单粗暴,直接有效,只需要预估好数据规划好节点,例如3台、8台、10台,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡+分而治之的作用。

缺点:
原来规划好的节点,进行扩容或者缩容就比较麻烦了,不管扩缩,每次数据变动导致节点有变动&#

以上是关于Redis分布式缓存剖析及大厂面试精髓v6.2.6的主要内容,如果未能解决你的问题,请参考以下文章

阿里大厂面试:2亿条数据需要缓存,如何设计这个存储方案?

Java入门:java分布式缓存技术

Java大厂技术面试题汇总!字节跳动java面试几轮

大厂Java高级多套面试专题整理集合,已获万赞

java面试线程并发,都是精髓!

Redis介绍及安装