Redis

NoSQL与RDBMS

RDBMS的特点:关系型数据库管理系统

  • 工具:MySQL、Oracle、SQL Server……

  • 应用:业务性数据存储系统:事务和稳定性

  • 特点:体现数据之间的关系,支持事务,保证业务完整性和稳定性,小数据量的性能也比较好

  • 开发:SQL

  • 业务架构中的问题

    • 问题:以网站后台存储为例,当并发量很大,所有高并发全部直接请求MySQL,容易导致MySQL奔溃
    • 需求:能实现高并发的数据库,接受高并发请求

NoSQL的特点:Not Only SQL:非关系型数据库

  • 工具:Redis、HBASE、MongoDB……

  • 应用:一般用于高并发高性能场景下的数据缓存或者数据库存储

  • 特点:读写速度特别快,并发量非常高,相对而言不如RDBMS稳定,对事务性的支持不太友好

  • 开发:每种NoSQL都有自己的命令语法

  • 解决上面RDBMS的问题:实现读写分离

    • 读请求:读请求不读取MySQL,读取Redis
    • 写请求:写请求直接写入MySQL

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

RDBMS和NoSQL的应用特点分别是什么?

  • RDBMS:业务性数据存储
    • 支持事务、更加稳定和安全、小数据量和低并发的场景下性能比较高
  • NoSQL:高并发和高性能的数据存储
    • 读写速度比较快,支持高并发读写,事务支持不完美,稳定性和安全性相对较差

Redis的功能与应用场景

官方介绍:

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

定义:基于内存的分布式的NoSQL数据库

  • 所有数据存储在内存中,并且有持久化机制
  • 每次redis重启,会从文件中重新加载数据到内存,所有读写都只基于内存

功能:提供高性能高并发的数据存储

特点

  • 基于C语言开发的系统,与硬件的交互性能更好
  • 基于内存实现数据读写,读写性能更快
  • 分布式的、扩展性和稳定性更好
  • 支持事务、拥有各种丰富的数据结构
    • String:类似于Java中的字符串
    • Hash:类似于Java中Map集合
    • List:有序可重复的元素集合
    • Set:无序且不可重复的元素集合,一般用于去重
    • Zset:Sorted Set,有序且不可重复

应用场景

  • 缓存:用于实现大数据量高并发的大数据量缓存【临时性存储】
  • 数据库:用于实现高性能的小数据量读写【永久性存储】
  • 消息中间件:消息队列【MQ】:用于实现消息传递,一般不用

Redis在Linux操作

Redis第一次安装完需要修改配置

## 61行,配置redis服务器接受链接的网卡
bind node1
## 128行,redis是否后台运行,设置为yes
daemonize yes
## 163行,设置redis服务日志存储路径
logfile "/export/server/redis-3.2.8-bin/logs/redis.log"
## 247行,设置redis持久化数据存储目录
dir /export/server/redis-3.2.8-bin/datas/

Redis的数据结构及数据类型

数据结构

  • 整个Reids中所有数据以KV结构形式存在
  • K:作为唯一标识符,唯一标识一条数据,固定为String类型
  • V:真正存储的数据,可以有多种类型
    • String、Hash、List、Set、Zset、BitMap、HypeLogLog
  • 理解Redis:类似于Java中的一个Map集合,可以存储多个KV,根据K获取V

数据类型

Key:StringValue类型Value值应用场景
pv_20200101String10000一般用于存储单个数据指标的结果
person001Hashname:laoer age : 20 sex female用于存储整个对象所有属性值
uvList{100,200,300,100,600}有序允许重复的集合,每天获取最后一个值
uv_20200101Set{userid1,userid2,userid3,userid4……}无序且不重复的集合,直接通过长度得到UV
top10_productZSet【score,element】{10000-牙膏,9999-玩具,9998-电视……}有序不可重复的集合,统计TopN
user_filterBitMap{0101010101010000000011010}将一个字符串构建位,通过0和1来标记每一位
product_20200101HypeLogLog{productid1,id2……}类似于Set集合,底层实现原理不一样,数据量大的情况下,性能会更好,结果可能存在一定的误差
  • String类型

    • KV:【String,String】,类似于Java中Map集合的一条KV
  • Hash类型

    • KV:【String,Map集合】:Map集合的嵌套,Map集合中的元素是无序的
  • List类型

    • KV:【String,List】:有序且可重复
  • Set类型

    • KV:【String,Set】:无序且不重复
  • Zset类型

    • KV:【String,TreeMap集合】:Value也类似于Map集合,有序的Map集合
    • 类似于List和Set集合特点的合并:有序且不可重复

在redis中1代表成功,0代表失败

Redis的通用命令

Redis默认有16个数据库,以db0-db15命名,个数可以通过配置文件修改,名称不能改。Redis是一层数据存储结构,所有的KV直接存储在数据库中,默认进入db0

  • keys 通配符 :keys 列举当前数据库中的所有key
  • del key:删除某个key
  • exists key :判断某个k是否存在
  • type key:判断这个k对应的v的类型
  • expire K 过期时间:设置某个K的过期时间,一旦达到过期时间,这个K会被自动删除 (过期时间以秒为单位)
  • ttl k:查看某个K剩余的存活时间
  • select N :切换数据库
  • move key N:将某个Key移动到某个数据库中
  • flushdb:清空当前数据库的所有key
  • flushall:清空所有数据库的所有key

String类型的常用命令

  • set K V :给String类型的Value进行赋值或更新
  • get K : 读取String类型的Value值
  • mset K1 V1 K2 V2… : 用于批量写多个String类型的KV
  • mget K1 K2 K3 : 用于批量读取String类型的Value
  • setnx K V :只能用于新增数据,当K不存在时可以进行新增
    • 应用:构建抢占锁,搭配expire来使用
  • incr K :指定数值类型的字符串进行递增1,一般用来做计数器
  • incrby K N :指定数值类型的字符串增长固定的步长
  • decr K :对数值类型的数据进行递减1
  • decrby K N :按照指定步长进行递减
  • incrbyfloat K N :基于浮点数递增
  • strlen K :统计字符串的长度
  • getrange K start end :用于截取字符串

Hash类型的常用命令 类似Map

  • hset K k v :用于为某个k添加一个属性
  • hget K k : 用于获取某个k的某个属性值
  • hmset K k1 v1 k2 v2… :批量的为某个K赋予新的属性
  • hmget K k1 k2… : 批量的 获取某个K的多个属性的值
  • hgetall K :获取所有属性的值
  • hdel K k1 k2… :删除某个属性
  • hlen K :统计K对应的Value总的属性的个数
  • hexists K k :判断这个K的V中是否包含这个属性
  • hvals K :获取所有属性的value值

