一,Redis作缓存服务器

​ 本篇博客是接着上一篇博客未分享完的技术点。

​ redis作为缓存服务器是众多企业中的选择之一,虽然该技术很成熟但也是存在一定的问题。就是缓存带来的缓存穿透,缓存击穿,缓存失效问题,继而引用分布式锁。

1.1,缓存穿透

​ 在如今的项目中大多采用垂直的MVC架构,由service层去调用DAO层,然后DAO层再去查询数据库。而redis作为缓存服务器就是在service层去调用DAO层去查询时先去缓存服务器查询,如果存在则直接返回该数据,否则再去查询数据库。由此可知,这么做大量减少了对磁盘I/O的操作,减轻了数据库的压力。

​ 现在我们假设一种情况,在数据库中存在有id为1到1000的数据。现在如果有人手动去模拟一个id为1001的请求,那么该数据在缓存服务器中是不存在的,因而便会去查询数据库。那么问题来了,如果是一个大量无效的请求去查询数据库。则势必会对数据库造成难以承受的压力,这种情况就是所谓的缓存穿透。

​ 那如何解决呢?

​ 1,将查询到的null值直接保存到缓存服务器中,但是这种做法并不推荐,因为如果是大量不同的请求id同样会去查询数据库。

​ 2,接口的限流,降级与熔断

​ 在项目中对于重要的接口一定要做限流,对于以上恶意攻击的请求除了要限流,还要做好降级准备,并且进行熔断,这种做法可以有效控制大量无效请求。

3,布隆过滤器

​ Bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,该做法是多数企业所选择的。

1.2,缓存击穿

​ 在高并发下,对某些热点的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上,此时这种大量的请求可能会是数据库崩盘。

​ 解决方案:

​ 1,将热点key设置成永不过期。

​ 2,使用互斥锁。

​ 以上两种情况均是属于缓存失效,但里面还有小小的细节。那就是存在多个缓存同时失效的问题,尤其在高并发时间段。为避免这种多个缓存失效的问题,我们在设置超时时间的时候,可以使用固定时间+随机时间。以最大限度避免当缓存失效时大量请求去查询数据库。

1.3,分布式锁

​ 通常情况下分布式锁有三种实现方式,1. 数据库乐观锁;2. 基于ZooKeeper的分布式锁;3. 基于Redis的分布式锁;这里只记录基于redis的分布式锁。

​ 作为分布式锁的要求:

  • ​ 互斥性: 保证在分布式应用集群中,同一把锁在同一时间只能被一台机器上的一个线程执行。
  • ​ 避免死锁:有一个客户端在持有锁的过程中崩溃而没有解锁,也能保证其他客户端能够加锁。

​ 先参看如下代码:

