Redis 相关知识点(持续更新)

一、什么是NoSQL

要介绍Redis前必须要先介绍下NoSQL,这两者间密不可分。什么是NoSQL?

NoSQL即(not only SQL)不仅仅是SQL,泛指非关系数据库/技术。非关系数据库在高并发的场景下有巨大优势,这点MySQL等关系型数据库是无法相比的。另外NoSQL在数据分析数据挖掘也更胜一筹。Redis和MongoDB是当前较流行的NoSQL。

二、Redis和MySQL的区别

两者间外在的区别主要体现在性能的差别:Redis的单位时间内的读/写速度往往是MySQL几倍到十几倍。

之所以两者有这么大性能差异主要因为两方面:

  1. MySQL数据持久化时面向硬盘,而Redis面向内存,所以读写更快
  2. MySQL存储和查询都更加复杂,要考虑索引、范式等,Redis存储和查询都更加简单,且Redis是单线程的非阻塞IO(IO多路复用)处理那么多的并发客户端连接多路复用IO,单线程避免了线程切换的开销,而多路复用IO避免了IO等待的开销。

Redis确实有很多优点,但是目前不能完全替代Mysql等关系型数据库,因为Redis也有很多的缺点,比如存储在内存上如果掉电就会丢失,内存相较硬盘代价高昂,虽然有事务但确实只能覆盖简单场景等,现实中往往是MySQL和Redis结合使用。

举个例子:在整点秒杀商品时,大量请求到来时,Mysql要在短时间内执行大量的SQL,很容易造成数据库“罢工”,这样的场景一般会考虑异步写入数据库,而在高速读写时使用Redis来抵挡这些大量请求,在满足一定的条件时,触发这些缓存在Redis中的数据写入数据库。

也就是请求到达时都是在redis中进行写,没有进行数据库的写操作,由于redis的高性能这样保证快速响应。当请求不在那么多时,或者业务已经结束时(比如商品秒杀完了,红包抢完了),将Redis缓存的数据写入数据库进行持久化。

同理,在读取的时候也可以优先读取Redis缓存,在Redis读取失败是再从MySQL等数据读取,如下:

三、什么场景下考虑使用Redis?

上面说了Redis往往和MySQL结合使用,那么到底什么时候考虑使用Redis?主要从下面3个方面考虑:

  1. 要操作的数据命中率(是否经常使用)高不高,如果命中率低没必要使用Redis;
  2. 读写谁更多?若写操作多于读操作,也没有必要使用Redis;
  3. 数据大小如何?若较大会给内存带来很大压力,也没必要使用Redis

四、Redis支持的6种数据类型

类型 存储的值 说明
String 字符串、整数、浮点数 可以对字符串做增加或求子串,整数和浮点数可以实现简单计算
List
它是链表,它的每个节点都包含一个字符串
Red 支持从链表(双向)的两端插入或者弹出节点,或在通过偏移对'8 进行裁剪;还可以读取一个或者多个节点,根据条件删除或者查找节点等
Set
它里面每个元素都是个字符串,且是各不相同、无序的,Set是string类型的无序集合。
可以新增、读取、删除单个元素,检测一个元索是否在集合中,计算它和 集合的交集、并集和差集等;
Hash
类似Map ,是个键值对应的无序列表
参见Map集合
ZSet 
有序集合,可以包含字符串、整数、浮点数、分值(score),排序依据分值的大小决定。 zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
可以地、删、查、改元素,根据分值 泡因或者成员来获取对应的元素
HyperLogLog 基数,计算重复的值, 确定存储数量
只提供基数的运饵,不提供返回的功能

补充说明:

