Redis总结


Redis是一个开源的key-value存储系统,和Memcached类似,Redis支持存储的value类型相对更多,包括string、list、set、zset和hash,这些数据类型都支持push/pop、add/remove及取交并差集及更丰富的操作,而且这些操作都是原子的
在此基础上,Redis支持各种不同方式的排序,与memcached一样,为了保证效率,数据都是混存在内存中,区别的是,Redis会周期性吧更新的数据写入磁盘或者修改操作写入追加的记录文件

安装配置

官网下载安装包,采用6.2.1版本

在解压安装前,由于redis依赖C语言开发环境,所以先安装相关依赖

yum -y install centos-release-scl scl-utils-build
yum -y install devtoolset-8-toolchain
scl enable devtoolset-8 bash

gcc安装成功
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uvvlRSCv-1624600278701)(D:\file\materials\Note\Redis\img\1.png)]
gcc安装完成后就可以解压安装包了

解压完成后,进入安装目录,执行make命令进行编译

接下来安装软件,(在安装目录下)执行命令make install(可以执行make install PREFIX=<path>指定路径)

到这里Redis就安装成功了,会默认安装在/usr/local/bin目录下

进入该目录下,可以看到有如下文件:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YA3VjcSI-1624600278705)(D:\file\materials\Note\Redis\img\QQ截图20210527223017.jpg)]

软件作用
redis-benchmark性能测试工具,可以在自己本子运行,看看子集本子性能如何
redis-check-aof修复有问题的AOF文件
redis-check-rdb修复有问题的RDB文件
redis-cli客户端操作入口
redis-sentinelRedis集群使用
redis-serverRedis服务器启动命令

启动方式

  • 前台启动(不推荐,命令行关闭后服务关闭):redis-server
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eNBP5B7u-1624600278706)(D:\file\materials\Note\Redis\img\QQ截图20210527223439.jpg)]
  • 后台启动:
  1. 将Redis家目录下的redis.conf配置文件复制一份到etc目录下(也可不复制):cp redis.conf /etc/redis.con
  2. 修改etc目录下的redis.conf配置文件,将daemonize no改为daemonize yes
  3. 后台启动:/usr/local/bin/redis-server /etc/redis.conf
  4. 启动完成后可以执行ps -ef | grep redis命令查看redis进程号,由于基于C语言环境,Redis进程无法使用JPS命令查看到

使用redis-cli命令可以连接Redis服务

基本配置项说明

# 单位设置方式,不区分大小写,并非注释项,提示如何书写注释
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes

# 绑定连接IP,有这一行表示只允许本机连接,若要允许远程连接,需要注释这一行
bind 127.0.0.1 -::1

# 表示开启保护模式,不支持远程访问,若要允许远程连接,需要改为no
protected-mode yes

# 端口号 默认5379
port 6379

# 511表示正在进行三次握手建立TCP连接和已经建立TCP连接的队列总和
tcp-backlog 511

# 超时时间,如果有一段时间不操作,就会与redis断开连接,设为0表示不超时
timeout 0

# 心跳检测周期
tcp-keepalive 300

# Redis启动方式,是否为后台启动
daemonize yes

# 保存Redis进程号的文件,有四个级别:debug、verbose、notice、warning
pidfile /var/run/redis_6379.pid

# redis中日志级别
loglevel notice

# 设置日志的输出路径,默认为空,此时日志将输出到/dev/null
logfile ""

# 默认数据库数量
databases 16

# 设置密码,默认是注释掉的,需要设置密码的话取消注释
# requirepass foobared

# Redis同时最多可以和多少客户端连接,默认无设置
# maxclients 10000

# 最大样本数量
# maxmemory-samples 5

基本信息及操作指令

基本信息

  • Redis默认端口号是6379(可以在配置文件中修改)
  • 默认16个数据库,类似数组下标从0开始,初始默认使用0号库(可以在配置文件中修改)
  • 统一密码管理,所有库同样密码

单线程多路复用

Redis是单线程+多路IO复用技术

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一线程里执行,也可以启动线程执行(比如使用线程池)。

串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)

一个线程去执行真正的操作(数据库相关),其余线程只负责通知该线程执行的任务,其余线程通知任务是异步操作,在工作线程获得结果后,会去通知发布任务的线程

Key值操作