List类型的常用命令 类似List

  • lpush K e1 e2 … :将每个元素放到集合的左边,左序放入
  • rpush K e1 e2… :将每个元素放到集合的右边 ,右序放入
  • lrange K start end :通过下表的范围来获取元素的数据 【从左往右的下标是从0开始,从右往左的下表是从-1开始,一定是从小到大的下标 (lrange K 0 -1 :所有元素)】
  • llen K :统计集合的长度
  • lpop K :删除左边的一个元素
  • rpop K :删除右边的一个元素

Set类型的常用命令 类似Set

  • sadd K e1 e2 e3…. :用于添加元素到Set集合
  • smembers K :用于查看Set集合的所有成员
  • sismenber K e1 :判断是否包含这个成员
  • srem K e :删除其中某个元素
  • scard K :统计集合长度
  • sunion K1 K2 :取两个集合的并集
  • sinter K1 K2 :取两个集合的交集

Zset类型的常用命令 类似TreeMap

  • zadd K score1 k1 score2 k2 … :用于添加元素到Zset集合中
  • zrange K start end [withscores] :范围查询
  • zreverange K stare end [withscores] :倒序查询
  • zrem K k1:移除一个元素
  • zcard K :统计集合长度
  • zscore K k :获取评分

BitMap类型的常用命令

  • setbit bit1 位置 0/1 修改某一位的值
  • getbit K 位置 : 查看某一为的值
  • bitcount K [start end ]:用于添加位图中所有1的个数
  • bittop and/or/xor/not bitrs bit1 bit2 :用于位图的运算

HyperLogLog类型的常用命令

功能类似于Set集合,用于实现数据的去重

  • 区别:底层实现原理不一样
  • 应用:适合于数据量比较庞大的情况下的使用,存在一定的误差率
  • 命令:
    • pfadd K e1 e2 … :用于添加元素
    • pfcount K :用于统计个数、
    • pfmerge pfrs pf1 pf2 … :用于实现集合的合并

Redis怎么用java连接 :jedis

依赖

    <properties>
        <jedis.version>3.2.0</jedis.version>
    </properties>

    <dependencies>
        <!-- Jedis 依赖 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>
        <!-- JUnit 4 依赖 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

jedis :构建连接

    //todo:1-构建连接对象
    Jedis jedis = null;

    @Before
    public void getConnection(){
        //方式一:直接构建Jedis对象
//        jedis = new Jedis("node1",6379);
        //方式二:通过连接池构建Jedis
        //构建连接池配置对象
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(10);//总连接构建
        config.setMaxIdle(5);//最大空闲连接
        config.setMinIdle(2);//最小空闲连接
        //构建连接池对象
        JedisPool jedisPool = new JedisPool(config,"node1",6379);
        //获取连接
        jedis = jedisPool.getResource();
    }

    //todo:2-调用连接对象的方法实现操作


    //todo:3-释放连接
    @After
    public void closeConnection(){
        jedis.close();
    }

jedis的方法小结

  • String
    • set / get / incr / exists / expire / setexp / ttl
  • Hash
    • hset / hmset / hget / hgetall / hdel / hlen / hexists
  • List
    • lpush / rpush / lrange / llen / lpop / rpop
  • Set
    • sadd / smembers / sismember / scard / srem
  • Zset
    • zadd / zrange / zrevrange / zcard / zrem

Redis持久化

数据存储设置

  • 问题:
    • 数据存储如何保证数据安全?
  • 磁盘存储:数据直接存储在硬盘上
    • 特点:容量大、安全性高、读写速度上相对不如内存
    • 数据丢失:硬盘损坏或者软件层面的数据丢失
  • 解决:
    • 软件层面:冗余备份
      • HDFS数据存储
    • 硬件层面:磁盘冗余阵列
      • HDFS的fsimage文件怎么保证安全?存储在Linux的硬盘上的
        • 软件层面:dfs.namenode.name.dir这个属性中配置多个目录
          • /export/server/hadoop-2.7.5/hadoopDatas/namenodeDatas1 =》 硬盘1
          • /export/server/hadoop-2.7.5/hadoopDatas/namenodeDatas2 =》 硬盘2
          • /export/server/hadoop-2.7.5/hadoopDatas/namenodeDatas3 =》 硬盘3
          • 每个目录存一份
        • 硬件层面:只配置一个目录,这个目录的硬盘做RAID1
          • RAID0:两块硬盘各自1TB,配置RAID0,在操作系统中只能看到一块硬盘,2T
          • RAID1:两块硬盘各自1TB,配置RAID0,在操作系统中只能看到一块硬盘,1T
      • 操作系统的硬盘坏了怎么办?
        • 必须做RAID1
  • 内存存储:数据直接放在内存中
    • 特点:容量小、安全性低、读写性能高
    • 场景:进程故障,内存淘汰、断电
    • 解决
      • 内存备份机制:将数据在别的机器的内存也存一份
      • 持久化日志:将内存的变化追加记录在日志中,可以通过持久化日志恢复内存中的数据

RDB设计

Redis默认的持久化方案

思想

  • 按照一定的时间内,如果Redis内存中的数据产生了一定次数的更新,就将整个Redis内存中的所有数据拍摄一个全量快照文件存储在硬盘上

  • 新的快照会覆盖老的快照文件,快照是全量快照,包含了内存中所有的内容,基本与内存一致

  • 如果Redis故障重启,从硬盘的快照文件进行恢复

  • 举例

    • 配置:save 30 2
    • 解释:如果30s内,redis内存中的数据发生了2条更新【插入、删除、修改】,就将整个Redis内存数据保存到磁盘文件中,作为快照
  • 过程

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

触发

  • 手动触发:当执行某些命令时,会自动拍摄快照【一般不用】

    • save:手动触发拍摄RDB快照的,将内存的所有数据拍摄最新的快照
      • 前端运行
      • 阻塞所有的客户端请求,等待快照拍摄完成后,再继续处理客户端请求
      • 特点:快照与内存是一致的,数据不会丢失,用户的请求会被阻塞
    • bgsave:手动触发拍摄RDB快照的,将内存的所有数据拍摄最新的快照
      • 后台运行
      • 主进程会fork一个子进程负责拍摄快照,客户端可以正常请求,不会被阻塞
      • 特点:用户请求继续执行,用户的新增的更新数据不在快照中
    • shutdown:执行关闭服务端命令
    • flushall:清空,没有意义
  • 自动触发:按照一定的时间内发生的更新的次数,拍摄快照

    • 配置文件中有对应的配置,决定什么时候做快照

      #Redis可以设置多组rdb条件,默认设置了三组,这三组共同交叉作用,满足任何一个都会拍摄快照
      save 900 1
      save 300 10
      save 60 10000
      
      • 为什么默认设置3组?
      • 原因:如果只有一组策略,面向不同的写的场景,会导致数据丢失
      • 针对不同读写速度,设置不同策略,进行交叉保存快照,满足各种情况下数据的保存策略