public List<Goods> goodsManager() {
        System.out.println("调用过了业务层的goodsManager方法");
        return goodsDao.queryAllPage();
        // 1,先去查询缓存服务器
        List<Goods> goodsList = (List<Goods>) redisTemplate.opsForValue().get("goods");
        if(goodsList == null){
            // 2,申请分布式锁
            RedisConnection conn = redisTemplate.getConnectionFactory().getConnection();
            if(conn.setNX("lock".getBytes(), "1".getBytes())){
                // 3,给分布式锁设置一个超时时间
                conn.expire("lock".getBytes(), 60);

                System.out.println("去数据库中查询所有的商品");
                // 4,缓存中没有商品列表的数据
                goodsList = goodsDao.queryAllPage();
                // 5,将结果放入缓存中
                redisTemplate.opsForValue().set("goods", goodsList);
                redisTemplate.expire("goods", 5, TimeUnit.MINUTES);
                // 6,释放分布式锁
                conn.del("lock".getBytes());
            } else {
                try {
                    Thread.sleep(50);
                    goodsManager();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return goodsList;
        } else {
           //缓存服务器中有商品列表的数据
            return goodsList;
        }
    }

​ 代码设计思路:

​ 1,请求到来调用方法。

​ 2,先去redis缓存中查询是否存在,如果没有则查询数据库。

​ 3,使用原生的连接(setNX)获得分布式锁,然后设置超时时间。

​ 设置超时时间的原因在于,如果线程获得锁之后不下心崩溃,为防止发生死锁因而设置超时时间。

​ 4,查询数据库获得数据,并保存在数据库中。

​ 5,释放锁。

1.4,雪崩效应

​ 简单来说,缓存在同一时间内大量键(key)过期(失效),而新的缓存又没有即时的保存到服务器中,此时大量的请求瞬间都落在了数据库中导致连接异常。

​ 解决方案:

​ 1、可以使用分布式锁 ,单架构项目使用syn

​ 2、永不过期

​ 3、在设置缓存超时时间,固定时间+随机超时时间,防止多数缓存同时失效。

​ 4、高可用,集群

1.5,redis缓存与springboot整合

​ 在启动函数中先要开启缓存注解@Enablecaching

@Cacheable

​ 被该注解标注的方法,会在执行前查询缓存服务器,如果缓存服务器有结果,则直接返回结果,当前方法就不会执行。如果没有结果,则在执行该方法的方法体,并且将该方法返回值放入缓存服务器中。

@CachePut

​ 该注解和@Cacheable注解的功能差不多,唯一的区别在于不管缓存服务器有没有对应的值,都会去调用相应的方法用于添加和更新的方法。

@CacheEvict

删除指定的缓存,一般用于删除方法的使用 。

二,Redis持久化

### 2.1,redis提供两种持久化方式:
  • RDB:它是备份当前瞬间Redis在内存中的数据结构(就是我们所说的快照)。
  • AOF:它的作用是当Redis执行写命令后,在一定的条件下将执行过的写命令依次保存在Redis的文件中,以后依次执行这些保存的命令就可回复Redis的数据。

2.2,RDB原理分析

​ RDB持久化有两种操作方式,手动操作进行持久化。

  • save:会阻塞当前Redis服务器,直到持久化完成,线上应该禁止使用。
  • bgsave:该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候。

​ bgsave和save最大的区别在于bgsave不会阻塞客户端的写操作,但是如果bgsave执行失败,Redis默认将停止接受接入操作,否则就没人会注意到灾难的发生,如果不希望这样做,可以将stop-writes-on-bgsave-error yes设置为no

​ 另一种为自动触发持久化,首先我们可以在配置文件中配置快照的规则。

​ save 900 1 当900秒以内执行1个写命令,使用快照备份
​ save 300 10 当300秒以内执行10个写命令,使用快照备份
​ save 60 10000 当60秒以内执行10000个写命令,使用快照备份

注意:redis执行备份命令时,将禁止写入命令

2.3,AOF原理分析

​ AOF的整个流程大体来看可以分为两步,一步是命令的实时写入,第二步是对aof文件的重写,重写是为了减少aof文件的大小。

​ AOF文件追加大致流程为:命令写入-->追加到aof_buf(缓冲区) -->同步到aof磁盘。为什么要先写入buf缓冲区在同步到磁盘呢?因为如果实时写入便会带来大量的磁盘I/O操作,会很大程度上降低系统的性能。

​ 关于AOF持久化大概有以下几种配置。

  • appendonly no 是否启用AOF备份,默认为no,不启用,如果需要启用则改为yes。
  • appendfilename "appendonly.aof" 定义追加命令写入的文件为appendonly.aof
  • #appendfsync always
  • always表示每次执行redis命令都会同步保存到AOF文件中,性能会收影响,但是安全性很高。
  • appendfsync everysec
  • evarysec(默认)表示每一秒同步一次,性能会提升,但是安全性会下降,可能丢失1秒之内的命令。
  • #appendfsync no
  • no表示不同步,需要手动执行同步命令,性能得到了保证,但是安全性太差。

2.4,Redis内存回收策略

​ 在redis.conf中的配置项maxmemory-policy用于配置redis的内存回收策略,当内存达到最大值时所采取的内存处理方式。

​ redis提供六种内存淘汰策略

  • volatile-lru:最近使用最少的淘汰策略,redis将回收那些设置了超时时间的键值对(仅仅只回收超时)。
  • allkeys-lru:采用使用最少的淘汰策略,redis将所有键值对中使用最少的。
  • volatile-random:才有随机淘汰策略删除设置了超时时间的键值对。
  • allkeys-random:采用随机淘汰策略删除所有的键值对,(不常用)。
  • volatile-ttl:删除剩余存活时间最短的键值对策略。
  • noeviction(默认):不淘汰任何键值对,当内存已经满时,进入只读模式。

​ 在内存回收机制中,LRU算法和TTl算法在redis中都不是精准计算,而是一个近似算法。redis默认有一个探测数量的配置maxmemory-samples 默认为3。

三,Redis高可用

3.1,主从复制

​ 在用户量非常庞大的时候,单台redis肯定是完全不够用的。因此更多的时候我们更希望可以读/写分离,读/写分离的前提就是读操作比写操作频繁的多,将数据放在多台服务器上那么久可以消除单台服务器的压力。

​ 因此对于服务器的搭建如图:

Redis缓存,持久化,高可用-LMLPHP

​ 假设一台服务器负责写操作,其余三台为读操作,以此实现一个独写分离的缓存功能。但是很明显存在一种弊端,就是其余三台读取数据的服务器它们之间的数据是不能够进行同步的。这样便造成数据不一致的情况,此时就需要对它们之间进行一个数据上的互通。

​ 简单介绍一下主从复制的概念,如上图Master为主,负责写入数据的操作,其余三台为从(Slave),负责读取数据操作。当有数据写入时,根据配置好的属性自动将更新的数据复制到其余三台服务器中,这样便实现了服务器之间的数据一致性。

主从复制的大致流程:

​ 1、保证主服务器(Master)的启动。

​ 2、当从服务器启动时,发送SYNC命令给主服务器。主服务器接受到同步命令时,就是执行bgsave命令备份数据,但是主服务器并不会拒绝客户端的写操作,而是将来自客户端的写命令写入缓冲区。从服务器在未收到主服务器的备份快照文件之前,会根据配置决定使用现有数据响应客户端还是返回错误。

​ 3、当bgsave命令被主服务器执行完后,开始向从服务器发送备份文件,这个时候从服务器就会丢弃现有的所有数据,开始载入发送过来的快照文件。

​ 4、当主服务器发送完备份文件后,会将bgsave执行之后的缓存区内的写命令也发送给从服务器,从服务器完成备份文件解析后,就开始等待主服务器的后续命令。

​ 5、同步完成以后,每次主服务器完成一次写入命令,都会同时往从服务器发送同步写入命令,主从同步就完成了。

从机配置:
slaveof server port 设置Master的ip和端口
masterauth root设置Master的密码

到此为止,就完成了吗?

​ 并不是,以上步骤只是完成了主从复制,并没有完成读写分离。并且,如果主(Master)服务器宕机,那整个缓存服务器就全部挂掉了。==而且作为从(Slave)服务器时不可以进行写的操作,==那又如何解决呢(哨兵模式)?

3.2,哨兵模式

1,什么时哨兵模式?

​ 当Master宕机以后需要手动的把一台Slave切换为Master,这种方式需要人工干预,费时费力。因此哨兵模式可以帮助我们解决这个问题。

2,简述哨兵模式

  • 哨兵是一个独立的进程。
  • 哨兵会检测多个redis服务器,包括Master和Slave。通过发送命令,让redis服务器响应,检测其运行状态。
  • 当哨兵检测到master宕机,就会自动将slave切换成master,然后通过发布订阅模式通知其他slave修改配置文件,切换主机。
  • 为了实现哨兵的高可用,可以配置成多哨兵模式,即多个哨兵进程运行在不同的服务器上检测各个redis服务器,哨兵两两之间也会互相监控。
  • 多哨兵模式时,Master一旦宕机,哨兵1检测到这个结果,并不会马上进行故障切换,而仅仅是哨兵1主管的认为Master不可用。当其他哨兵也检测到Master不可用时,并且有一定的数量后,那么哨兵之间就会形成一次投票,投票的结果由一个哨兵发起,进行切换操作,切换完成后,就会通过发布订阅方式让各个哨兵把自己监控的服务器实现切换主机。

3,哨兵模式配置

3.1,#配置哨兵配置文件:
redis/src/sentinel.conf

3.2,#禁止保护模式
protected-mode no

3.3,#配置监听的主服务, 最后的2表示当2个或2个以上的哨兵认为主服务不可用才会进行故障切换
sentinel monitor 服务器名称(自定义) 主服务ip 端口 2

3.4,#定义服务密码
sentinel auth-pass 服务器名称(和上面相同) 密码

3.5,#启动哨兵模式;

./redis-sentinel sentinel.conf

4,其他相关配置

sentinel down-after-milliseconds :指定哨兵在检测redis服务时,当redis服务在一个毫秒数内都无法回答时,单个哨兵认为的主观下线时间,默认为30秒。

sentinel failover-timeout:指定故障切换运行的毫秒数,当超过这个毫秒数时,就认为切换故障失败,默认3分钟。

sentinel notification-script:指定哨兵检测到redis实例异常时,调用的报警脚本。

3.3,分片集群

​ 分片集群原理在于多个缓存服务器之间两两相互通信,每个复制集具有一个主实例和多个从实例。并且每个复制集朱保存一部分数据库中的键值对,解决了主从复制集中总数据存储量最小实例的限制,大大扩大了缓存服务器的大小。

​ 其结构图如下:
Redis缓存,持久化,高可用-LMLPHP

1,分片集群特点

​ 1、Client与redis节点直接连接,不需要中间proxy层。

​ 2、 redis-cluster把所有的物理节点映射到[0-16383]slot(插槽)上,cluster 负责维护。

​ 3、所有的redis节点彼此互联(PING-PONG机制),内部使用gossip二进制协议优化传输数据。

​ 4、 节点的失效检测是通过集群中超过半数的节点检测失效时才生效。

​ ==问题:Redis 集群中内置了 16384 个哈希槽,那他是如何决定将key放入哪个插槽的?==

​ 当Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。

​ 2,集群搭建步骤

​ 前置条件:

​ 删除redis/src下的appendonly.aof,dump.rdb,nodes-6379.conf3个文件。

​ 1、修改redis.conf,配置集群信息开启集群,

cluster-enabled yes指定集群的配置文件,

cluster-config-file nodes-端口.conf

​ 2、用redis-trib.rb搭建集群因为redis-trib.rb是用Ruby实现的Redis集群管理工具,所以我们需要先安装ruby的环境.

​ 2.1、安装ruby
yum -y install zlib ruby rubygems
​ 2.2、安装rubygems的redis依赖
gem install -l redis-3.3.0.gem

​ 3、安装好依赖的环境之后,我们就可以来使用脚本命令了
​ 注意:此脚本文件在我们的解压缩目录src下。

​ 执行命令:
./redis-trib.rb create --replicas 0 192.168.10.167:6379 192.168.10.167:6380 192.168.10.167:6381 开放16379 redis端口+1W--replicas 0:指定了从数据的数量为0。

4、查看集群状态
​ 通过客户端输入以下命令:
​ cluster nodes:这个命令可以查看插槽的分配情况
​ 整个Redis提供了16384个插槽,./redis-trib.rb 脚本实现了是将16384个插槽平均分配给了N个节点。

四,总结

​ 本篇博客文字较多,大多数是描述问题的原因及解决方案。本次博客与上一篇一起,是Redis从入门到高可用。其大部分在工作中都会遇到,以此做一个浅显的记录。但其中描述的较为简单,仍有很多不可预料的问题,在此后会继续更新,也会将问题与方案及原理描述更加清楚详细。

​ 由于能力有限,更为深层次的技术点没有记录到,以上如有不适之处,还请留言(邮箱)指教。

感谢阅读!

08-25 18:58