指令含义
keys <pattern>查看所有符合传入正则表达式规则的Key,常用keys *查看所有Key
exists <key> [key ...]判断某个key是否存在,可传入多个,有一个存在返回1,都不存在返回0
type <key>查看指定key是什么数据类型
del <key> [key ...]删除指定key数据,可以同时删除多个
unlink <key> [key ...]根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作
expire <key> <seconds>为给定的key设置过期时间,时间单位为秒
ttl <key>查看还有多少秒过期,-1表示永不过期,-2表示已过期
select <index>切换到指定索引号的数据库
dbsize查看当前数据库的key的数量
flushdb [ASYNC|SYNC]清空当前库,可指定同步异步操作
flushall [ASYNC|SYNC]清空全部库,可指定同步异步操作

常用五大数据类型

String

简介
String是Redis最基本的类型,可以理解成与Memcached一模一样的类型,一个Key对应一个Value。

String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象

String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

常用命令

命令含义
set <key> <value>添加键值对,类似于map键值对操作,新增的键值对如果键已存在,会变为覆盖操作
get <key>获取指定键对应的值,没有键会返回空(nil)
append <kye> <value>将给定的<value>追加到原值的末尾
strlen <key>获取值的长度
setnx <key> <value>只有在key不存在时,设置key的值
incr <key>将Key中存储的数字值增加1,只能对数字值操作,如果为空,新增值为1
decr <key>将key中存储的数字值减一,只能对数值操作,如果为空,新增值为-1
incrby / decrby <key> <step>将key中储存的数字值增减,自定义的步长。注意:incrdecr是原子操作
mset <key1> <value1> <key2> <value2>......同时设置一个或者多个键值对
mget <key1> <key2> <key3>......同时获取一个或者多个value
msetnx <key1> <value1> <key2> <value2> ......同时设置一个或者多个键值对,当前进党所有给定的key都不存在时操作成功
getrange <key> <start> <end>获取值的方位,类似与java中的substring,左闭右闭
setrange <key> <start> <value><value>覆写<key>所存储的字符串值,从<start>开始(索引从0开始)
setex <key> <过期时间> <value>设置键值的同时,设置过期时间,单位秒
getset <key> <value>以新换旧,设置了新值同时获得旧值

数据结构

String的数据结构是简单的动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配

当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时只会多扩容1M,需要注意的是字符串最大长度是512M。

List(列表)

简介

特点:单键多值

Redis列表时简单的字符串列表,按照插入顺序排序(有序的),你可以添加一个元素到列表的头部或者尾部

它的底层实际是一个双向链表,对两端的操作性能都很高,通过索引下标的操作中间的节点性能会较差

常用命令

命令含义
lpush/rpush <key> <value1> <value2> ...从左边/右边插入一个或多个值,lpush每次都将value放在列表头,rpush每次都将value放在列表尾
lpop/rpop <key>从左边/右边吐出一个值。值在键在,值光键亡
rpoplpush <key1> <key2>从key1列表右边吐出一个值,插入key2列表的左边
lrange <key> <start> <pop>按照索引下标获得元素(从左向右),索引可以是负值,例如-1表示从右向左第一个,-2表示从右向左第二个
lindex <key> <index>按照索引下标获得元素(从左向右)
llen <key>获得列表长度
linesert <key> before|after <value> <newvalue>在value的前/后面插入newvalue插入值
lrem <key> <n> <value>从左边删除n个value(只删除指定的值),n可以取0(不限制个数,全部删除)或者负数(代表从右边开始删除)
lset <key> <index> <value>将列表key下表为index的值替换成value

数据结构

List的数据结构为快速链表quickList

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构式ziplist,也即是压缩列表

它将所有的元素紧挨着一起存储,分配的是一块连续的内存

当数据量比较多的时候才会改成quicklist。

因为普通链表的附加指针空间太大,会比较浪费空间。比如这个列表中存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余

Set(集合)

简介

set对外提供的功能和list很相似,特殊之处在于set是自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个程序是否在一个set集合内的重要接口,这个也是list所不能提供的。

set是string类型的无序集合它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)

一个算法,随着数据的增加,执行的时间长短,如果是O(1),数据增加,查找数据的时间不变

常用命令