优缺点

  • 优点
    • rdb方式实现的是**全量快照**,快照文件中的数据与内存中的数据是一致的
    • 快照是==二进制文件==,生成快照加载快照都比较快,体积更小
    • Fork进程实现,性能更好
    • 总结:更快、更小、性能更好
  • 缺点
    • 存在一定概率导致部分数据丢失

应用:希望有一个高性能的读写,不影响业务,允许一部分的数据存在一定概率的丢失,大规模的数据备份和恢复缓存使用

小结

  • 什么是RDB机制,优缺点分别是什么?

    • 思想:在一定时间内发生一定次数的更新,就将内存中所有数据拍摄一个全量快照存储在磁盘上
    • 实现
      • 手动:save、bgsave、shutdown、flushall
      • 自动:save 时间 次数
    • 优缺点
      • 优点:全量快照、二进制文件、不影响性能,更快、更小、性能更好
      • 缺点:存在一定概率的数据丢失
    • 应用:高并发或者高性能的数据缓存,可以实现大量数据的备份和恢复

RDB测试

查看当前快照

ll /export/server/redis/datas/
  • 配置修改

    cd /export/server/redis
    vim redis.conf
    #202行
    save 900 1
    save 300 10
    save 60 10000
    save 20 3
    
  • 重启redis服务,配置才会生效

    ps -ef | grep redis
    kill -9 pid
    redis-start.sh
    
  • 插入数据

    set s1 "laoda"
    set s2 "laoliu"
    set s3 "laoliu"
    
  • 查看dump的rdb快照

    ll /export/server/redis/datas/
    

AOF设计

需要解决的问题:RDB存在一定概率的数据丢失,如何解决?

思想

  • 按照一定的规则,将内存数据的操作日志追加写入一个文件中
  • 当Redis发生故障,重启,从文件中进行读取所有的操作日志,恢复内存中的数据
  • 重新对Redis进行执行,用于恢复内存中的数据

过程

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

实现:追加的规则

  • appendfsync always
    • 每更新一条数据就同步将这个更新操作追加到文件中
    • 优点:数据会相对安全,几乎不会出现数据丢失的情况
    • 缺点:频繁的进行数据的追加,增大磁盘的IO,导致性能较差
  • appendfsync everysec
    • 每秒将一秒内Redis内存中数据的操作异步追加写入文件
    • 优点:在安全性和性能之间做了权衡,性能要比always高
    • 缺点:有数据丢失风险 ,但最多丢失1秒
  • appendfsync no
    • 交给操作系统来做,不由Redis控制
  • 肯定不用的

优缺点

  • 优点:安全性和性能做了折中方案,提供了灵活的机制,如果性能要求不高,安全性可以达到最高

  • 缺点

    • 这个文件是**普通文本文件**,相比于二进制文件来说,每次追加和加载比较慢

    • 数据的变化以追加的方式写入AOF文件

      • 问题:文件会不断变大,文件中会包含不必要的操作【过期的数据】
      • 解决:模拟类似于RDB做全量的方式,定期生成一次全量的AOF文件

应用:数据持久化安全方案

  • 理论上绝对性保证数据的安全

持久化方案:两种方案怎么选?

  • 两种方案都用:默认不配置AOF,使用的RDB
  • 问题:两种都用,重启Redis加载的是谁的数据?
    • 加载AOF

小结

  • 什么是AOF机制?

    • 思想:按照一定规则,将内存中的数据操作追加记录在操作日志中

    • 实现

      • always:内存中每更新一条,文件中就追加一条,安全
      • everysec:每秒追加一次,最多丢失1s的数据
      • no:不用,交给操作系统来进行同步
    • 优缺点

      • 优点:安全性和性能的选择更加灵活
      • 缺点:恢复比较慢,aof文件也会越来越大,会包含很多无用的操作
    • 应用:Redis安全性的数据库持久化数据存储

    • 方案:RDB和AOF放在一起,AOF优先级更高

AOF的实现

  • 开启并配置

    vim redis.conf
    #594行:开启aof
    appendonly yes
    #624行:默认每s刷写一次
    appendfsync everysec
    #665,666
    #增幅100%就重新覆盖一次
    auto-aof-rewrite-percentage 100
    #文件至少要大于64MB,一般建议更改为GB大小
    auto-aof-rewrite-min-size 64mb
    
  • 重启Redis

    ps -ef | grep redis
    kill -9 pid
    redis-start.sh
    
  • 查看数据

    keys *
    
    • 从AOF文件恢复数据
  • 查看aof文件

    ll /export/server/redis/datas
    

Redis的事务机制(一般不用)

  • 事务定义:事务是数据库操作的最小工作单元,包含原子性、一致性、隔离性、持久性

  • Redis事务Redis一般不用事务

    • Redis本身是单线程的,所以本身没有事务等概念
    • Redis 支持事务的本质是一组命令的集合,事务支持一次执行多个命令,串行执行每个命令
    • 一旦Redis开启了事务,将所有命令放入一个队列中,提交事务时,对整个队列中的命令进行执行
    • redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
    • 没有隔离性:批量的事务命令执行前在缓存队列中,没有事务交叉,不存在脏读幻读等问题
    • 不能保证原子性:单条命令是原子性执行的,但事务不保证原子性,且没有回滚机制,事务中任意命令执行失败,其余的命令仍会被执行。
  • 过程

    • 开启事务
    • 提交命令
    • 执行事务
  • 命令

    • multi:开启事务
    • exec:执行事务
    • discard:取消事务
    • watch:监听机制,类似于乐观锁
    • unwatch:取消监听

Redis实现消息队列

消息队列是什么?

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

  • 问题

    • A的并发高,B的并发量低,导致B无法接受所有数据,会有数据丢失
    • A的数据只给了B,如果C也要A的数据,必须重修修改代码,让A给C再发一份
  • 解决:引入消息队列

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

    Redis如何实现消息队列?

    • 命令

      • PUBLISH:广播生产消息
      • subscribe:订阅消息
      • unsubscribe:取消订阅
    • 测试

      • 启动多个客户端,先订阅后发布

      • 客户端1

        PUBLISH "cctv.5" "this is China"
        
      • 客户端2

        SUBSCRIBE "cctv.5"
        
      • 客户端3

        SUBSCRIBE "cctv.5"
        

Redis过期策略与内存淘汰机制

解决的问题: Redis使用的是内存,内存如果满了,怎么解决?