基数是一种算法。举个例子 一本英文著作由数百万个单词组成,你的内存却不足以存储它们,那么我们先分析下业务。英文单词本身是有限的,在这本书的几百万个单词中有许许多多重复单词,扣去重复的单词,这本书中也就 千到 万多个单词而己,那么内存就足够存储它 了。比如数字集合{ l,2,5 1,5,9 }的基数集合为{ 1,2,5 }那么 基数(不重复元素)就是基数的作用是评估大约需要准备多少个存储单元去存储数据,基数并不是存储元素,存储元素消耗内存空间比较大,而是给某个有重复元素的数据集合( 般是很大的数据集合〉评估需要的空间单元数。 

五、Spring中集成Redis

添加依赖

<!-- Redis依赖 -->
<dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <version>2.7.1</version>
</dependency>

做个简单的性能测试:

public void performance() {
    Jedis jedis = new Jedis("localhost", 6379);
    int i = 0;
    long start = System.currentTimeMillis();
    while (true) {
       long end = System.currentTimeMillis();
       if (end - start >= 1000) {
           break;
       }
       i++;
       jedis.set("key" + i, "value" +i);
    }
    System.out.println("redis每秒写入" + i + "次");
}

结果:redis每秒写入19023次。这里是自测结果,是串行执行,是一条条执行的,若采用流水线技术则会高得多。

六、获取Redis连接的两种方式 及 两种操作方式

 获取Redis连接有两种方式:

  1. 通过jedisPool
  2. 通过JedisConnectionFactory连接工厂

两种方式也分别对应两种操作方式:

  1. Jedis

  2. RedisTemplate

下面分别介绍

七、通过jedisPool获取redis连接

上面的redis是单个链接,实际中更多的会使用连接池。主要是Jedis jedis = jedisPool.getResource();

public void pool() {
   JedisPoolConfig poolConfig = new JedisPoolConfig();
   // 最大空闲数 控制一个pool最多有多少个状态为idle的jedis实例;
   poolConfig.setMaxIdle(50);
   // 最大连接数
   poolConfig.setMaxTotal(100);
   // 当池内没有返回对象时,最大等待时间
   poolConfig.setMaxWaitMillis(20000);
   // 创建连接池
   JedisPool pool = new JedisPool(poolConfig, "localhost");
   // 从连接池获取一个链接
   Jedis jedis = pool.getResource();
   int i = 0;
   long start = System.currentTimeMillis();
   while (true) {
       long end = System.currentTimeMillis();
       if (end - start >= 1000) {
           break;
       }
       i++;
       jedis.set("key" + i, "value" +i);
   }
   System.out.println("redis每秒写入" + i + "次");
}
redis每秒写入16794次

八、通过JedisConnectionFactory连接工厂获取Redis连接

为了方便的进行Redis的读写Spring给我们提供了RedisTemplate类,但在使用该类之前先要选择一种连接工厂,共有2种(原来是4种,其中两个已过时)连接工厂:

  1. JedisConnectionFactory
  2. LettuceConnectionFactory

这2个都是RedisConnectionFactory接口的实现类,以JedisConnectionFactory为例,它的源码如下:

如果我们想从jedisConnectionFactory获取Jedis实例又想使用连接池。可以这样:

	private Jedis getJedis() {
		if (jedis == null) {
            jedisConnection = jedisConnectionFactory.getConnection();
            jedis = jedisConnection.getNativeConnection();
			return jedis;
		}
		return jedis;
	}

九、Jedis 和 RedisTemplate

Jedis是Redis官方推荐的面向Java的操作Redis的客户端,而RedisTemplate是spring-data-redis.jar包中对JedisApi的高度封装。
spring-data-redis.jar相对于Jedis来说可以方便地更换Redis的Java客户端,比Jedis多了自动管理连接池的特性,方便与其他Spring框架进行搭配使用如:SpringCache

 十、RedisTemplate

有了 RedisConnectionFactory 工厂,就可以使用 RedisTemplate了。RedisTemplate是Spring封装的来进行对Redis的各种操作的类,它支持所有的Redis原生的api。RedisTemplate位于spring-data-redis包下。我们以短信验证码为例,产生验证码,将该验证码存储到Redis:

    @ApiOperation(value = "生成并发送短信验证码")
    @PostMapping("sendVerificationCode")
    public JsonResult sendVerificationCode(@RequestBody SendMessageRequest sendMessageRequest) throws ApiException {
        // 生成4位验证码并存储到Redis缓存(缓存有效时长5分钟)
        String verifyCode = RandomStringUtils.randomNumeric(4);
        String key = KEY_PREFIX + "-" + sendMessageRequest.getPhoneNo();
        redisTemplate.opsForValue().set(key, verifyCode, 300L);
        System.out.println("SERVER_URL: " + SEND_SERVER_URL);
        // 发送生成的短信验证码
        .....................
        return isSuccess ? JsonResult.ok("发送短信成功!") : JsonResult.error("手机号" + sendMessageRequest.getPhoneNo() + "发送短信失败!");
    }

这里的存储操作是redisTemplate.opsForValue().set(key, verifyCode, 300L);

为什么说有了 RedisConnectionFactory 工厂才能进行RedisTemplate的操作?我去看追踪一下 opsForValue() 这个方法的源码:

再看 ValueOperations<K, V> 这个类,看它的 set(K key, V value方法:

这里有个 execute()方法,

这里就明白了为啥说:有了 RedisConnectionFactory 工厂,才可以使用 RedisTemplate了,因RedisTemplate对象在操作前先要获取与Redis的连接。

十一、RedisSerializer 

普通的连接使用没有办法把 Java 对象直接存入 Redis ,需要将java对象序列化( 序列化是将对象转换为一系列字节)后再存入Redis。同理,从Redis读取字符串后,反序列成java对象。 RedisSerializer接口是Spring提供的完成以上对象序列化操作的一个接口,它的实现类有很多:
 

以 StringRedisSerializer 为例,首先被存储的对象需要实现序列化接口 :

public class User implements Serializable {

    private int id;
    private String name;
    private String phone;

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getPhone() {
        return phone;
    }

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", name=" + name + ", phone=" + phone + "]";
    }

}
 public void test(RedisTemplate redisTemplate) {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        User user = new User();
        user.setId(1);
        user.setName("dsds");
        user.setPhone("110");
        redisTemplate . opsForValue() .set ("user1 ", user);
        User user1 = (User) redisTemplate . opsForValue() . get ("user1 ") ;
        System.out.println(user1.getName()) ;
 }

十二、SessionCallback 和 RedisCallback

set 和 get 方法看起来很简单,但它可以来自于 Redis 连接池的不同 Redis 的连接。 为了使得所有的操作都来自于同一个连接,可以使用 SessionCallback 或者 RedisCallback 两个接口,而 RedisCallback 较底层, 封装使用不是很友好,所以更多的时候会使SessionCallback 接口, 通过这个接口就可以把多个命令放入到同一个 Redis 连接中去执行,写法如下:
    public void test(RedisTemplate redisTemplate) {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        User user = new User();
        user.setId(1);
        user.setName("dsds");
        user.setPhone("110");

        SessionCallback callBack = new SessionCallback<User>() {
            @Override
            public User execute(RedisOperations ops) throws DataAccessException {
                ops.boundValueOps("user1 ").set(user);
                return (User) ops.boundValueOps("user1 ").get();
            }
        };    
        User savedUser = (User) redisTemplate.execute(callBack);    
    }

十三、Redis的事务

Redis 开启事务是 multi 命令,而执行事务是 exec 命令。multi 和 exec 命令之间的 Redis 令将采取进入队列的形式,直至exec 命令的出现,才会 次性发送队列里的命令 去执行,而在执行这些命令的时候其他客户端就不能再插入任何命令了,这就是 Redis 事务机制。

如果回滚事务,则可以使用 discard 命令,它就会进入在事务队列中的命令,这样事务中的方法就不会被执行了:

当使用了 discard 令后 ,再使用 exec 命令时就会报错,因为 discard 令已经取消了事务中的命令,而到了 exec 时,队列里面己经没有命令可以执行了,所以就出现了报错的情况。

 十四、发布订阅

当使用银行卡消费的时候,银行往往会通过微信、短信或邮件通知用户这笔交易的信息,这便是一 种发布订阅模式, 这里的发布是交易信息的发布,订阅则是各个渠道。这实际工作中十分常用, Redis 支持这样的发布订阅 模式。

这里涉及到一个模式:观察者模式,可以参考:https://blog.csdn.net/weixin_41231928/article/details/105037562

发布订阅模式首先需要消息源,也就是要有消息发布出来,比如例子中的银行通知,首先是银行的记账系统,收到了交易的命令,成功记账后,它就会把消息发送出来,这个 时候,订阅者就可以收到这个消息进行处理了,观察者模式就是这个模式的典型应用。

这个过程可以使用redis客户端模拟一下:

这里打开了3个客户端,使用 SUBSCRIBE命令来注册两个订阅的客户端(观察者,上面两个客户端),chat是一个通道,这个通道里有发布信息就会被所有订阅这个通道的客户端接收到。
再使用第三个客户端(发布者,下面的一个客户端)向chat通道发布消息;
最后两个观察者都同时收到了发布的消息。
 

14.1  监听类

实际代码怎么实现这种订阅发布的?先看是怎么实现监听的或者说订阅的,只要是要实现 MessageListener 接口,并重写onMessage() 方法即可,如下:
 
public class Demo1 implements MessageListener {
    
    @Override
    public void onMessage(Message message, byte[] pattern) { 
        // 获取消息
        byte[] body= message . getBody() ;
        // 获取 channel
        byte[] channel = message . getChannel();
    }
}

这里你肯定想知道Message类有啥,它实际是个接口,源码如下:

Message接口有个实现类DefaultMessage,源码如下:
 

14.2 怎么发布?

上面已经准备好接收了,那怎么发布信息呢?
public void publishMessage(){
    String channel = "chat";
    redisTemplate.convertAndSend(channel, "Hello");
}
convertAndSend 方法就是向渠道 chat 发送消息的, 当发送后对应的监听者就能监听到消息了。
 
 

十五、Redis 内存回收策略

之所以要讨论是因为Redi 也会因为内存不足而产生错误,也可能因为回收过久而导致系统长期的停顿,因此掌握执行回收策略十分有必要。
 
这个内存回收策略可以在,redis.windows.conf(redis.conf,Lunix)中配置,可以看下文档中的说明:
有6种策略,默认为:noeviction
 
  1. olatil -lru: 采用最近使用最少的淘汰策略, 只淘汰那些超时(仅仅是超时的)的键值对
  2. allkey-lru: 采用淘汰最少使用的策略, 将对所有的(不仅仅是超时的)键值对采用最近使用最少的淘汰策略
  3. olatil-random :采用随机淘汰策略删除超时的(仅仅是超时的)键值对
  4. allkeys- random : 采用随机、淘汰策略删除所有的(不仅仅是超时的)键值对,这个策 略不常用
  5. volatile-ttl: 采用删除存活时间最短的键值对策略
  6. noeviction:  根本就不淘汰任何键值对 当内存己满时 如果做读操作,例如 get 它将正常工作,而做写操作,它将返回错误 也就是说 Redis 采用这个策略内存达最大时, 它就只能读而不能写了。
  7. volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
  8. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  9. allkeys-lfu:从所有键中驱逐使用频率最少的键

十六、Redis的持久化机制

redis是一个内存数据库,数据保存在内存中,但是我们都知道内存的数据变化是很快的,也容易发生丢失。但Redis为我们提供了持久化的机制,分别是RDB(Redis DataBase)和AOF(Append Only File)。所有的配置都是在redis.conf文件中,里面保存了RDB和AOF两种持久化机制的各种配置。先看下Redis的持久化流程。

16.1 Redis持久化流程

  1. 客户端向服务端发送写操作(数据在客户端的内存中)。

  2. 数据库服务端接收到写请求的数据(数据在服务端的内存中)。

  3. 服务端调用write这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中)。

  4. 操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)。

  5. 磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)。