命令含义
sadd <key> <value1> <value2>......将一个或多个member加入到集合key中,已存在的member将被忽略
smembers <key>查看该集合中的所有值
sismember <key> <value>判断集合key是否含有value,有1,没有0
scard <key>返回该集合的元素个数
srem <key> <value1> <value2> ...删除集合中的某个元素
spop <key>随机从该集合中吐出一个值
srandmember <key> <n>随机从该集合中查看n个值。不会从集合中删除
smove <source> <destination> value把集合中的一个值value从source中移动到destination中
sinter <key1> <key2>返回两个集合的交集元素
sunion <key1> <key2>返回两个集合的并集元素
sdiff <key1> <key2>返回两个集合的差集元素(key1中的,不包含key2)

数据结构

set数据结构是dict字典,字典是用哈希表实现的。

Java中的HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象,Redis的set结构也是一样的,它的内部也适用hash结构,所有的value都指向同一个内部值

Hash(哈希)

简介

hash是一个键值对集合

hash是一个string类型的fieldvalue的映射表,hash特别适合用于存储对象。

类似于Java里面的Map<String,Object>

常用命令

命令含义
hset <key> <field> <value>给key集合中的field赋值value
hget <key> <field>从key集合field中取出value
hmset <key> <field1> <value1> <field2> <value2>批量设置hash的值
hexists <key> <field>查看哈希表key中是否包含指定field
hkeys <key>列出该hash集合中所有的field
hvals <key>列出该hash集合中所有的value
hincrby <key> <field> <increment>为hash的key中的field的值增加increment
hsetnx <key> <field> <value>当key不存在时,才进行field-value的赋值操作

数据结构