过期策略

  • 设计思想:避免内存满,指定Key的存活时间,到达存活时间以后自动删除

    • 命令:expire/setex
  • 定时过期:指定Key的存活时间,一直监听这个存活时间,一旦达到存活时间,自动删除

    • 需要CPU一直做监听,如果Key比较多,CPU的消耗比较严重
  • 惰性过期:指定Key的存活时间,当使用这个Key的时候,判断是否过期,如果过期就删除

    • 如果某个Key设置了过期时间,但是一直没有使用,不会被发现过期了,就会导致资源浪费
  • 定期过期:每隔一段时间就检查数据是否过期,如果过期就进行删除

    • 中和的策略机制
  • Redis中使用了惰性过期和定期过期两种策略共同作用

淘汰机制

  • 设计思想:内存满了,怎么淘汰

  • Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据, Redis 源码中的默认配置

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#最大内存策略:当到达最大使用内存时,你可以在下面5种行为中选择,Redis如何选择淘汰数据库键

#当内存不足以容纳新写入数据时

# volatile-lru -> remove the key with an expire set using an LRU algorithm
# volatile-lru :在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把 redis 既当缓存,又做持久化存储的时候才用。

# allkeys-lru -> remove any key according to the LRU algorithm
# allkeys-lru : 移除最近最少使用的key (推荐)

# volatile-random -> remove a random key with an expire set
# volatile-random : 在设置了过期时间的键空间中,随机移除一个键,不推荐

# allkeys-random -> remove a random key, any key
# allkeys-random : 直接在键空间中随机移除一个键,弄啥叻

# volatile-ttl -> remove the key with the nearest expire time (minor TTL)
# volatile-ttl : 在设置了过期时间的键空间中,有更早过期时间的key优先移除 不推荐

# noeviction -> don't expire at all, just return an error on write operations
# noeviction : 不做过键处理,只返回一个写操作错误。 不推荐

# Note: with any of the above policies, Redis will return an error on write
#       operations, when there are no suitable keys for eviction.
# 上面所有的策略下,在没有合适的淘汰删除的键时,执行写操作时,Redis 会返回一个错误。下面是写入命令:
#       At the date of writing these commands are: set setnx setex append
#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
#       getset mset msetnx exec sort

# 过期策略默认是:
# The default is:
# maxmemory-policy noeviction

实际项目中设置内存淘汰策略:

maxmemory-policy allkeys-lru,移除最近最少使用的key。

LRU算法:最近最少使用算法

  • 缓存使用:allkeys-lru
  • 数据库使用:volatile-lru

Redis架构:主从复制集群设计

架构问题

  • Redis的服务只有单台机器
  • 问题1:单点故障问题,如果Redis服务故障,整个Redis服务将不可用
  • 问题2:单台机器的内存比较小,数据存储的容量不足,会导致redis无法满足需求

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

设计

  • 分布式主从架构
  • Master:主节点
    • 负责对外提供数据的读写
  • Slave:从节点
    • 负责对外提供读的请求
    • 负责与主节点同步数据

特点:主从节点上的数据都是一致的,连接任何一个节点实现读,写操作只能连接主节点

优缺点

  • 优点:实现了读写分离,分摊了读写的压力负载,如果一台Redis的Slave故障,其他的Redis服务节点照常对外提供服务
  • 缺点:如果Master故障,整个集群不能对外提供写的操作,Master没有HA机制

主从复制同步与配置

  • 同步策略

    • 全量同步
      • 新的Slave节点添加到集群中或者Slave与Master断开很长时间,超出了数据积压缓冲大小
      • Master会通过bgsave构建一份完整的内存快照,发送给Slave,Slave加载快照的数据
      • Master也会维护一个缓存队列,将快照中没有的数据,放在缓存队列中,Slave会同步缓存中的数据
      • 全量同步比较消耗性能
    • 增量同步
      • Master会将当前Slave没有同步的数据放在积压缓存区中
      • Slave请求同步数据时,Master如果判断同步的数据在积压缓冲区中进行进行返回同步
  • 常见配置

    ################################# REPLICATION #################################
    # 复制选项,slave复制对应的master。
    slaveof <masterip> <masterport>
    
    # 如果master设置了requirepass,那么slave要连上master,需要有master的密码才行。
    # masterauth就是用来配置master的密码,这样可以在连上master后进行认证。
    # masterauth <master-password>
    
    # 当从库同主机失去连接或者复制正在进行,从机库有两种运行方式:
    # 1) 如果slave-serve-stale-data设置为yes(默认设置),从库会继续响应客户端的请求。
    # 2) 如果slave-serve-stale-data设置为no,除去INFO和SLAVOF命令之外的任何请求都会返回一个错误”SYNC with master in progress”。
    slave-serve-stale-data yes
    
    # 作为从服务器,默认情况下是只读的(yes)
    slave-read-only yes
    
    # 是否使用socket方式复制数据。
    # 目前redis复制提供两种方式,disk和socket。
    # 如果新的slave连上来或者重连的slave无法部分同步,就会执行全量同步,master会生成rdb文件。
    # 有2种方式:
    # 1.disk方式是master创建一个新的进程把rdb文件保存到磁盘,再把磁盘上的rdb文件传递给slave。
    # 2.socket是master创建一个新的进程,直接把rdb文件以socket的方式发给slave。
    # disk方式的时候,当一个rdb保存的过程中,多个slave都能共享这个rdb文件。
    # socket的方式就的一个个slave顺序复制。
    # 在磁盘速度缓慢,网速快的情况下推荐用socket方式。
    repl-diskless-sync no
    
    # diskless复制的延迟时间,防止设置为0。
    # 一旦复制开始,节点不会再接收新slave的复制请求直到下一个rdb传输。
    # 所以最好等待一段时间,等更多的slave连上来。
    repl-diskless-sync-delay 5
    
    # slave根据指定的时间间隔向服务器发送ping请求。
    # 时间间隔可以通过 repl_ping_slave_period 来设置,默认10秒。
    # repl-ping-slave-period 10
    
    # 复制连接超时时间。
    # master和slave都有超时时间的设置。
    # master检测到slave上次发送的时间超过repl-timeout,即认为slave离线,清除该slave信息。
    # slave检测到上次和master交互的时间超过repl-timeout,则认为master离线。
    # 需要注意的是repl-timeout需要设置一个比repl-ping-slave-period更大的值,不然会经常检测到超时。
    # repl-timeout 60
    
    # 是否禁止复制tcp链接的tcp nodelay参数,可传递yes或者no。
    # 默认是no,即使用tcp nodelay。
    # 如果master设置了yes来禁止tcp nodelay设置,在把数据复制给slave的时候,会减少包的数量和更小的网络带宽。
    # 但是这也可能带来数据的延迟。
    # 默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择yes。
    repl-disable-tcp-nodelay no
    
    # 复制缓冲区大小,这是一个环形复制缓冲区,用来保存最新复制的命令。
    # 这样在slave离线的时候,不需要完全复制master的数据,如果可以执行部分同步,只需要把缓冲区的部分数据复制给slave,就能恢复正常复制状态。
    # 缓冲区的大小越大,slave离线的时间可以更长,复制缓冲区只有在有slave连接的时候才分配内存。
    # 没有slave的一段时间,内存会被释放出来,默认1m。
    # repl-backlog-size 5mb
    
    # master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。
    # 单位为秒。
    # repl-backlog-ttl 3600
    
    # 当master不可用,Sentinel会根据slave的优先级选举一个master。
    # 最低的优先级的slave,当选master。
    # 而配置成0,永远不会被选举。
    # 注意:要实现Sentinel自动选举,至少需要2台slave。
    slave-priority 100
    
    # redis提供了可以让master停止写入的方式,如果配置了min-slaves-to-write,健康的slave的个数小于N,mater就禁止写入。
    # master最少得有多少个健康的slave存活才能执行写命令。
    # 这个配置虽然不能保证N个slave都一定能接收到master的写操作,但是能避免没有足够健康的slave的时候,master不能写入来避免数据丢失。
    # 设置为0是关闭该功能,默认也是0。
    # min-slaves-to-write 3
    
    # 延迟小于min-slaves-max-lag秒的slave才认为是健康的slave。
    # min-slaves-max-lag 10
    

