• 【Java知识体系】Redis实用教程,深入原理


    大家好!我是未来村村长,就是那个“请你跟我这样做,我就跟你这样做!”的村长👨‍🌾!

    ||To Up||👩‍🌾

    未来村村长正推出一系列【To Up】文章,该系列文章重要是对Java开发知识体系的梳理,关注底层原理和知识重点。”天下苦八股文久矣?吾甚哀,若学而作苦,此门无缘,望去之。“该系列与八股文不同,重点在于对知识体系的构建和原理的探究。

    文章目录

    一、概述与安装🐧

    1、Redis概述

    (1)概述

    ​ Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

    ​ Redis数据都是缓存在内存,会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

    (2)相关知识

    ​ Redis默认端口6379,默认有16个数据库,可以使用select <dbid>来切换数据库。所有的库使用同一个密码,可以通过以下命令操作数据库(删库跑路)。

    flushdb #清空当前数据库
    flushall #清空所有数据库
    dbsize #查看当前数据库key数量
    
    • 1
    • 2
    • 3

    ​ Redis支持多种数据类型,支持数据持久化,单线程+多路IO复用

    2、Redis安装

    (1)下载压缩文件

    ​ 下载地址:https://redis.io/download/

    ​ 下载版本:

    在这里插入图片描述

    (2)上传压缩文件

    ​ 使用Xftp连接到Linux操作系统,将Redis压缩文件上传到/opt目录下

    在这里插入图片描述

    (3)安装gcc编译器

    ​ Redis安装需要有gcc编译环境,我们使用yum命令进行安装

    yum install gcc 
    
    • 1

    ​ 查看gcc版本

    gcc --version
    
    • 1

    (4)解压安装

    ① 进入到目录

    cd /opt
    
    • 1

    ② 查看目录文件

    ls
    
    • 1

    ③ 解压压缩文件

    tar -zxvf redis-6.2.7.tar.gz
    
    • 1

    ④ 到解压文件中执行make,进行编译

    cd redis-6.2.7
    
    • 1
    make
    
    • 1

    ⑤ 执行make install,进行安装

    make install
    
    • 1

    ⑥ 查看安装

    cd /usr/local/bin
    
    • 1
    ll
    
    • 1

    在这里插入图片描述

    ​ 查询到以上信息,说明安装成功。

    3、Redis启动

    (1)前台启动[不推荐]

    ​ 前台启动后,不能再进行Linux命令操作,必须关闭Redis才能进行其它操作,所以推荐使用后台启动

    ① 启动

    redis-server
    
    • 1

    在这里插入图片描述

    ② 退出:Ctrl+C

    (2)后台启动

    ① 修改配置文件

    ​ 到redis-6.2.7文件夹中,将配置文件redis.conf复制到etc目录下

    cp redis.conf /etc/redis.conf
    
    • 1

    ​ 在etc目录下修改配置文件

    vi redis.conf
    
    • 1

    ​ 将daemonize no 改成yes

    #/daem进行搜索
    #inseert进行输入
    daemonize yes
    #Esc退出输入
    :wq
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ② 后台启动

    ​ 到/usr/local/bin目录下,输入命令进行启动

    redis-server /etc/redis.conf
    
    • 1

    ​ 然后通过命令查看启动情况

    ps -ef | grep redis
    
    • 1

    在这里插入图片描述

    ③ Redis操作

    ​ 因为是后台启动,所以关闭了shell窗口也不会导致redis关闭,我们可以通过以下命令进行Redis的操作。

    redis-cli
    
    • 1

    ④ Redis关闭

    ​ 启动后,如果需要关闭,可以通过以下命令进行关闭

    shutdown
    #或通过kill命令,杀掉进程
    ps -ef | grep redis #查看运行id
    kill -9 2529005
    
    • 1
    • 2
    • 3
    • 4

    二、数据类型🐈

    1、String字符串

    (1)介绍与底层结构

    ​ String是Redis最基本的数据类型,且是二进制安全的,String可以包含任何数据,比如jpg图片或者其它序列化对象。一个Value的String字符串大小不能超过512M。

    (2)常用操作

    ① 增删改查

    # 增加key-value
    set key value
    
    #获得value
    get key
    
    #查看是否存在
    exists key
    
    #删除key-value
    del key
    
    #更新key
    set key value2
    
    #如果key不存在则进行创建
    setnx key value
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    ② 批量操作

    #批量插入
    mset key1 value1 key2 value2 key3 value3
    
    #批量获取
    mget key1 key2 key3
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ③ 过期设置

    #设置key存活时间为5s
    expire key 5
    
    #set + expire
    setex key 5 value
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ④ 计数

    #自增:当value为整数时才能自增
    incr key
    
    #增加指定值:例如增加10086
    incrby key 10086
    
    #减少指定值
    incrby key -5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2、List列表

    (1)介绍与底层结构

    ​ Redis列表相当于Java语言中的LinkedList,是一个双向链表。但是又不是一个简单的LinkedList,而是QuickList。在列表元素较少的情况下,会使用一块连续的内存存储,这个结构是ZipList。多个ZipList用双向指针串联起来形成QuickList。相较于普通双向链表,降低了内存的碎片化,节省空间。

    在这里插入图片描述

    (2)常用操作

    ① 队列操作:右进左出[先进先出]

    #右进
    rpush keys value1 value2 value3
    
    #左出:先删除value1
    lpop keys
    
    #统计长度
    llen
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ② 栈操作:右进右出[先进后出]

    #右进
    rpush keys value1 value2 value3
    
    #右出:先删除value3
    rpop keys
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ③ 含索引操作[慢操作]

    #取到第n个值:从0开始索引
    lindex key n
    
    #取出所有元素
    lrange key 0 -1
    
    #取出m到n元素
    lrange key m n
    
    #清除m到n元素:n为负数则倒序
    ltrim key m n
    
    #清除所有元素:n-m为负数
    ltrim key 1 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3、Hash字典

    (1)介绍与底层结构

    ​ Redis字典数据结构类似Java中的HashMap<String,Object>,是无序字典,结构为“数组+链表”。每个存储单元为下图结构:key = {[field1,value1],[field2,value2],[field3,value3]}。

    在这里插入图片描述

    (2)常用操作

    ① 增删改查

    #增加
    hset key1 field1 "value1"
    hset key1 field2 "value2"
    hset key1 field3 "value3"
    
    #获取全部
    hgetall key1
    
    #获取指定field的value
    hget key1 field3
    
    #获取长度
    hlen key1
    
    #更新指定field的value
    hset key1 field3 "newValue3"
    
    #批量设置
    hmset key1 field1 "value1" field2 "value2" field3 "value3"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ② 计数自增

    hset key field 10
    #增加n
    hincrby key field n
    
    • 1
    • 2
    • 3

    4、Set集合

    (1)介绍与底层结构

    ​ Redis集合结构类似Java中的HashSet,内部无序,value唯一。

    (2)常用操作

    #增加
    sadd key value1
    sadd key value2
    
    #获取key的所有calue
    smembers key
    
    #查询key中某个value是否存在
    sismenmber key value
    
    #获取key的长度
    scard key
    
    #随机弹出key的一个value
    spop key
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    5、Zset有序集合

    (1)介绍与底层结构

    ​ Zset是有序集合,其value的值唯一,但是会为每一个value赋值权重,通过权重来实现有序。其内部实现是跳跃列表。跳跃列表将数据分为几个层次,每一层都是链表结构,从第0层开始,每次向下一层减少50%的元素个数,每次查询从最高层开始向下查询。[类似sql一个页面中的插槽和数据分组]

    在这里插入图片描述

    (2)常用操作

    #增加
    zadd key 1 value1
    zadd key 2 value2
    
    #删除
    zrem key value1
    
    #获取key所有值[升序]
    zrange key 0 -1
    
    #获取key所有制[降序]
    zrevrange key 0 -1
    
    #获取key长度
    zcard key
    
    #获取指定value的score
    zscore key value1
    
    #获取指定value的排名
    zrank key value1
    
    #根据score分值区间获取对应的value(n,m]
    zrangebyscore key n m
    
    #获取value和score:inf代表无穷大,-inf无穷小
    zrangebyscore key -inf inf withscores
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    三、SpringBoot整合🦧

    1、Redis配置

    (1)设置访问配置

    ​ 我们到之前的/etc目录下,通过vi修改redis.conf文件。

    vi redis.conf
    
    • 1

    ​ 具体需要修改以下内容,一是将bind[访问ip限制]注释掉,二是将保护模式关闭。(可以通过[/+搜索内容]进行搜索)

    #bind 127.0.0.1 -::1 #这条需要注释
    protected-mode no #之前是yes,改成no
    :wq #保存退出
    
    • 1
    • 2
    • 3

    ​ 修改配置后,需要重新启动Redis。

    shutdown
    ps -ef | grep redis #查看redis的6379端口对应进程是否运行
    
    • 1
    • 2

    ​ 到/usr/local/bin目录下,输入命令进行启动

    redis-server /etc/redis.conf
    
    • 1

    (2)设置密码

    ​ Redis默认没有密码,但是不修改密码的话,可能会被别人恶意操作。我们可以通过redis-cli进入交互控制行进行修改,具体设置命令如下。在命令中设置密码,只是临时的,若需要永久设置,则需要到配置文件中进行设置。

    config get requirepass
    config set requirepass "123456"
    auth "123456"
    
    • 1
    • 2
    • 3

    (3)设定内存使用大小

    ​ 同样在redis.conf文件,我们需要修改maxmemory属性来设置redis可以使用的内存量。若达到内存上限,Redis会试图清除内部数据,移除规则可通过设置maxmemory-policy来指定。

    ​ maxmemory-policy有以下规则:

    • volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
    • allkeys-lru:在所有集合key中,使用LRU算法移除key
    • volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
    • allkeys-random:在所有集合key中,移除随机的key
    • volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
    • noeviction:不进行移除。针对写操作,只是返回错误信息

    (4)添加防火墙规则

    ​ 如果使用的是云服务器,则需要开放6379端口,也可以在linux系统中中设置防火墙端口号的开启,否则会出现"connect timed out"连接超时错误。

    在这里插入图片描述

    2、Jedis

    (1)Maven依赖

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.1.0</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)Jedis的创建与连接

    public static void main(String[] args) {
        	//创建Jredis对象
            Jedis jedis = new Jedis("127.0.0.1",6379);
            //输出PONG,redis连通成功
            System.out.println(jedis.ping());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    (3)Jedis操作Redis

    ​ Jedis封装了redis-cli中的所有指令操作,只需要通过Jedis实例对象进行操作即可。

    ​ 这篇文章对Jedis操作Redi的API进行了整理:https://blog.csdn.net/fanbaodan/article/details/89047909

    3、RedisTemplate[SpringBoot整合]

    (1)Maven依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4

    (2)连接信息配置

    连接信息配置

    spring:
        redis:
          host: 127.0.0.1 #主机
          port: 6379	  #端口
          password:       #密码:默认为空
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ​ RedisTemplate默认使用的不是jedis,而是lettuce。我们可以通过配置进行修改:

    spring:
        redis:
          client-type: jedis
    
    • 1
    • 2
    • 3

    ​ 然后配置相应type的属性,比如配置jedis的

    spring:
        redis:
          jedis:		  #jedis配置
            pool:
              max-active: 8 #最大连接数
              max-wait: -1	#最大阻塞等待时间
              max-idle: 500 #最大空闲连接
              min-idle: 0   #最小空闲连接
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ​ 或者配置lettuce,lettuce整体好于jedis,底层基于Netty实现不存在线程安全问题。

    spring:
        redis:
          lettuce:		  #jedis配置
            pool:
              max-active: 8 #最大连接数
              max-wait: -1	#最大阻塞等待时间
              max-idle: 500 #最大空闲连接
              min-idle: 0   #最小空闲连接
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (3)使用

    @Controller
    public class RedisController{
        @Autowired
        private RedisTemplate redisTamplate;
        
        @RequestMapping("set")
        public void set(){
            //不同的数据类型有不同的执行对象
            ValueOperations vOps = redisTamplate.opsForValue();
            //redisTamplate.opsForxxx来获取指定类型的执行对象
            vOps.set("key","value");
        }
        
        @RequestMapping("get")
        public void get(){
            //不同的数据类型有不同的执行对象
            ValueOperations vOps = redisTamplate.opsForValue();
            //redisTamplate.opsForxxx来获取指定类型的执行对象
            Object value = vOps.get("key");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    四、网络模型与通信协议🐅

    1、网络模型

    (1)阻塞IO

    ​ 数据的读取可以分为两个阶段:一是等待内核空间数据准备完毕,二是进行数据的读取。

    ​ 阻塞IO指的是数据读取的两个阶段都必须等待阻塞,这里的等待指的是:

    • 在内核空间没有数据时,进程需要等待内核空间去拷贝磁盘中的数据
    • 读取数据时,需要等待用户空间拷贝内核空间相应的数据

    (2)非阻塞IO

    ​ 当进程要进行磁盘数据读取时,会查看内核空间是否存在相应数据,若没有则直接返回失败信息。

    ​ 用户进程会持续进行数据访问,直到内核空间中拷贝了磁盘中的数据,此时会等待用户空间拷贝内核空间的数据,然后返回相应的数据结果。

    (3)IO多路复用

    ​ 在阻塞IO或非阻塞IO中,当服务端处理客户端Socket请求时,在单线程情况下,只能依次处理每一个Socket请求,如果正在处理的Socket恰好未就绪,线程就会被阻塞,所有其它客户端Socket请求都必须等待,性能较差。

    ​ IO多路复用:利用单个线程来同时监听多个关联Socket关联的FD(文件描述符,File Descriptor),当FD可读写时通知单线程进行相关的请求操作。通知方式有select、poll、epoll。

    ​ FD:从0开始递增的无符号整数,用来关联Linux中的文件,在Linux中一切皆文件,包括Socket。

    2、Redis的单线程

    ​ Redis对于命令的处理是单线程执行,但在Redis4.0和Redis6.0后不同程度地引入了多线程操作。例如Redis4.0引入了多线程来异步处理像unlink这样耗时较长的工作,Redis6.0的核心网络模型引入了多线程来提高多核CPU的利用率。

    (1)为什么选择单线程?

    ​ 最主要的原因是Redis是纯内存操作,执行速度非常快,其性能瓶颈是网络延迟,而不是执行速度。若采用多线程会导致过多的上下文切换,带来不必要的开销。且多线程还会带来线程安全问题,需要进行同步设置,这会降低性能。

    (2)使用的是什么网络模型?

    ​ 使用IO多路复用+事件派发的网络模型进行Socket请求的处理也是Redis较快的原因之一。

    ​ 事件派发:事件派发指的是提前定义多个事件处理器,将相应的事件交给相应的处理器处理。

    在这里插入图片描述

    ​ 即Redis使用一个线程来监听客户端发来的Socket请求,使用IO多路复用来遍历接收到的所有Socket请求对应的文件描述符,监听到相应可以处理的事件后,进行事件派发。Redis定义了事件处理器来进行相应的事件处理(比如连接应答处理器、命令请求处理器、命令回复处理器)。

    ​ ps:图中标记[多线程],指的是Redis6.0以后的网络模型线程使用变化。

    3、通信协议

    ​ 从上节图中我们已经知道Redis是如何接收请求、执行命令和返回请求的了。那与Redis服务器进行请求和响应就必须采用统一的文件格式规范,这个规范就是通信协议。

    ​ Redis采用的是RESP协议(Redis Serialization Protocal)。

    五、持久化🦘

    ​ Redis的数据全部存放在内存中,如果突然宕机,则会导致数据全部丢失。因此Redis采用两种持久化机制:快照和AOF日志来保障数据安全。

    1、快照(RDB持久化)

    ​ RDB(Redis DataBase)持久化是把当前Redis中全部数据生成快照保存在硬盘上。快照是一次全量备份,是内存数据的二进制序列化。指在指定的时间间隔内将内存中的数据集快照写入磁盘。

    ​ 手动触发:

    save #会导致Redis服务阻塞
    bgsave #不会导致REdis服务阻塞,只会在调用fork命令时阻塞
    
    • 1
    • 2

    ​ 自动触发:

    • 若redis.conf配置中设置了save m n,即在m秒内有n次修改操作,则会自动触发bgsave操作
    • 执行shutdown时,若没有开启AOF持久化,会自动触发bgsave操作
    • 从节点做全类复制时,主节点会自动进行bgsave操作,然后将更新的RDB文件发送给从节点

    (1)持久化流程

    ​ Redis是单线程程序,如果需要一边处理请求,一边进行文件IO持久化[文件IO不能使用多路复用],那会忙不过来,这里使用的是操作系统的COW机制(Copy On Write,写时复制机制)来实现快照持久化。持久化流程如下:

    在这里插入图片描述

    • Redis调用glibc(linux系统中最底层的api)的函数fork创建一个子进程,将快照持久化交给该进程处理,父进程Redis继续处理客户端请求。子进程与父进程共享内存数据。
      • fork的作用就是复制一个与当前完全一致的进程,作为原进程的子进程。
    • 子进程会根据Redis进程的内存生成快照文件,替换原有的RDB文件(dump.rdb,即存储在磁盘中的数据文件)。此时父进程若修改数据,会通过写时复制机制机进行修改。
      • 写时复制机制:指的是当父进程需要修改数据时,会将被修改的页面复制一份出来进行修改,若父进程在持久化期间持续进程页面修改,则会导致占用内存不断增加。
    • 持久化过程结束后,子进程通过信号量通知Redis进程操作已经完成

    (2)缺点

    • fork操作的成本较高,写时复制机制来修改页面会占用一定的内存空间
    • 最后一次RDB持久化操作可能导致数据丢失,即在下一次持久化操作前,服务器宕机了,此时的数据不会保存。

    2、AOF日志

    ​ AOF日志是连续的增量备份,记录的是内存数据修改的指令记录文本。AOF会随着系统的运行而不断增大,数据库每次重启都需要加载AOF日志,所以需要定期维护AOF。

    AOF的开启:到redis.conf配置文件进行修改,主要修改以下内容。如果RDB和AOF同时开启,系统会默认使用AOF日志的数据。

    appendonly yes
    
    • 1

    (1)持久化流程

    在这里插入图片描述

    • 客户端的请求写命令会被append追加到AOF缓冲区内
    • AOF缓冲区根据AOF持久化策略将操作sync同步到磁盘AOF文件中
    • AOF文件大小如果超过重写策略或进行手动重写时,会进行bgrewriteaof操作,压缩AOF文件容量
      • Redis提供了bgrewriteaof指令对AOF日志进行瘦身,会开启一个子进程对内存进行遍历,转换成一系列Redis操作指令,序列化到新的AOF文件中。
      • 在序列化过程发生的操作会追加到这个新的AOF文件中,追加完毕后执行文件的替换,完成瘦身工作。

    (2)AOF同步频率设置

    ​ 可在redis.conf配置文件进行AOF配置频率的修改。

    #立即同步,性能较差
    appendfsync always
    #每秒同步,最后一秒的数据可能丢失
    appendfsync everysec
    #关闭自动同步,同步时机交给操作系统
    appendfsync no
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    (3)文件修复与重写

    AOF的修复:若AOF文件[appendonly.aof]损坏了我们可以使用以下命令进行修复。

    redis-check-aof --fix
    
    • 1

    3、混合持久化

    ​ Redis4.0后增加了一个新的选项来支持使用RDB和AOF混合持久化,持久化使用RDB来完成,但是持久化期间发生的数据变化不使用写时复制技术,而是通过AOF日志来进行记录。

    ​ 当Redis重启时,可以先加载RDB的文件内容,然后重放增量AOF日志。

    六、事务和锁🦙

    ​ Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行过程中,不会被其他客户端发送的命令请求打断。Redis事务的主要作用就是串联多个命令,防止别的命令插队。

    ​ MySQL中事务的开启是通过begin、commit和rollback完成。Redis中也有一套这样的指令。我们可以看到没有rollback,说明Redis不支持事务的回滚。

    multi #事务的开始
    exec  #事务的执行
    discard #事务的丢弃
    
    • 1
    • 2
    • 3
    • multi代表事务的开启,执行后会将后续指令存放在事务缓存队列中,直到执行exec或discard
    • 所有的命令在执行exec前不执行,缓冲在事务队列中,服务器一旦收到exec指令,则开始执行整个事务队列。
    • discard可丢弃事务缓存队列的所有指令,在discard之后,会丢弃整个事务队列中的指令

    1、非原子性

    ​ 我们来看以下一系列指令。

    multi
    set key value
    incr key
    set key2 value2
    exec
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ​ 其对应的执行结果为

    OK
    (error) ERR value is not an integer or out range
    OK
    
    • 1
    • 2
    • 3

    ​ 然后我们执行以下操作,依然能得到value这个值,这代表尽管事务队列中存在错误指令,但是除了错误指令以外的指令依旧能执行成功。

    get key
    
    • 1

    ​ 所以Redis事务不具备原子性,只实现了指令执行的顺序性和隔离性。

    2、并发问题解决[锁]

    (1)乐观锁-watch

    ​ watch是数据的版本监视器,会在事务开始之前盯住一个或多个关键变量。当事务执行exec指令时,Redis会先检查被watch监视的对象是否被修改了。如果在事务开启期间,exec执行之前,关键数据被改动,则exec指令会直接返回NULL,即告诉客户端事务执行失败,客户端需要重新执行该事务。

    set key 100
    watch key
    
    #线程一
    multi
    incrby key 10
    
    #线程二
    incrby key 10
    
    #线程一
    exec #返回nil(NULL)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ​ Redis禁止在multi和exec之间执行watch指令,必须在multi之前盯住关键变量。

    (2)悲观锁-分布式锁

    ① 上锁与锁的释放

    ​ 分布式锁有很多实现方式,我们此处介绍基于Redis缓存的实现方式。

    ​ 可以使用setnx进行锁的操作,即每次要设置一个lock(key),或者通过set对key进行修改时,都先检查是否key是否存在,若存在则不能修改,则无法进行后续操作实现上锁。释放锁的方式是将key删除。

    setnx lock value1 #设置锁,执行成功返回true,失败返回null
    del key #释放锁
    
    • 1
    • 2

    ​ setnx相当于门钥匙,lock相当于门,redis相当于厕所。一个线程执行setnx lock,就相当于拿钥匙把门打开,进去享用厕所(redis),此时别的线程想通过setnx lock打开门是打不开的因为已经有人了,就不能上厕所(使用redis执行后续操作)。

    ​ 还可以通过以下指令设置锁的时间,即在指定时间间隔后,将该lock(key)进行删除,Redis提供了以下原子性操作。

    set lock value ex 10 nx #10秒后会del key
    #nx表示锁,ex表示expire设定存活时间
    
    • 1
    • 2
    ② 锁释放冲突-版本号

    ​ 上锁以后,如果删除锁的操作并发执行了,就会导致一个线程在持有锁的时候,该锁被其他线程删除了。

    ​ 比如线程1设置了5s的锁并且设置了del动态删除锁的操作,此时线程1忽然卡顿了5s,这时锁已经删除了,线程2拿到锁。线程2执行完后,线程1又执行了del,把线程2操作的锁给删除了。这就是并发下的删除冲突问题。

    ​ 我们可以为锁添加一个id值,在释放锁的时候判断lockid的值是否为当前设置锁的值,如果一样可以释放,如果不一样则不能进行释放。即自己才能释放自己的锁,且不能释放其它线程的锁。

    setnx lock lockid
    
    #jedis解锁
    if(jedis.get("lock")=="lockid"){
    	jedis.del("lock");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    ③ 删除操作的原子性-lua脚本

    ​ 上述操作只是将锁通过版本号进行隔离,但是锁的自动失效还是会影响锁的误删操作。

    //定义lua脚本
    String script = 
    	"if redis.call('get',KEYS[1])==ARGV[1] then 
        	return redis.call('del',KEYS[1]) 
        else 
            return 0 
        end";
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ​ Lua脚本能够确保操作的原子性,可以保证连续的多个指令原子性执行。这就能保证,要想执行锁的删除就必须执行lockid值的判断。这样就能确保当一个线程释放锁后,另一个线程才能拿到锁。

    七、主从集群🦏

    ​ Redis定义了两个角色,一是Master主人,二是Slave奴隶。两者代表着不同的Redis进程之间可以建立主从关系。一个Master可以有多个Slave,Slave负责不断更新获取主人的数据信息,并在主人挂掉时(MasterRedis服务挂掉)时,反客为主成为新的主人,干主人干的事。

    ​ 一般Slave Redis服务器负责读取操作,Master服务器负责写操作,这样可以减轻主节点服务器的负担,并且增加数据的安全性,并且Slave Redis也可以拥有自己的Slave,但Master永远只有一个。

    在这里插入图片描述

    ​ 我们可以通过以下指令将一个Redis进程设置为另一个Redis进程的奴隶(从属服务器)。

    slaveof 主机IP地址 端口号 #例如slaveof 127.0.0.1 6379
    
    • 1

    ​ 设置后我们可以通过info指令查看相关信息

    info replication #主从复制相关信息
    info persistence #持久化信息
    info cluster #集群信息
    info #获取所有信息
    
    • 1
    • 2
    • 3
    • 4

    1、主从复制[同步]

    ​ Redis的主从同步主要通过两个方式完成,一是增量同步,二是快照同步。

    ​ 当一个Redis服务器,刚通过slaveof成为从属节点时,会先进行快照同步,再进行增量同步。

    ​ 同时Redis还支持无盘复制,主服务器可以直接通过套接字一边遍历内存,一边将序列化的快照内容发送到从节点,从节点先将快照内容存到磁盘,再进行一次性加载。

    (1)增量同步

    ​ 增量同步的执行流程如下:

    • 主节点会将修改性影响的指令记录到本地内存的缓冲区中,然后异步将缓冲区的指令同步到从节点
    • 从节点一边执行主节点传入的指令流,一边向主节点返回指向的偏移量(指向到哪)

    ​ 缓冲区的内存有限,当缓冲区的指令加载到内存不足时,新加载的指令就会覆盖旧的内存区域,如果网络较差就可能发生从节点未加载的指令被覆盖,此时需要进行快照同步。

    (2)快照同步

    ​ 快照同步的执行流程如下:

    • 主节点进行一次bgsave,即进行一次RDB持久化,将内存的数据通过快照文件存入磁盘中
    • 然后将快照文件内容全部传到从节点,从节点清空自己的数据,进行全量加载
    • 从节点加载完毕后,通知主节点,随后继续进行增量同步

    如果增量同步过程中,还是存在延迟,有内容被覆盖,就可能陷入同步死循环,所以需要对缓冲区的大小设定合适的值。

    2、哨兵模式[揭竿而起]

    (1)原理

    ​ Master挂掉了怎么办?Slave必揭竿而起,大呼“王侯将相宁有种乎?”,然后成为新一任Master。但是一般Slave节点都有很多个,而且节点之间相隔较远,无法知道主节点挂掉了,Slave也没办法称王。如果及时发现,我们可以手动设置。

    ​ 如果是大半夜呢?Redis建立了一个哨兵机制(Redis Sentinel),由哨兵负责监控主节点的健康,当主节点挂掉后,会自动在从节点中选择一个最优的切换成为主节点。

    ​ 客户端来连接Redis集群时,会首先连接Sentinel,通过Sentinel查询主节点的地址,然后再连接主节点进行交互。主节点发生故障时,客户端会重新向Sentinel要地址,Sentinel会将最新的主节点地址告诉客户端。

    ​ 若主节点起死回生了,他将成为新主节点的从节点,只能感叹“沉舟侧畔千帆过,病树前头万木春”。

    (2)手动设置

    ​ 可以通过以下指令进行主服务器的手动设置,以下指令需要手动设置,没有使用到Sentinel。

    slave no one
    
    • 1

    (3)Sentinel自动

    ​ 新建一个sentinel.conf文件,添加以下内容。

    sentinel monitor master 127.0.0.1 6379 1
    #master 是监控对象服务器的名称
    #127.0.0.1 6379 是master服务器的地址
    #1 是至少由多少个哨兵同意迁移的数量
    
    • 1
    • 2
    • 3
    • 4

    ​ 然后执行以下指令通过sentinel.conf配置文件进行启动。

    redis-sentinel sentinel.conf
    #默认端口26379
    
    • 1
    • 2

    ​ 当主节点挂掉了,sentinel就会选择从节点作为主节点,如果原主节点启动,将会自动降级为从节点。我们可以在从节点的配置文件中设置其优先级,优先级较高的优先成为主节点。

    slave-priority n
    #n为整数,n越大则优先级越高
    
    • 1
    • 2

    八、分布式集群🐩

    ​ 我们发现主从集群下,好几台服务器的数据都一样,主服务器的访问负担获取减少了,数据安全性也增加了,但是数据的内存负担和数据修改的运算负担依旧没有改变。久而久之,当数据增长到一定程度时,单个Redis的内存会不够用,当访问量也逐渐增大时,单个Redis也无法承担。所以可以使用分布式集群来分担每一个Redis的工作。

    ​ 分布式集群提供高度高度可用性,即时集群中有部分节点挂掉或无法通信,集群的其它也可以继续进行请求处理。

    ​ Redis Cluster机制是Redis作者提供的集群方案,是去中心化的。

    1、Cluster集群的搭建

    (1)修改文件配置

    ​ 需要修改对应Redis服务器的conf配置文件,具体修改以下内容

    cluster-enable yes #打开集群模式
    cluster-config-file nodes-6379.conf #设置节点配置文件名
    cluster-node-timeout 15000 #设定节点失联时间,超过该时间,集群自动进行主从切换
    
    • 1
    • 2
    • 3

    (2)集群合成

    ​ 启动需要加入集群中的Redis服务器。到redis安装目录下的src目录中。

    cd /opt/redis-6.2.7/src
    
    • 1

    ​ 执行以下命令,就完成了集群的搭建

    redis-cli --cluster create --cluster-replicas 1 iP地址:端口号 ip地址:端口号 iP地址:端口号 ip地址:端口号 iP地址:端口号 ip地址:端口号
    #--cluster-replicas 1 :集群搭建方式为1,代表一主一从,此处将六台主机分为三组
    
    • 1
    • 2

    (3)集群连接

    redis-cli -c -p 6379 #可选任一Redis进行连接
    
    • 1

    ​ 连接以后,可以通过以下命令查看集群信息

    cluster nodes
    
    • 1

    2、Cluster集群的原理

    (1)插槽分配

    ​ Redis Cluster将所有数据划分为16384个槽位,每个节点负责其中一部分槽位,槽位的相关信息存储在各自节点中。如下图,假设我们一个集群中有三个主节点(一个集群至少有三个节点),那么节点的槽分配会如图中所示。

    在这里插入图片描述

    (2)插槽定位

    ​ Redis Cluster默认会对请求操作中的key进行crc16算法计算,得出相应的hash整数值再进行对16384取模得到对应的槽位,一个slots槽可以存放多个key值。

    (3)服务跳转

    ​ 当客户端向一个错误的节点发出指令后,该节点发现对应的key不归自己管理,则会向客户端发送MOVED指令,告知客户端应当连接的节点地址。

    get key
    -MOVED 3999 ip地址:端口号
    
    • 1
    • 2

    (4)容错

    ​ Redis Cluster可以为每个主节点设置若干从节点,当主节点发生故障时,集群会自动将其中某个从节点提升为主节点。若某个主节点没有从节点,当它发生故障时,集群将完全处于不可用状态。

    ​ Redis也提供了cluster-require-full-coverage,可以允许部分节点发送故障时,其它节点还可以继续对外提供访问。

    ​ Redis提供了cluster-node-timeout,表示当某个节点持续timeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换。

    ——————————————————————

    作者:未来村村长

    参考:

    [1] 《Redis深度历险》—钱文品.电子工业出版社

    [2] 尚硅谷Redis教学视频

    [3] 黑马程序员Redis教学视频

    个人网站:www.76pl.com

    👨‍🌾点个关注,在未来村不会迷路👩‍🌾

    ——————————————————————

  • 相关阅读:
    WPF数据绑定Binding对数据的转换与效验(四)
    Java高级面试问题
    5、架构-负载均衡
    操作系统【OS】线程的分类
    腾讯出来的3年测试经验小伙来面试,他这情况要求18K我该给吗?
    什么是HR管理系统?为您的企业提供高效的人力资源管理解决方案
    LQ0009 平方十位数【枚举】
    DIM层维度表学习之用户维度表分析
    VRP基础及操作
    【微信小程序系列:四】前端利用wx.setStorageSync缓存设置有效时间
  • 原文地址:https://blog.csdn.net/apple_51976307/article/details/124991211