hash类型对应的数据结构是两种:zip(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

Zset

简介

有序集合zset和普通集合set非常相似,是一个没有重复元素的字符串集合

不同之处是有序集合的每个成员都关联了一个评分,这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的

因为元素是有序的,所以可以很快的根据评分或者次序来获取一个范围的元素

访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的只能列表

常用命令

命令含义
zadd <key> <score1> <value1> <score2> <value2>...将一个或多个member元素及其score值加入到有序集中
zrange <key> <start> <stop> [WITHSCORES]返回有序集中,下标在start和stop之间的元素;当带有WITHSCORES,可以让分数一起和值返回到结果中
zrangebyscore key min max [withscores] [limit offset count]返回有序集中,所有score值介于min和max之间(左右均包括)的member。有序集成员按score值递增次序排序
zrevrangebyscore key max min [withscores] [limit offset count]同上,改为从大到小排序
zincrby <key> <increment> <value>为元素的score加上增量
zrem <key> <value>删除指定值的元素
zcount <key> <min> <max>统计集合中,指定区间内元素个数
zrank <key> <value>返回该值在集合中的排名,从0开始

数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String,Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名词,还可以通过score的范围来获取元素的列表

zset底层使用了两个数据结构

  1. hash,hash的作用就是关联value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score
  2. 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表,跳跃表从链表中提取了一些索引,并在索引的基础上再次提取了索引,可以快速定位元素

Jedis操作

导入maven依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>

测试连接

// 创建Jedis对象
Jedis jedis = new Jedis("num04",6379);

// 测试连接
String value = jedis.ping();
System.out.println(value);

// 关闭连接
jedis.close();

能输出“PONG”代表成功连接到了Redis服务

如果连接失败,查看Redis启动配置文件中下面两项配置是否正确

  • bind 127.0.0.1 -::1 ==>该项需要被注释掉
  • protected-mode no ==>该项需要是no

基本方法演示

import org.junit.Test;
import redis.clients.jedis.Jedis;

import java.util.List;
import java.util.Set;

public class BasicDateType {

    // 查看全部已有key
    @Test
    public void listKeys() {
        Jedis jedis = new Jedis("num04", 6379);
        Set<String> keys = jedis.keys("*");
        for (String key : keys) {
            System.out.println(key);
        }
    }

    // 添加String类型的KV
    @Test
    public void addStringKey() {
        Jedis jedis = new Jedis("num04", 6379);
        jedis.set("name", "lucy");
    }

    // 获取String类型的Value
    @Test
    public void getStringKey() {
        Jedis jedis = new Jedis("num04", 6379);
        String nameValue = jedis.get("name");
        System.out.println(nameValue);
    }

    // String类型mset同时设置多个KV
    @Test
    public void msetString() {
        Jedis jedis = new Jedis("num04", 6379);
        jedis.mset("k1", "v1", "k2", "v2");
    }

    // String类型mget同时获取多个V
    @Test
    public void mgetString() {
        Jedis jedis = new Jedis("num04", 6379);
        List<String> values = jedis.mget("k1", "k2");
        for (String value : values) {
            System.out.println(value);
        }
    }

    // 操作List集合
    @Test
    public void testList() {
        Jedis jedis = new Jedis("num04", 6379);
        jedis.lpush("key1","value1","value2","value3");
        List<String> values = jedis.lrange("key1", 0, -1);
        for (String value : values) {
            System.out.println(value);
        }
    }

    // 操作Set集合
    @Test
    public void testSet(){
        Jedis jedis = new Jedis("num04", 6379);
        jedis.sadd("skey1","lucy","jack","jack");
        Set<String> values = jedis.smembers("skey1");
        for (String value : values) {
            System.out.println(value);
        }
    }

    // 操作hash
    @Test
    public void testHash(){
        Jedis jedis = new Jedis("num04", 6379);
        jedis.hset("users","age","20");
        String age = jedis.hget("users", "age");
        System.out.println(age);
    }

    // 操作ZSet
    @Test
    public void testZSet(){
        Jedis jedis = new Jedis("num04", 6379);
        jedis.zadd("china",100d,"shanghai");
        Set<String> res = jedis.zrange("china", 0, -1);
        System.out.println(res);
    }

    // 清空DB
    @Test
    public void flush(){
        Jedis jedis = new Jedis("num04", 6379);
        String res = jedis.flushDB();
        System.out.println(res);
    }
}

可以看到,Jedis对象的方法中基本包含了命令行中全部的可用命令,所有命令行的操作都可以简单便捷的转换为Jedis操作,其余指令就不再一一列举了

持久化操作

RDB

基本定义

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot块中,恢复时是将快照文件直接读到内存里

基本配置

生成的文件dump.rdb默认在启动redis服务时所在的目录。

该项在配置文件SNAPSHOT下有配置:dir ./

同时有快照生成时间间隔:

Unless specified otherwise, by default Redis will save the DB:
  * After 3600 seconds (an hour) if at least 1 key changed
  * After 300 seconds (5 minutes) if at least 100 keys changed
  * After 60 seconds if at least 10000 keys changed
You can set these explicitly by uncommenting the three following lines.

save 3600 1
save 300 100
save 60 10000

上面的配置默认是注释掉的,文档说明了快照生成的规则,key修改的越多,快照生成越快

save 3600 1为例,再3600秒内,如果有1个key修改了,就保存快照

如果300秒内修改了102个key,那么只会持久化100个,后面2个算在后面的持久化周期内

savemode

save:save时只管保存,其他不管,全部阻塞,手动保存。不建议使用

bgsave:Redis会在后台异步进行快照操作,快照同时还可以相应客户端请求。

可以通过lastsave命令获取最后一次成功执行快照的时间

flushall

执行flushall命令也会生成快照文件

stop-writes-on-bgsave-error

默认yes,当Redis无法写入磁盘时,直接关闭Redis写操作,推荐yes

rdbcompression

对于存储到磁盘中的快照,可以设置是否进行压缩存储,如果是的话,redis会采用LZF算法进行压缩

默认为yes

rdbchecksum检查完整性

默认yes,推荐yes

在存储快照后,使用CRC64算法进行数据校验

但是这样会增减大约10%的性能消耗,如果希望获取最大的性能提升,可以关闭此功能

备份是如何执行的

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据回复的完整性不是很敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失

Fork

  • Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数等)数值都和原进程一直。但是是一个全新的进程,并作为原进程的子进程
  • 在Linux程序中,fork会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux引入了“写时复制技术”

RDB的恢复操作

关闭Redis

先把备份的文件拷贝到工作目录下

启动Redis,备份数据会直接被加载

RDB的优缺点

优点

  • 适合大规模的数据恢复
  • 对数据的完整性和一致性要求不高更适合使用
  • 节省磁盘空间
  • 恢复速度快

缺点

  • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  • 虽然Redis在fork时候使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
  • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down的话,就会丢失最后一次快照后的所有修改

停止RDB:redis-cli config set save""#save后给空值,表示禁用保存策略

AOF持久化策略

Append Only File

以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