Redis架构:哨兵集群的设计

解决问题:主从复制集群的Master存在单点故障问题,怎么解决?

类似于ZK的设计

  • 每台节点存储的数据都是一样的
  • 如果Leader故障,允许Follower选举成为Leader

哨兵设计

  • 思想:基于主从复制模式之上封装了哨兵模式,如果Master出现故障,让Slave选举成为新的Master

  • 实现:哨兵进程实现

    • 必须能发现Master的故障
    • 必须负责重新选举新的Master
  • 架构

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

  • 哨兵进程

    • 每个哨兵负责监听所有Redis节点和其他哨兵
    • 为什么要监听所有Redis的节点:发现所有节点是否会出现故障
      • 如果Master出现故障,会进行投票选择一个Slave来成为新的Master
    • 为什么要监听别的哨兵:如果哨兵故障,不能让这个哨兵参与投票选举等
  • 哨兵功能

    • 集群监控:监控节点状态
    • 消息通知:汇报节点状态
    • 故障转移:实现Master重新选举
    • 配置中心:实现配置同步
  • 流程

    • step1:如果Master突然故障,有一个哨兵会发现这个问题,这个哨兵会立即通告给所有哨兵
      • 主观性故障【sdown】
    • step2:当有一定的个数的哨兵都通告Master故障了,整体认为Master故障了
      • 客观性故障【odown】
    • step3:所有哨兵根据每台Slave通信的健康状况以及Slave权重选举一个新的Master
    • step4:将其他所有的Slave的配置文件中的Master切换为当前最新的Master

哨兵集群的实现

  • 配置哨兵服务

    • 第一台机器复制哨兵配置文件:sentinel.conf

      cp /export/server/redis-3.2.8/sentinel.conf /export/server/redis/
      
    • 修改配置文件

      vim sentinel.conf
      
      #18行
      bind 0.0.0.0
      protected-mode no
      daemonize yes
      logfile "/export/server/redis-3.2.8-bin/logs/sentinel.log"
      #73行
      sentinel monitor mymaster node1 6379 2
      
    • 分发给第二台和第三台

      cd /export/server/redis
      scp -r sentinel.conf root@node2:$PWD
      scp -r sentinel.conf root@node3:$PWD
      
  • 启动

    • 启动三台Redis服务

      redis-start.sh
      
    • 启动三台哨兵服务

      redis-sentinel /export/server/redis/sentinel.conf
      
  • 测试

    • 连接Redis

      x 1redis-cli -h node1
      
    • 连接哨兵

      redis-cli -h node3 -p 26379
      info sentinel
      
  • Jedis中哨兵的连接

    //方案三:构建哨兵连接池:第一个参数是master的逻辑名称,第二个参数是哨兵列表,第三个是连接池的配置
    HashSet<String> sets = new HashSet<>();
    sets.add("node1:26379");
    sets.add("node2:26379");
    sets.add("node3:26379");
    JedisSentinelPool mymaster = new JedisSentinelPool("mymaster", sets, jedisPoolConfig);
    //从连接池中获取连接
    jedis = mymaster.getResource();
    

Redis架构:分片集群的设计

解决的问题:Redis哨兵集群中的存储容量只有单台机器,如何解决大量数据使用Redis存储问题?

分片集群设计

  • 分片集群模式

  • 思想:将多个Redis小集群从逻辑上合并为一个大集群,每个小集群分摊一部分槽位,对每一条Redis的数据进行槽位计算,这条数据属于哪个槽位,就存储对应槽位的小集群中

    • 分片的规则:根据Key进行槽位运算:CRC16【K】 & 16383 = 0 ~ 16383

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

架构:

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