16.2 RDB机制

RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。

既然RDB机制是通过把某个时刻的所有数据生成一个快照来保存,那么就应该有一种触发机制,是实现这个过程。对于RDB来说,提供了三种触发机制:save、bgsave、自动化。

16.2.1 save触发方式

该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,即不能响应其他客户端的请求,直到RDB过程完成为止。

16.2.2 bgsave触发方式

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

具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。

16.2.3 自动触发

自动触发是由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置,我们可以去设置:

①save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。表示60 秒内如果至少有 10000 个 key 的值变化,则保存save 60 10000。

②stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了

③rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。

④rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

⑤dbfilename :设置快照的文件名,默认是 dump.rdb

⑥dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。

RDB 的优势和劣势:

①、优势

(1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。

(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。

(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

②、劣势

RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。

16.3 AOF机制

全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的“写”命令都通过write函数追加到文件中。通俗的理解就是日志记录。每当有一个写命令过来时,就直接保存在我们的AOF文件中。如果 appendonly 配置为 no ,则不启用 AOF 方式进行备份。

16.3.1 AOF原理

AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

AOF也有三种触发机制

(1)每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好

(2)每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失

(3)不同no:从不同步

16.3.2 AOF优缺点

优点:

(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。

(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。

(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

缺点:

(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。

16.4 RDB和AOF到底该如何选择

选择的话,两者加一起才更好。因为两个持久化机制你明白了,剩下的就是看自己的需求了,需求不同选择的也不一定,但是通常都是结合使用。有一张图可供总结:

对于快照备份而言, 果当前 Redis 的数 据量大,备份可能造成Redis 卡顿,但是恢复重启 快速 ;对于 AOF 备份而言,它 只是追 加写入命令,所以备份一 般不会造 Red is 卡顿, 但是恢复重启要执 行更多的 命令, 备份文件可能 很大。
 

十七、Redis 部署架构

17.1 单机版

问题:1、内存容量有限 2、处理能力有限 3、无法高可用。
 

17.2 主从复制

Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。只要主从服务器之间网络连接正常,主从服务器两者会具有相同数据,主服务器就会一直将发生在自己身上的数据更新同步给从服务器,从而一直保证主从服务器的数据相同。 

特点:
1、master/slave 角色
2、master/slave 数据相同
3、降低 master 读压力在转交给从库
 
问题:1、无法保证高可用(即master挂了,则后面的所有slave都会受到影响);2、没有解决 master 写的压力
 

17.3 哨兵

哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是通过发送命令,等待 Redis 务器响应,从而监控运行的多个 Redis 实例。
这里的哨兵有两个作用:
  1. 通过发送命令,让Redis务器返回监测其运行状态,包括主服务器和从服务器。
  2. 当哨兵监测到 master 宕机, 会自动在slave中选举新的master,将这个被选举的slave 切换成 master ,然后通过发布订阅模式通知到其他的从服务器,修改配置文件,让它们切换主机
只是现实中1个哨兵进程对 Redis 服务器进行监控,也可能出现问题,为了处理这个问题,还可以使用多个哨兵的监控,而各个哨兵之间还会相互监控,这样就变为了多个哨兵模式。多个哨兵不仅监控各个 Redis 务器,而且哨兵之间互相监控,看看各个哨兵是否还“活”着。
 
论述下故障切换( failover )的过程。假设主服务器宕机,哨兵1先监测到这个结果,当时系统并不会马上进行 failover 操作 ,而仅仅是哨兵1观地认为主机己经不可用,这个现象被称为主观下线。当后面的哨兵监测也监测到了主服务器不可用 并且有了一定数量的哨兵认为主服务器不可用,那么哨兵之间就会形成一次投票,投票的结果由一个哨兵发起,进行 failover 操作,在 failover 操作的过程中切换成功后,就会通过发布订阅方式,让各个哨兵把自己监控的服务器实现切换主机 这个过程被称为客观下线。整个过程客户端对于客户端是透明的,即所有客户端无感知。
 

Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移。其中三个特性:
监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。
特点
1、保证高可用
2、监控各个节点
3、自动故障迁移
问题:主从模式,切换需要时间丢数据,没有解决 master 写的压力。

17.4 集群(proxy型)

Twemproxy 是一个 Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器; Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和 redis 协议。
特点
1、多种 hash 算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
2、支持失败节点自动删除
3、后端 Sharding 分片逻辑对业务透明,业务方的读写方式和操作单个 Redis 一致
问题:
增加了新的 proxy,需要维护其高可用。failover 逻辑需要自己实现,其本身不能支持故障的自动转移可扩展性差,进行扩缩容都需要手动干预。

17.5 集群(直连型)

从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
特点:
1、无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
2、数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
3、可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
4、高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本
5、实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。
问题:
1、资源隔离性较差,容易出现相互影响的情况。
2、数据通过异步复制,不保证数据的强一致性

补充 - 1:redis的通信协议

redis的通信协议是Redis Serialization Protocol,简称RESP,有如下特性:

  • 是二进制安全的
  • 使用TCP
  • 基于请求-响应的模式

需注意的是:RESP是redis客户端和服务端通信的协议,节点交互不使用这个协议。

补充 - 2:什么是缓存穿透?什么是缓存雪崩?什么是缓存穿透?何如避免?

1、缓存穿透:

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

    如何避免?

  1. 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
  2. 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

2、缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。

产生雪崩的原因之一,比如马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。

在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。

     如何避免?

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  2. 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
  3. 不同的key,缓存失效时间加入随机因子,尽量让失效时间点均匀分布,设置不同的过期时间。

2、缓存穿透

      缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

     如何避免?

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

补充 - 3:热点数据 和 冷数据 是什么

热数据:是需要被计算节点频繁访问的在线类数据。
冷数据:是对于离线类不经常访问的数据,比如企业备份数据、业务与操作日志数据、话单与统计数据。

热数据就近计算,冷数据集中存储。热数据因为访问频次需求大,效率要求高,所以就近计算和部署;冷数据访问频次低,效率要求慢,可以做集中化部署,而基于大规模存储池里,可以对数据进行压缩、去重等降低成本的方法。

从数据分析的层面来看,不仅有冷热两种数据,还有温数据。而提出这个概念的是个灯,个灯是这么介绍的:

  1. 冷数据——性别、兴趣、常住地、职业、年龄等数据画像,表征“这是什么样的人”;
  2. 温数据——近期活跃应用、近期去过的地方等具有一定时效性的行为数据,表征“最近对什么感兴趣”;
  3. 热数据——当前地点、打开的应用等场景化明显的、稍纵即逝的营销机会,表征“正在哪里干什么”

为了处理冷热数据识别与交换,阿里云自主研发了Redis混合存储产品,是的完全兼容Redis协议和特性的混合存储产品。通过将部分冷数据存储到磁盘,在保证绝大部分访问性能不下降的基础上,大大降低了用户成本并突破了内存对Redis单实例数据量的限制。

Redis混合存储实例将所有的Key都认为是热数据,以少量的内存为代价保证所有Key的访问请求的性能是高效且一致的。而对于Value部分,在内存不足的情况下,实例本身会根据最近访问时间,访问频度,Value大小等维度选取出部分value作为冷数据后台异步存储到磁盘上直到内存小于制定阈值为止。

在Redis混合存储实例中,我们将所有的Key都认为是热数据保存在内存中是出于以下两点考虑:

1、Key的访问频度比Value要高很多。

作为KV数据库,通常的访问请求都需要先查找Key确认Key是否存在,而要确认一个key不存在,就需要以某种形式检查所有Key的集合。在内存中保留所有Key,可以保证key的查找速度与纯内存版完全一致。

2、Key的大小占比很低。

即使是普通字符串类型,通常的业务模型里面Value比Key要大几倍。而对于Set,List,Hash等集合对象,所有成员加起来组成的Value更是比Key大了好几个数量级。

因此,Redis混合存储实例的适用场景主要有以下两种:

  1. 数据访问不均匀,存在热点数据;
  2. 内存不足以放下所有数据,且Value较大(相对于Key而言)

冷热数据识别:

当内存不足时的情况下,实例会按照最近访问时间,访问频度,value大小等维度计算出value的权重,将权重最低的value存储到磁盘上并从内存中删除。

补充 - 4:为什么Redis是单线程的,优势

Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

具体的原因:

1)不需要各种锁的性能消耗

在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

2)CPU消耗

采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。

Redis单线程的优劣势

1.单进程单线程优势

  • 代码更清晰,处理逻辑更简单
  • 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
  • 不存在多进程或者多线程导致的切换而消耗CPU

2.单进程单线程弊端

  • 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;

以上也是Redis能够支持高并发的原因。

补充 - 5:如何解决redis的并发竞争key问题?

这个问题大致就是,同时有多个子系统去set一个key。

方案:

(1)如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
(2)如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.

期望按照key1的value值按照 valueA-->valueB-->valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下:

系统A key 1 {valueA  3:00}
系统B key 1 {valueB  3:05}
系统C key 1 {valueC  3:10}

那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。

其他方法,比如利用队列,将set方法变成串行访问也可以。

补充 - 6:如何保证Redis与数据库的数据一致性?

一般来说,只要你用到了缓存,不管是Redis还是memcache,就可能会涉及到数据库缓存与数据的一致性问题。

首先考虑清楚一点:那就是到底是更新DB还是更新缓存更合适?这个很关键,目前来讲更新缓存是一件赔本买卖,原因是:

  • 大多数情况下,redis缓存中的数据并不是完全copy db中的数据,而是将db中多张表的数据进行了重新计算,筛选后更新到redis。如果在db某一张表的数据发生了变化的情况下,需要同步重新计算redis中的值,成本过高。
  • 缓存更新后的新值,无法保证一定会有读请求命中,如果一直没有请求命中该部分冷数据,其实是产生了一定的资源浪费(计算成本+存储成本)。

所以首先的出的结论就是:最好不要更新缓存,因为代价高昂,能删除则删除。

那么删除缓存和更新DB谁先谁后呢?首先想到的可能就是:

  1. 更新的时候,先更新数据库,然后再删除缓存
  2. 读的时候,先读缓存;如果没有的话,就读数据库,同时将数据放入缓存,并返回响应。

但这样会有一个问题:若先更新了数据库,删除缓存的时候失败了怎么办?那么数据库中是新数据,缓存中是老数据,数据出现就出现了不一致。那么先删除缓存,后更新数据库呢,因为即使后面更新数据库失败了,缓存是空的,读的时候会从数据库中重新拉,虽然都是旧数据,但数据是一致的,即:

  1. 更新的时候,先删除缓存,然后再更新数据库
  2. 读的时候,先读缓存;如果没有的话,就读数据库,同时将数据放入缓存,并返回响应。

到这里是不是问题就得到了彻底的解决了呢?其实并没有,在高并发的场景下,会出现这样的情况:数据发生了变更,先删除了缓存,然后去修改数据库。此时还没来得及修改,一个请求过来了,去读缓存,发现缓存空了,去读数据库,读到了准备修改前的旧数据,并且把旧数据放到了缓存。随后,数据变更程序完成了数据库的修改。那么现在数据又不一致了。

所以有了下面几种方案:

(1)串行队列方案:

可以先把“修改DB”的操作放到一个JVM队列,后面读请求过来之后,“更新缓存”的操作也放进同一个JVM队列,每个队列,对于一个作业线程,按照队列的顺序,依次执行相关操作。也就是通过队列使得“修改DB”一定是在“更新缓存”之前。当然这个方案还可以优化:

  1. 读请求过多的时候,队列里面会有多个“更新缓存”操作串在一起,其实是没有意义的,往队列里面塞数据的时候可以先判断一下,有的话就不用再塞进去
  2. 遇到更新DB比较频繁的业务场景时,可能会导致读请求长时间阻塞,这个时候可以通过扩机器增加吞吐量,或者可以先返回一个旧的值。

(2)延时双删策略

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。伪代码:

public void write( String key, Object data )
{
    redis.delKey( key );
    db.updateData( data );
    Thread.sleep(  );
    redis.delKey( key );
}

步骤:

  1. 先删除缓存
  2. 再写数据库
  3. 休眠500毫秒
  4. 再次删除缓存

实际过程:

  • A请求进行写操作,先淘汰缓存
  • B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中。注意,此时redis中被更新的依然是老数据,A请求的数据库更新操作尚未完成。假设该步骤耗时N秒
  • A请求进行数据库更新操作。
  • 由于此时redis中写入了老数据,因此A请求在休眠M秒后(M略大于N),再次对redis进行淘汰缓存操作
  • 该方案虽然解决了数据不一致的问题,但是由于请求A在更新完数据库之后,需要休眠M秒再次淘汰缓存,一定程度上影响了数据更新操作的吞吐量。可以尝试将等待M秒更新redis的操作放到另一个单独的线程(比如消息队列 + 重试机制)。可以有效缓解吞吐量降低的问题。

(3)异步更新缓存(基于订阅binlog的同步机制)

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis。

流程:

  1. 更新数据库数据;
  2. 数据库会将操作信息写入binlog日志当中;
  3. 订阅程序提取出所需要的数据以及key;
  4. 另起一段非业务代码,获得该信息;
  5. 尝试删除缓存操作,发现删除失败;
  6. 将这些信息发送至消息队列;
  7. 重新从消息队列中获得该数据,重试操作;

一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。上述的订阅binlog程序在mysql中有现成的中间件叫canal(阿里的一款开源框架)通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果,可以完成订阅binlog日志的功能,也可以使用定时任务等去控制删除失败重试次数、时间、频率。

总结:

一般来说,若不是系统需要严格要求缓存和数据库必须一致的话,缓存可以允许稍微的跟数据库偶尔有不一致的情况,上面的更新DB和更新缓存的串行话可以防止不一致的产生,但是串行后系统的吞吐量就会大幅降低,需要比正常情况下多几倍的机器去支持。

猜你喜欢

转载自blog.csdn.net/weixin_41231928/article/details/106183561
今日推荐