AOF持久化流程

  1. 客户端的请求写命令会被append追加到AOF缓冲区内;
  2. AOF缓冲区根据AOF持久化策略(always、everysec、no)将操作sync同步到磁盘的AOF文件中;
    • always:始终同步,每次Redis的写入都会立刻记录日志;性能较差但数据完整性比较好
    • everysec:每秒同步,每秒记入日志依次,如果宕机,本秒的数据可能会丢失
    • no:不主动进行同步,把同步的时机交给操作系统
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的

AOF默认是不开启的,需要修改配置文件修改为开启appendonly yes

AOF文件目录和RDB文件生成目录相同,可以修改生成文件的名称:appendfilename xxx.aof

AOF和RDB同时存在时,系统会使用AOF备份数据

AOF文件的备份恢复和RDB相同,只需要复制AOF文件,在恢复时将备份的文件放回到数据目录下就可以了

异常恢复

  • 修改默认的appendonly no,改为yes
  • 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof --fix appendonly.aof进行恢复
  • 备份被写坏的AOF文件
  • 恢复:重启redis,然后重新加载

AOF优缺点

AOF优点

  • 备份机制更稳健,丢失数据概率更低
  • 可读的日志文件,通过操作AOF文件,可以处理误操作

缺点

  • 比起RDB占用更多的磁盘空间,
  • 恢复备份速度更慢
  • 每次读写都同步的话,有一定性能压力
  • 存在个别BUG,造成恢复不能成功

选用策略

  • 官方推荐两个都启用
  • 如果对数据不敏感,单独选用RDB
  • 不推荐单独启用AOF

多结点部署

主从模式

主机数据更新后根据配置和策略,自动同步到备机的master/slave机制,Master以写为主,Slave以读为主

主从模式的作用

  • 压力分摊
  • 容灾机制

主从模式配置

  1. 复制一份配置文件到单独的目录下:mkdir /myrediscp /etc/redis.conf /myredis/redis.conf
  2. 为每个结点配置个配置文件,按照配置项配置,计划使用6379、6380和6381三个端口,所以创建了三个配置文件:redis6379.confredis6380.confredis6381.conf
include /myredis/redis.conf		# 刚才复制的主配置文件
pidfile /var/run/redis6379.pid	# 后缀和启动端口号一致即可
port 6379						# 启动端口
dbfilename dump6379.rdb			# 持久化文件名称
  1. 配置完成后启动三个redis服务:redis-server /myredis/redis6379.conf逐个启动redis-server服务
  2. 启动后使用ps -ef | grep redis命令查看,有三个服务启动
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mW7JPoPH-1624603168122)(D:\file\materials\Note\Redis\img\image-20210601230335418.png)]
  3. 三台结点登录redis客户端服务,执行info replication命令可以查看当前结点的集群情况,现在三台结点应该都是主节点,无从节点
  4. 在从节点上执行命令slaveof <ip><port>,指定主节点的IP和端口后,该节点将变为从节点。重新执行命令查看情况
    在这里插入图片描述
    8.此时可以做一个测试,在主节点上创建一个key,在其他从节点都可以查看,但从结点此时无法创建key了
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OecZ4vkD-1624603168125)(D:\file\materials\Note\Redis\img\QQ截图20210601230756.jpg)]

主从模式原理

当从服务器宕机后,剩余服务器不会受影响,只是从节点减一,但是宕机的服务器重启后,它将不再是从服务器,而是主服务器,且不会保有当前机器宕机后主服务器写入的数据,只有当再次执行slaveof <ip> <port>后才能重新称为从服务器,并同步主服务器数据

当主服务器宕机后,从服务器中不会产生新的主服务器,此时仍可执行查询操作。待主服务器重启后,它仍是集群的主服务器,且保有全部的从服务器

原理

  1. 当连接上主服务器之后,从服务器向主服务器发送数据同步请求;
  2. 主服务器接收到请求后,会将主服务器中的数据进行持久化,并发送持久化的RDB文件发送从服务器,从服务器拿到RDB进行读取
  3. 每次主服务器进行写操作后,会和从服务器进行数据同步

主从模式还有以下几种部署模式:

  1. 上一个Slave可以是下一个slave的Master(依旧使用slaveof <ip> <port>命令),Slave同样可以接收其他slaves的连接同步请求,那么该slave作为了链条中的一个mater,可以有效减轻master的同步压力,去中心化降低风险

  2. 即将从服务器转换为主服务器(slaveof no one指令),但实际这种指令和Hadoop的高可用不一样,从服务器切换为主服务器后是一个单结点,并不会作为其他从服务器的主服务器存在

  3. 哨兵模式(sentinel)
    第二种模式的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