分片集群的实现

  • 三台机器都保证有C语言环境

    yum -y install gcc-c++ tcl
    
  • 上传redis5到第一台机器

    cd /export/software/
    rz
    
  • 解压编译安装

    tar -zxf redis-5.0.8.tar.gz -C /export/server/
    cd /export/server/redis-5.0.8
    # 编译
    make
    # 安装至指定目录
    make PREFIX=/export/server/redis-5.0.8-bin install
    
  • 创建软连接

    cd /export/server
    ln -s redis-5.0.8-bin redis
    
  • 拷贝配置文件

    cp /export/server/redis-5.0.8/redis.conf /export/server/redis
    
  • 创建两个服务目录

    # 创建目录:7001和7002
    cd /export/server/redis
    mkdir -p 7001 7002
    
  • 配置7001

    cd /export/server/redis
    cp redis.conf 7001/redis_7001.conf
    
    vim 7001/redis_7001.conf
    
    #69行
    bind 0.0.0.0
    #88行
    protected-mode no
    #92行
    port 7001
    #136行
    daemonize yes
    #158行
    pidfile /var/run/redis_7001.pid
    #171行
    logfile "/export/server/redis-5.0.8-bin/7001/logs/redis.log"
    #263行
    dir /export/server/redis-5.0.8-bin/7001/datas/
    #293行
    masterauth 123456
    #507行
    requirepass 123456
    #699行
    appendonly yes
    #832行
    cluster-enabled yes
    #840行
    cluster-config-file nodes-7001.conf
    #846行
    cluster-node-timeout 15000
    
    mkdir -p /export/server/redis/7001/logs
    mkdir -p /export/server/redis/7001/datas
    
  • 配置7002

    • 拷贝

      cd /export/server/redis
      cp 7001/redis_7001.conf 7002/redis_7002.conf
      
    • 替换

      所有7001换成7002
      
    • 创建目录

      mkdir -p /export/server/redis/7002/logs
      mkdir -p /export/server/redis/7002/datas
      
  • 发送给node2和node3

    cd /export/server
    scp -r redis-5.0.8-bin root@node2:$PWD
    scp -r redis-5.0.8-bin root@node3:$PWD
    
  • 创建软连接

    cd /export/server
    ln -s redis-5.0.8-bin redis
    
  • 启动服务

    • 三台机器启动所有redis进程

      redis-server /export/server/redis/7001/redis_7001.conf
      redis-server /export/server/redis/7002/redis_7002.conf
      
    • 初始化配置集群

      redis-cli -a 123456 --cluster create \
      192.168.88.221:7001 192.168.88.221:7002 \
      192.168.88.222:7001 192.168.88.222:7002 \
      192.168.88.223:7001 192.168.88.223:7002 \
      --cluster-replicas 1
      
  • 启动脚本

    cd /export/server/redis
    vim bin/redis-cluster-start.sh
    
    #!/bin/bash
    
    REDIS_HOME=/export/server/redis
    # Start Server
    ## node1
    ssh node1 "${REDIS_HOME}/bin/redis-server /export/server/redis/7001/redis_7001.conf"
    ssh node1 "${REDIS_HOME}/bin/redis-server /export/server/redis/7002/redis_7002.conf"
    ## node2
    ssh node2 "${REDIS_HOME}/bin/redis-server /export/server/redis/7001/redis_7001.conf"
    ssh node2 "${REDIS_HOME}/bin/redis-server /export/server/redis/7002/redis_7002.conf"
    ## node3
    ssh node3 "${REDIS_HOME}/bin/redis-server /export/server/redis/7001/redis_7001.conf"
    ssh node3 "${REDIS_HOME}/bin/redis-server /export/server/redis/7002/redis_7002.conf"
    
    chmod u+x bin/redis-cluster-start.sh
    
  • 关闭脚本

    vim bin/redis-cluster-stop.sh
    
    #!/bin/bash
    
    REDIS_HOME=/export/server/redis
    # Stop Server
    ## node1
    ${REDIS_HOME}/bin/redis-cli -h node1 -p 7001 -a 123456 SHUTDOWN
    ${REDIS_HOME}/bin/redis-cli -h node1 -p 7002 -a 123456 SHUTDOWN
    ## node2
    ${REDIS_HOME}/bin/redis-cli -h node2 -p 7001 -a 123456 SHUTDOWN
    ${REDIS_HOME}/bin/redis-cli -h node2 -p 7002 -a 123456 SHUTDOWN
    ## node3
    ${REDIS_HOME}/bin/redis-cli -h node3 -p 7001 -a 123456 SHUTDOWN
    ${REDIS_HOME}/bin/redis-cli -h node3 -p 7002 -a 123456 SHUTDOWN
    
    chmod u+x bin/redis-cluster-stop.sh
    
  • Jedis中连接

    JedisCluster jedisCluster = null;//分片集群连接对象    
    @Before
        public void getJedisCluster(){
            //构建连接池的配置对象
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(30);//总连接数
            jedisPoolConfig.setMaxIdle(20);//最大空闲连接
            jedisPoolConfig.setMaxWaitMillis(1500);//等待时间
            //构建集群模式的额连接池
            HashSet<HostAndPort> sets = new HashSet<HostAndPort>();
            sets.add(new HostAndPort("node1",7001));
            sets.add(new HostAndPort("node1",7002));
            sets.add(new HostAndPort("node2",7001));
          sets.add(new HostAndPort("node2",7002));
            sets.add(new HostAndPort("node3",7001));
            sets.add(new HostAndPort("node3",7002));
            jedisCluster = new JedisCluster(sets,2000,2000,5,"123456",jedisPoolConfig);
        }
    

Redis常见面试题

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

常见面试题

  • 什么是缓存穿透,怎么解决?

    • 现象:客户端高并发不断向Redis请求一个不存在的Key,MySQL中也没有

      • 由于Redis没有,导致这个并发全部落在MySQL上
    • 解决

      • step1:对于那些每秒访问频次过高的IP进行限制,拒绝访问

      • step2:如果第一次redis中没有,读MYSQL,MySQL也没有,在Redis中设置一个临时默认值

      • step3:利用BitMap类型构建布隆过滤器

        • MySQL
          • 1 hadoop
          • 2 hive
          • 3 hive
          • 4 spark
          • 5 hue
          • 10 oozie
        111110000100000000000000000000000000000000000000000000000000
        
        • 如果用户请求一个Key,对Key进行计算取余一个数字:10

        • 将MySQL中如果存在的数据都设置为1

        • 如果用户请求1:MySQL中有,可以请求

        • 如果用户请求8:MySQL中没有这个值,不可以请求MySQL

  • 什么是缓存击穿,怎么解决?

    • 现象:有一个Key,经常需要高并发的访问,这个Key有过期时间的,一旦达到过期时间,这个Key被删除,所有高并发落到了MySQL中,被击穿了

    • 解决

      • step1:资源充足的情况下,设置永不过期

      • step2:对这个Key做一个互斥锁,只允许一个请求去读取,其他的所有请求先阻塞掉

        • 第一个请求redis中没有读取到,读了MySQL,再将这个数据放到Redis中

        • 释放所有阻塞的请求

  • 什么是缓存雪崩,怎么解决?

    • 现象:大量的Key在同一个时间段过期,大量的Key的请求在Redis中都没有,都去请求MySQL,导致MySQL奔溃

    • 解决

      • step1:资源充足允许的情况下,设置大部分的Key不过期

      • step2:给所有Key设置过期时间时加上随机值,让Key不再同一时间过期

  • Redis中的Key怎么设计?

    • 使用统一的命名规范

    • 一般使用业务名(或数据库名)为前缀,用冒号分隔,例如,业务名:表名:id。

      • 例如:shop:usr:msg_code(电商:用户:验证码)
    • 控制key名称的长度,不要使用过长的key

    • 在保证语义清晰的情况下,尽量减少Key的长度。有些常用单词可使用缩写,例如,user缩写为u,messages缩写为msg

    • 名称中不要包含特殊字符、包含空格、单双引号以及其他转义字符

  • 为什么Redis是单线程的?

    • 因为Redis是基于内存的操作,CPU不是Redis的瓶颈

    • Redis的瓶颈最有可能是机器内存的大小或者网络带宽

    • 单线程容易实现,而且CPU不会成为瓶颈,所以没必要使用多线程增加复杂度

    • 可以使用多Redis压榨CPU,提高性能

  • 为什么Redis的性能很高?

    • 完全基于内存,非常快速
    • 数据结构简单,对数据操作也简单
    • 采用单线程,避免了不必要的上下文切换和竞争条件
    • 多路I/O复用模型,非阻塞IO