配置步骤

  1. 调整为一主二仆模式。
  2. /myredis目录下创建sentinel.conf文件,名字绝不能错
  3. 配置文件内容:sentinel monitor mymaster 127.0.0.1 6379 1
  4. mymaster是为监控的服务器起的名字,1表示至少有多少个哨兵认为主服务器故障了,同一迁移
  5. 启动哨兵:redis-sentinel /myredis/sentinel.conf
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y6i2mhgQ-1624603168126)(D:\file\materials\Note\Redis\img\QQ截图20210602203803.jpg)]
  6. 此时被监控的主机故障后,一段时间之后,将会从其从服务器中选择一个作为新的主机
  7. 原主机重启后会变为从机

复制延迟

由于所有的写操作都是先在Master上操作,然后同步到Slave上,所以从Master同步到Slave机器会有一定的延迟,当系统很繁忙的时候,延迟问题会更见严重,Slave机器数量的增加也会使这个问题更加严重

故障恢复过程

故障恢复时,选择作为新主服务器的从服务器有一个选择条件,依次是(优先按照前面的条件选择,当前面条件相同时,逐个向后比较):

  • 选择优先级靠前的
  • 选择偏移量最大的
  • 选择runid最小的

优先级在redis.conf中设置,配置项为:replica-priority 100,即默认值为100,值越小优先级越高

偏移量是指获得原主机数据最全的

runid指每个redis实例启动后随机生成的进程ID

主备模式

Redis集群实现了对Redis的水平扩容,即启动N个结点,将整个数据库分布存储在这N个节点中,每个结点存储总数据的1/N。

Redis集群通过分区(partitions)来提供一定程度的可用性(availability):即使集群中有一部分结点失效或者无法进行通讯,集群也可以继续处理命令请求。

搭建

  1. 需要开启daemonize yes
  2. 每个结点创建一个配置文件,配置内容
include /myredis/redis.conf
pidfile "/var/run/redis6379.pid"
port 6379
dbfilename "dump6379.rdb"
cluster-enabled yes	# 表示打开集群模式
cluster-config-file nodes-6379.conf # 生成文件名
cluster-node-timeout 15000 # 连接超时时间
  1. 将修改好的配置文件拷贝多份,并修改对应内容
  2. 启动6个redis服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vq1LQYoF-1624603517618)(D:\file\materials\Note\Redis\img\QQ截图20210602223915.jpg)]
5. 将6个结点合成一个集群

- 组合前,确保redis实例启动后,nodes-xxx。conf(配置的文件名)文件正常生成

- 进入redis最初解压安装路径下的src目录下:`cd /opt/redis/src`

- 执行合成命令(这里不要使用127.0.0.1):
./redis-cli --cluster create --cluster-replicas 1 192.168.226.40:6379 192.168.226.40:6380 192.168.226.40:6381 192.168.226.40:6389 192.168.226.40:6390 192.168.226.40:6391
// 上面的1表示用最简单的方式创建集群,一主一仆搭配
  1. 合成完成后可以看到如下界面:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-weyPjF2J-1624603517620)(D:\file\materials\Note\Redis\img\QQ截图20210602223159.jpg)]
  2. 此时要登陆需要使用集群登录方式:redis-cli -c -p 6379
  3. 登录成功后使用cluster nodes命令查看集群状态信息
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VSD750L6-1624603517622)(D:\file\materials\Note\Redis\img\QQ截图20210602223412.jpg)]

redis cluster结点分配

一个集群至少要有三个结点

选项-cluster-replicas 1表示我们希望为集群中的每个主结点创建一个从结点

分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP低智商

slots

上面cluster nodes命令可以看到ALL 16384 slots covered

一个Redis集群包含16384个插槽,数据库中每个键都数据这些插槽中的一个

集群使用公式CRC16(KEY) % 16384来计算key属于哪个插槽,其中CRC16(key)语句用于计算key和CRC16校验和

集群中每个结点负责处理一部分插槽,举个例子,如果一个集群可以有主结点,其中

结点A负责处理0-5460号插槽;

结点B负责处理5461-10922号插槽;

结点C负责处理10923-16383号插槽。

故障恢复

当主节点下线,从节点将自动升为主节点,但是需要15秒超时时间过后