Redis后面追加

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

基于内存实现

内存读写是比磁盘读写快很多的。Redis是基于内存存储实现的数据库,相对于数据存在磁盘的数据库,就省去磁盘磁盘I/O的消耗。MySQL等磁盘数据库,需要建立索引来加快查询效率,而Redis数据存放在内存,直接操作内存,所以就很快。

高效的数据结构

MySQL索引为了提高效率,选择了B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。先看下Redis的数据结构&内部编码图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3yaX6BMN-1625581931737)(Redis.assets/image-20210627151740394.png)]

SDS简单动态字符串

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

字符串长度处理

在C语言中,要获取捡田螺的小男孩这个字符串的长度,需要从头开始遍历,复杂度为O(n); 在Redis中, 已经有一个len字段记录当前字符串的长度啦,直接获取即可,时间复杂度为O(1)。

减少内存重新分配的次数

在C语言中,修改一个字符串,需要重新分配内存,修改越频繁,内存分配就越频繁,而分配内存是会消耗性能的。而在Redis中,SDS提供了两种优化策略:空间预分配和惰性空间释放。

空间预分配

当SDS简单动态字符串修改和空间扩充时,除了分配必需的内存空间,还会额外分配未使用的空间。

分配规则如下:

  • SDS修改后,len的长度小于1M,那么将额外分配与len相同长度的未使用空间。比如len=100,重新分配后,buf 的实际长度会变为100(已使用空间)+100(额外空间)+1(空字符)=201。
  • SDS修改后, len长度大于1M,那么程序将分配1M的未使用空间。

惰性空间释放

当SDS缩短时,不是回收多余的内存空间,而是用free记录下多余的空间。后续再有修改操作,直接使用free中的空间,减少内存分配。

哈希

Redis 作为一个K-V的内存数据库,它使用用一张全局的哈希来保存所有的键值对。这张哈希表,有多个哈希桶组成,哈希桶中的entry元素保存了*key*value指针,其中*key指向了实际的键,*value指向了实际的值。

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

哈希表查找速率很快的,有点类似于Java中的HashMap,它让我们在O(1) 的时间复杂度快速找到键值对。首先通过key计算哈希值,找到对应的哈希桶位置,然后定位到entry,在entry找到对应的数据。

哈希冲突 通过不同的key,计算出一样的哈希值,导致落在同一个哈希桶中。

Redis为了解决哈希冲突,采用了链式哈希。链式哈希是指同一个哈希桶中,多个元素用一个链表来保存,它们之间依次用指针连接。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rAefBlc-1625581931740)(Redis.assets/image-20210627152235039-1624778556684.png)]

为了保持高效,Redis 会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。

跳跃表

跳跃表是Redis特有的数据结构,它其实就是在链表的基础上,增加多级索引,以提高查找效率。跳跃表的简单原理图如下:

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

  • 每一层都有一条有序的链表,最底层的链表包含了所有的元素。
  • 跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。

压缩列表ziplist

压缩列表ziplist是列表键和字典键的的底层实现之一。它是由一系列特殊编码的内存块构成的列表, 一个ziplist可以包含多个entry, 每个entry可以保存一个长度受限的字符数组或者整数,如下:

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

  • zlbytes :记录整个压缩列表占用的内存字节数
  • zltail: 尾节点至起始节点的偏移量
  • zllen : 记录整个压缩列表包含的节点数量
  • entryX: 压缩列表包含的各个节点
  • zlend : 特殊值0xFF(十进制255),用于标记压缩列表末端

由于内存是连续分配的,所以遍历速度很快

合理的数据编码

Redis支持多种数据基本类型,每种基本类型对应不同的数据结构,每种数据结构对应不一样的编码。为了提高性能,Redis设计者总结出,数据结构最适合的编码搭配。

Redis是使用对象(redisObject)来表示数据库中的键值,当我们在 Redis 中创建一个键值对时,至少创建两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。

redisObject中,type 对应的是对象类型,包含String对象、List对象、Hash对象、Set对象、zset对象。encoding 对应的是编码。

  • String:如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。
  • List:如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码
  • Hash:哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码。
  • Set:如果集合中的元素都是整数且元素个数小于512个,使用intset编码,否则使用hashtable编码。
  • Zset:当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码

合理的线程模型

单线程模型:避免了上下文切换

Redis是单线程的,其实是指Redis的网络IO和键值对读写是由一个线程来完成的。但Redis的其他功能,比如持久化、异步删除、集群数据同步等等,实际是由额外的线程执行的。

Redis的单线程模型,避免了CPU不必要的上下文切换竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如hgetall命令),会造成阻塞。Redis是面向快速执行场景的内存数据库,所以要慎用如lrange和smembers、hgetall等命令。

I/O 多路复用

  • I/O :网络 I/O
  • 多路 :多个网络连接
  • 复用:复用同一个线程。
  • IO多路复用其实就是一种同步IO模型,它实现了一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;而没有文件句柄就绪时,就会阻塞应用程序,交出cpu。

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

多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。

虚拟内存机制

Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。

虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。

Redis 雪崩、击穿、穿透、预热、降级

缓存雪崩

Redis雪崩我们一般都称为缓存雪崩,意思就是说在某个时间节点,大量的 key 失效,导致大量的请求从缓存中获取不到数据而去请求数据库

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

上面的黑色的部分表示缓存无效了,也就意味着所有的请求都需要到数据库中去查询数据。那这对于数据库的压力必然是剧增的,如果是在一线互联网这样超高并发的场景下,数据库直接宕机。

重启也没有用,因为重启了还会有巨大的流量涌进来,然后继续被搞宕机。所以对于预防缓存雪崩这种情况的发生意义还是很大的的。

缓存雪崩解决方案之加随机值

缓存雪崩是由于某个时间节点大量的 key 失效而导致的问题,那现在的问题不就是变成了如何防止同一个时间节点大量的 key 失效这种情况发生吗?

最简单的情况就是把key的过期时间分散开,也就是在设置key的过期时间的时候再加一个随机值,就这样就能完美的解决缓存雪崩的问题。

缓存雪崩解决方案之加锁

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