主节点恢复后,将做为从节点继续提供服务

如果某一段插槽的主从节点都宕掉,redis服务是否继续取决于cluster-require-full-coverage

如果值为yes,那么整个集群宕机,停止服务

如果值为no,那么对应插槽数据不能使用,其余位置可以正常使用

集群操作

集群中不能直接使用mset,mget等多键操作。

需要在每个键后面使用{},来定义组,集群键根据组算出分配的插槽位置,例如:mset name{user} lucy age{user} 20 address{user} china

查询某个键的插槽编号:cluster keyslot <key>

查询value数量:cluster countkeysinslot 12706

查询插槽中的键:CLUSTER GETKEYSINSLOT <slot> <count>

注意:只能看到当前所在服务器所属的插槽范围内的key内容

集群的Jedis操作

除了要获取专门的JedisCluster对象外,其余操作一致

public static void main(String[] args) {
    HostAndPort hostAndPort = new HostAndPort("num04", 6379);
    JedisCluster jedisCluster = new JedisCluster(hostAndPort);

    jedisCluster.set("k10", "v10");

    String value = jedisCluster.get("k10");
    System.out.println(value);

    jedisCluster.close();
}

主备模式优缺点

优点

  • 水平扩容
  • 分摊压力
  • 无中心配置相对简单

缺点

  • 多键操作不支持
  • 多键的Redis事务是不被支持的。lua脚本不被支持
  • 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大

常见应用问题

缓存穿透

问题描述
在这里插入图片描述
正常服务器接收到请求后,会先向Redis查询数据,如果Redis中不存在,会再向数据库中查询

当应用服务器压力增大,请求查询项Redis中查询数量降低(命中率降低),导致大量请求直接查询数据库,最终导致数据库瘫痪。

原因

  1. redis查询不到数据了
  2. 请求中包含了大量的非正常url(被黑客攻击,这种情况居多)

解决方案

  1. 对空值做缓存
    如果一个查询的返回数据为空(不管数据是否不存在),我们扔把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。
  2. 设置可访问的白名单:
    使用bitmaps类型定义一个可访问白名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问
  3. 采用布隆过滤器
    (布隆过滤器(Bloom Filter))是1970年由布隆提出的,它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
    布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间都远远超过一半的算法,缺点是有一定的误识别率和删除困难
    将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力
  4. 进行实时监控
    当发现Redis的命中率开始急速降低,需要排查访问对象和访问数据,和运维人员配合,可以设置黑名单限制服务

缓存击穿

问题描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZT7TjPGQ-1624603778179)(D:\file\materials\Note\Redis\img\123.jpg)]

  1. 数据库访问压力瞬时增加
  2. redis里面没有出现大量的key过期
  3. redis正常运行

原因
redis某个key过期了,而碰巧该key被大量访问

解决方案

  1. 预先设置热门数据
    在redis高峰访问之前,把一些热门数据提前存入到redis中,加大这些热门数据的时长
  2. 实时调整
    现场监控哪些数据热门,实时调整key的过期时长
  3. 使用锁
    就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
    当操作返回成功时,在进行load db的操作,并回设缓存,最后删除mutex key;
    当操作返回失败时,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法

缓存雪崩

问题描述
正常访问
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DqfOfrun-1624603778180)(D:\file\materials\Note\Redis\img\normal.png)]
缓存失效
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WVcwFW3Z-1624603778182)(D:\file\materials\Note\Redis\img\bug.png)]

  1. 数据库压力变大
  2. 服务器崩溃

原因

在极少时间段,查询大量key的集中过期情况

解决方案

  1. 构建多级缓存架构
    nginx缓存+redis缓存+其他缓存(ehcache等)
  2. 使用锁或队列
    用加锁或者队列的方式来保证不会有大量线程对数据库一次性进行读写,从而避免失效时大量的并发请求落在底层存储系统上,不适用高并发情况
  3. 设置过期标志更新缓存
    记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存
  4. 将缓存失效的时间分散开
    可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的时间

分布式锁

问题描述

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程,多进程并且分布在不同的机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题

目前分布式锁的主流实现方案:

  1. 基于数据库实现分布式锁
  2. 基于缓存(redis等)
  3. 基于zookeeper

其中基于redis的实现性能最好,基于zookeeper的安全性最高

基于redis的实现通过setnx和设置过期时间实现

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值