在多个请求同时到达业务系统时候,只能有一个线程能获取到锁,然后才能继续去缓存或者是数据库中查询数据,然后后面的流程和之前的是一样的,执行完成后释放锁,然后其他线程再争抢锁,然后重复前面的流程。

这个方案的优点是可以很好的保护数据库不会被打挂,缺点就是并发度极低。

再优化下的:

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

这个就是在缓存中如果获取不到,再去串行的访问数据看,这里不一定非要串行,可以配合线程池,控制一定的并发数。

这个缺点虽然很多,但是也是一种解决方案。用不用就看实际的业务场景了。毕竟没有没用技术方案,只有不适合业务场景的技术方案

缓存击穿

缓存击穿指的是某个 key 一直在扛着高并发,所谓扛着高并发就是说大量的请求都是获取这个 key 对应的值。

而这个 key 在某个时间突然失效了,那是不是就意味着大量的请求就无法在缓存中获取数据了,而是去请求数据库了,这样很有可能导致数据库被击垮。这就是缓存击穿。

既然这个 key 这个受欢迎,那么就不要设置过期时间了,如果该key的数据更新了,那么就通过互斥锁的方式将其更新

为什么要用互斥锁的方式?如果不使用互斥锁的方式很容易导致数据不一致的情况,这里为了保证缓存和数据库的一致性,就只能牺牲一点点的效率了。

缓存穿透

缓存穿透意思就是某个不存在的key一直被访问,结果发现数据库中也没有这样的数据,最终导致访问该key的所有请求都直接请求到数据库了。如果是并发高的场景下就容易搞垮数据库。

缓存穿透解决方案之缓存空数据

啥叫缓存空数据?就是假设某个key数据并不存在,那么就存一个 NULL 就好了,但是一定不要忘记设置过期时间,因为假设id=3的记录不存在,然后本次访问没有查询到数据,缓存中存的是null如果过一会儿新增了一条记录为3的数据,如果缓存不设置过期时间,那么这条数据就永远获取不到。

缓存穿透解决方案之布隆过滤器

布隆过滤器是一种数据结构,更准确的说是一种概率型的数据结构,因为它能判断某个元素一定不存在或者是可能存在

布隆过滤器是一个bit数组,一个很长的bit数组和一系列的hash函数构成。**(布隆过滤器不存储元素,仅仅是为一个元素是否存在打一个标志)**跟hashcode一样,只能判断你不同,不能判断相同,就是过滤掉哪些肯定不对的数据可以通过布隆过滤器来快速的判断出一个key是否存在数据库中,如果可能存在再去数据库查询,如果布隆过滤器中不存在那么就需要再去数据库查询了

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

缓存预热

缓存预热就是将一些可能经常使用数据在系统启动的时候预先设置到缓存中,这样可以避免在使用到的时候先去数据库中查询。

这就是缓存预热,这个缓存预热在实际场景是经常使用的。

还有一种方式就是添加一个缓存刷新页,这样通过人工干预的方式将一些可能为热点的key添加到缓存中。

缓存降级

当访问量突然剧增(例如下班的点,大家都在地铁上刷手机呢)、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。

系统可以根据一些关键数据进行自动降级,降级的最终目的是保证核心服务可用,即使是有损的。但是有的一些业务的核心服务是不能降级的。这是一种丢卒保帅的思想。
中…(img-NyW4yMrs-1625581931742)]

在多个请求同时到达业务系统时候,只能有一个线程能获取到锁,然后才能继续去缓存或者是数据库中查询数据,然后后面的流程和之前的是一样的,执行完成后释放锁,然后其他线程再争抢锁,然后重复前面的流程。

这个方案的优点是可以很好的保护数据库不会被打挂,缺点就是并发度极低。

再优化下的:

[外链图片转存中…(img-OfzNaK63-1625581931743)]

这个就是在缓存中如果获取不到,再去串行的访问数据看,这里不一定非要串行,可以配合线程池,控制一定的并发数。

这个缺点虽然很多,但是也是一种解决方案。用不用就看实际的业务场景了。毕竟没有没用技术方案,只有不适合业务场景的技术方案

缓存击穿

缓存击穿指的是某个 key 一直在扛着高并发,所谓扛着高并发就是说大量的请求都是获取这个 key 对应的值。

而这个 key 在某个时间突然失效了,那是不是就意味着大量的请求就无法在缓存中获取数据了,而是去请求数据库了,这样很有可能导致数据库被击垮。这就是缓存击穿。

既然这个 key 这个受欢迎,那么就不要设置过期时间了,如果该key的数据更新了,那么就通过互斥锁的方式将其更新

为什么要用互斥锁的方式?如果不使用互斥锁的方式很容易导致数据不一致的情况,这里为了保证缓存和数据库的一致性,就只能牺牲一点点的效率了。

缓存穿透

缓存穿透意思就是某个不存在的key一直被访问,结果发现数据库中也没有这样的数据,最终导致访问该key的所有请求都直接请求到数据库了。如果是并发高的场景下就容易搞垮数据库。

缓存穿透解决方案之缓存空数据

啥叫缓存空数据?就是假设某个key数据并不存在,那么就存一个 NULL 就好了,但是一定不要忘记设置过期时间,因为假设id=3的记录不存在,然后本次访问没有查询到数据,缓存中存的是null如果过一会儿新增了一条记录为3的数据,如果缓存不设置过期时间,那么这条数据就永远获取不到。

缓存穿透解决方案之布隆过滤器

布隆过滤器是一种数据结构,更准确的说是一种概率型的数据结构,因为它能判断某个元素一定不存在或者是可能存在

布隆过滤器是一个bit数组,一个很长的bit数组和一系列的hash函数构成。**(布隆过滤器不存储元素,仅仅是为一个元素是否存在打一个标志)**跟hashcode一样,只能判断你不同,不能判断相同,就是过滤掉哪些肯定不对的数据可以通过布隆过滤器来快速的判断出一个key是否存在数据库中,如果可能存在再去数据库查询,如果布隆过滤器中不存在那么就需要再去数据库查询了

[外链图片转存中…(img-Nt7bzatx-1625581931743)]

缓存预热

缓存预热就是将一些可能经常使用数据在系统启动的时候预先设置到缓存中,这样可以避免在使用到的时候先去数据库中查询。

这就是缓存预热,这个缓存预热在实际场景是经常使用的。

还有一种方式就是添加一个缓存刷新页,这样通过人工干预的方式将一些可能为热点的key添加到缓存中。

缓存降级

当访问量突然剧增(例如下班的点,大家都在地铁上刷手机呢)、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。

系统可以根据一些关键数据进行自动降级,降级的最终目的是保证核心服务可用,即使是有损的。但是有的一些业务的核心服务是不能降级的。这是一种丢卒保帅的思想。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值