导语
  互联网三高-高可用、高扩展、高性能,这样的一个软件结构,在真实的场景中如何落地实现。如何把合适的技术放到合适的地方,才能打造出这样的架构。不要让语言本身成为限制发展的瓶颈。

文章目录

  • 高并发下的分布式缓存
    • 为什么要使用缓存
    • Redis集群常见的两个模式
      • 主从副本模式
      • 切面模式集群
        • 如何能让数据能均匀的分布在这三台服务器上?
        • 总结
        • 一个优雅的切片规则(一致性Hash算法)
        • 一致性Hash算法是有问题的?
    • 缓存穿透
      • 布隆算法
        • 布隆算法的使用
    • 缓存雪崩

高并发下的分布式缓存

  这个话题在面试的时候很大可能会被问道,在缓存的使用上,现在使用比较多的就是Memcache和Redis。而现在使用最多的就是这个Redis。那么为什么从Memcache迁移到Redis呢?原因其实就是Redis随着不断的发展,变的越来越优秀起来。

  从所支持的数据类型来看,Memcache所支持的数据类型主要是String类型的数据,而对于Redis来说它所支持的数据类型就多了。包括 String、Map、List、ZSet、Set这些。同时Redis是一个单线程模型,在处理一些业务的时候比较方面便。

为什么要使用缓存

  在一般的场景下,我们开发的项目都是一个B/S 架构的项目,对于B/S架构的项目。如下图所示,多个客户端通过一个服务器访问数据库,数据库进行增删改查操作。

  那么随着客户端访问的次数的增加,我们的Tomcat服务器以及,后端数据库就是一个大的考验了。随着并发量,访问量不断的增加,不断的上升,其实最先达到瓶颈的就是后端数据库这一块。数据达到瓶颈,就会导致客户端的访问效率降低。那么如何解决这个问题,这个时候我们的缓存就排上用场了。

  例如我们在Tomcat 与数据库之间加入了一个Redis的缓存层。如下图所示,

  可以把经常查询的数据称为是热点数据。将这些热点数据都存储到Redis中,当Tomcat想要到数据库中获取数据的时候。就可以先在Redis中进行查找,这样的话Redis就可以阻挡一部分对于数据库的访问数据。这样对于MySQL访问的时候就会提升一定的用户体验。

  那么这样的方式固然好,但是在有些场景下,Redis是一个内存数据库,我们的内存空间不可能像是磁盘空间那样子,所以它也有用完的时候,这个时候就需要我们扩展多态机器。那么扩展的这多态机器就可以被称为是集群。

Redis集群常见的两个模式

主从副本模式

  什么是主从模式呢?就是由一个Master节点和一堆Slave节点组成。而我们这里看到的Master就是用来管理这些Slave节点的。

  如果客户端进行数据读取的时候,那么找到的是这些Slave节点,如果客户端想要去写数据的时候那么找到就是这些Master节点。往master节点中写完数据之后,Master将数据同步到各个Slave节点上,也就是说下面三个节点上的数据是一样的。

  那么搭建这样一个集群能否解决海量热点数据的存储问题?简单的分析一下,这个集群只是实现了一个读写分离的操作,将读操作拆分到了Slave节点上,将写操作放到了Master节点上,但是它所能存储的数据总量还是由其中的每台机器所能存储的数据总量所决定的。并没有解决大的数据缓存问题。

切面模式集群

  如果我们有大量的热点数据,我们可以将这些数据按照某种规则进行切片,将这些切片存储到不同的数据库服务器上。如下图所示

  那么通过这样的方式就可以看到,通过切片的方式可以在这个集群中存储大量的数据。那么既然需要切片,就需要定义一个切片的规则,那么切片的规则到底是什么样的呢?或者说如何切片数据才能更高效?同时对于数据库的上线和下线不影响数据本身。

如何能让数据能均匀的分布在这三台服务器上?

  Redis中存储数据都是以KV对的形式去存储,那么可不可以通过计算每一个Key的Hash值,然后与3取余数,那么这个时候这个得数,只有0、1、2三种结果,如果是0的话就存储到第一台数据库中,如果是1的话就是第二台,如果是2的话就是第三台。这样的话就可以将这些大量的热点数据均匀的分布到这三台服务器上。

  但是这个规则好像是一点问题的,什么问题呢?不利于集群的扩展,这里回到上面的问题。就是需要在集群进行上下线扩展的时候它能不影响数据本身,实现一个平滑的过度。那么上面说的规则就不行了。例如,新增加一台数据库,那么新增加的这台数据库的数据量一定是比前面三台的数据量小。如图所示,那么这个时候就需要把前面三台的数据往这个数据库上进行迁移。因为你新增了一台数据库之后,你进行取模运算的时候就不在是3的模,而是4的模了,那么取模的结果就出现0、1、2、3 这四种结果。

  如果是需要删除一台数据库呢?那么就需要将需要删除的数据库的内容,迁移到前面两台数据库上,这个时候还是与新增的时候是一样的。

总结

  通过上面这种方式,就会导致集群中所有的Redis发生大面积的网络IO。而Redis是单线程的模式,一旦发生了网络IO操作,那么他就不能很好的往外提供服务了。就会导致大面积的Redis不可用。

一个优雅的切片规则(一致性Hash算法)

  现在有一个环,我们给这个环起一个名字叫做Hash环。如下图所示。假设一开始的时候,准备了也是三台服务器Redis1、Redis2、Redis3。数据也是很多的热点数据。


  接下来就是利用Hash环来组织这些数据,那么如何去组织呢?这个就是比较重要的一致性Hash算法。在准备服务器的时候这个服务器一定是有一个ip的,我们可以将这个服务器地址看作是一个服务器的编号。

  接下来的操作就是需要对这个服务的IP+编号进行Hash,计算一个Hash值,然后再与2的32次方取余,也就是说Hash环上可以有2的32次方的点分布在这个环上。那么这个余数的值一定是在0~2^32-1这个数据之间。这个计算结果就作为这个服务器在这个环上的某一个点上的坐标进行映射。

  假设映射完成之后结果如下图所示


  接下来就是,将数据的key与2^32次方取余,这个计算结果也是在0~2的32次方-1之间的数据。假设映射的数据与节点的分布如下图所示


  然后将这些数据顺时针进行查找,将数据存储在顺时针查找的第一个服务器上。

  通过这样的方式将数据与服务器关联到一起了。这样进行切片有什么好处呢?这就解决了集群扩展或者删除的问题。

  假设增加了一个Redis4 的服务器,那么通过上面计算完成之后它一定也在这个环的某一个位置,如图所示,我们只需要把前半段数据进行迁移到Redis4 上就可以了,原来存储在4到3 之间的数据顺时针的指向还是指向3。所以不需要进行迁移,当然删除一个服务器节点的时候也是一样的。这样就只有一台服务器的数据进行迁移。

一致性Hash算法是有问题的?

  那么一个算法有好的地方,就会有不好的地方,要辩证的去看,那么会出现什么样的问题呢?那就是数据倾斜问题。什么是数据倾斜问题呢?如果对于一个存储框架,数据倾斜,就是大部分的数据存储在少量的服务器上,少量的数据存储在大部分的服务器上,对于计算框架来说就是大部分的数据有少量的服务器计算,少量的数据由大部分的服务器计算。

  对于Redis来说它是存储框架,那么它所描述的数据倾斜就大量的数据存储在少量的服务器上,少量的数据存储到大量的服务器上。

  那么对于一致性Hash来说为什么会出现在这样的问题呢?如图所示,如果经过计算之后发现如下的一个数据情况,那么我们就会看到,其实在Redis2上存储的只有少部分数据。

  那么如何解决这个问题?我们可以在这个环上多虚拟几个Redis的点。例如将每个Redis的服务器都虚拟成2个点,都分布在这两个点,那么经过计算之后,他虚拟出来的点就会有不同的分布。

  这样的话数据分布就如下图所示了。那么有人就会说,这个只是一个理想的状态,如果不是一个理想的状态呢?那么可以虚拟出多个来,就可以很顺利的解决这个问题,或者是你就虚拟出2^32次方个来,当然是极端情况了。

缓存穿透

  假设上面的操作完成了,这些大量的热点数据已经被我们有效的存储了,但是这个时候,如果有人往这个缓存集群中发送id=-1 的数据进行操作,那么这个时候,发现缓存中没有的话,就会到数据库中进行查询,这个时候数据库中也没有。如果有大量的这样的数据的操作,还是会导致MySQL的瘫痪。这样的话就相当于Redis集群不存在,缓存是没有的一样。这样Redis并没有起到一个限流的作用。这种情况就被称为是缓存穿透。

  缓存穿透不可怕,可怕的是大量的缓存穿透。

  这个情况也好解决,既然拿了id=-1的数据进行查询操作,那么数据库返回的数据是null,那么我们就将-1与null的键值对进行缓存。当第二次请求的时候就可以从缓存中找到对应的null的数据进行返回。但是人家也不傻,如果人家再次更换新的数据进行查询,就会导致Redis缓存被撑爆了,就会导致缓存不可用,最终还是会请求到数据库中的。当然短时间内会出现这样的问题,但是长时间的话,缓存是有一定的淘汰策略的例如说LRU、LFU等等。但是这样的话用户体验也不是太好。所以不能采用这样的方案。

  既然上面的方案是有问题的的,那么如何解决呢。可以搞一个过滤器,如果在Redis中没有找到数据,那么在这个过滤器中就保存了所有的数据库中id。经过过滤器之后,就会知道需要查找的这个数据在数据库中有没有,如果有的话操作,如果没有的话,直接将请求拒绝。但是这个过滤器,随着Mysql数据的增加,会占用大量的内存。所以需要解决过滤器内存占用过大的问题。

布隆算法

  这个时候就出现了一个算法,这个算法就是布隆算法。可以使用这个算法来降低这个过滤器的内存使用率。它是通过错误率来获取空间的占用。

  算法原理: 它由一个二进制码数组,假设长度为10。布隆算法,就是通过这个数组中的数来标识这个MySQL里面的ID号。例如现在要标记一条id=100 的值,会把这个id值传入到一个Hash函数中,这个Hash函数的计算结果永远是在0~9之间,如果经过计算值为1 的时候,那么就会在第二个位置插入1,其他地方都是0。

  操作如下图所示。

  如果现在要标识一个id=999的数据也是同样的操作。

  那么这两个位置的数据就分别表示id=100和id=999。那么就会知道,这个肯定是有错误率的,如下图所示,如果有如下的一个操作id=1000 计算出来第二个位置还是1,那么就表示这个位置即表示了id=100也表示id=1000


  那么当Client 请求了一个id=9988的数据,想要看一下这个数据是否被标记,那么这个值的计算结果也有可能是1,这个时候计算结果1,但是真实的情况是这条数据并没有被标识过。这其实就是一个错误率的体现。所以说如果使用布隆算法,出现数据存在,那么其实也是不一定的,那么如果使用布隆算法出现数据不存在,那么数据一定是不存在的。采用这种方式是比较节省空间的。

  也就是说使用一个比特位就可以表示一条数据存在。通过这种方式,会发现这种关系的错误率还是比较高的。那么这个错误率高的原因就是数组的长度太短了,那么如果数组的长度越长,那么hash冲突的概率就越小,将这种存储的粒度称为是装填比。也就是装填比越高,错误率越低。

  当然装填比是一个方式,那么如果我们将Hash计算的次数增多了之后呢?如果第一个hash的冲突的概率是1/10,第二个也是1/10,这样数量增多了之后,也会降低错误率。只有三个位置同时为1 的时候,才表示这个数存在,如果不满足的话,就说明这个数据不存在。

布隆算法的使用

  有两个文件1、2 每个文件中有100亿个url,使用最高的效率,找出文件交集,分别给出精准算法和模糊算法。

  这个问题考察的就是布隆算法。

精准的算法,采用分治法。拆分成一个个的小文件,怎么拆?根据Hash进行拆分,需要拆分成多少个,就对多少取模就可以了。所以只需要对比对应的小文件就可以了。而不是把第一个小文件与其他所有文件进行对比,因为没有必要。

  第一遍遍历拆分,第二遍把小文件进行比较。

模糊算法 ,布隆算法,将文件读取过来之后,通过一个hash函数,这个hash函数,会在数组中去标记一个数据,例如第一条记录经过hash函数之后可以在数组中标记一个位置。第二个文件通过同样的方式进行计算,就可以知道对应的url是否出现过,这里有个错误率的问题,所以是模糊查找。这样的操作,只需要读取两个文件一次就可以了。

缓存雪崩

  如果缓存集群同时宕机,或者是缓存的值同时失效,都会导致缓存雪崩。解决缓存雪崩一个是建立备用集群,一个是设置随机的过期时间。

实战系列-分布式缓存相关推荐

  1. asp按时间自动递增编号_Java秒杀系统实战系列-分布式唯一ID生成订单编号

    本文是"Java秒杀系统实战系列文章"的第七篇,在本文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们将介绍两种方法 ...

  2. Java秒杀系统实战系列~分布式唯一ID生成订单编号

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第七篇,在本博文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们 ...

  3. java 唯一编号_Java秒杀系统实战系列~分布式唯一ID生成订单编号

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第七篇,在本博文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们 ...

  4. 实战系列-分布式锁的Redis实现

    导语   本篇博客,博主使用本地的Docker 搭建了一套测试环境,用来手写一个属于自己的基于Redis分布式锁实现方案,通过自己实现来了解分布式锁的原理.并且对整个的构建过程做了分享,希望可以对大家 ...

  5. memcached java 客户端优化,分布式缓存技术memcached学习系列(五)—— memcached java客户端的使用...

    Memcached的客户端简介 我们已经知道,memcached是一套分布式的缓存系统,memcached的服务端只是缓存数据的地方,并不能实现分布式,而memcached的客户端才是实现分布式的地方 ...

  6. 云栖社区特邀专家徐雷——Java Spring Boot开发实战系列课程【往期直播回顾】...

    徐雷,花名:徐雷frank:资深架构师,MongoDB中文社区联席主席,吉林大学计算机学士,上海交通大学硕士.从事了 10年+开发工作,专注于分布式架构,Java Spring Boot.Spring ...

  7. [.NET领域驱动设计实战系列]专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现...

    原文:[.NET领域驱动设计实战系列]专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现 一.引言 在上一专题中,商家发货和用户确认收货功能引入了消息队列来实现的,引入消息队列的好处可以保证 ...

  8. java设计前期工作基础和存在的困难_Java秒杀系统实战系列-基于Redisson的分布式锁优化秒杀逻辑...

    本文是"Java秒杀系统实战系列文章"的第十五篇,本文我们将借助综合中间件Redisson优化"秒杀系统中秒杀的核心业务逻辑",解决Redis的原子操作在优化秒 ...

  9. Java秒杀系统实战系列~基于Redisson的分布式锁优化秒杀逻辑

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十五篇,本文我们将借助综合中间件Redisson优化"秒杀系统中秒杀的核心业务逻辑",解决Redis的原子 ...

最新文章

  1. 框架:Hibernate和Mybatis的区别
  2. Win32多线程编程(2) — 线程控制
  3. 前端学习(3196):虚拟dom和真实dom
  4. mysql 两字段相乘_sql统计2列相乘和语句.doc
  5. ggforce|绘制区域轮廓-区域放大-寻找你的“onepiece”
  6. MFC中App、Doc、MainFrame、View各指针的互相获取
  7. Linux之time命令
  8. ResourceExhaustedError 解决方案
  9. jdbc如何使用oracle数据库连接池,使用JDBC连接池技术连接Oracle数据库
  10. Python_命名空间和作用域_25
  11. 报童问题求解最大利润_Ortools调用第三方求解器
  12. 类型多样的建筑场景unity3d模型素材,速来收藏
  13. 2019寒假·纪中记Day0-Day3
  14. mysql blast2go,blast2go
  15. 离线安装selenium
  16. Python get-pip.py文件
  17. electron 通过nodejs的winre库实现软件开机自启动
  18. 菜鸟程序员成长史 --记 Github 1000+ contributions
  19. linux挂载u盘的方法,linux挂载U盘的方法
  20. 第三十二章 XML基础知识概念

热门文章

  1. linux shell命令 复制,将bash提示符下的当前命令复制到剪贴板
  2. Java线程面试题TOP50
  3. 看了些关于rem的知识点,在这做个自我总结归纳
  4. 电梯调度需求调研报告
  5. sysctl修改内核参数
  6. 一位 女生在第一次应聘软件开发工作时成功被淘汰的例子
  7. 一个图的带权邻接表存储结构的应用
  8. 售前工程师的成长---一个老员工的经验之谈(三)
  9. 十字星文化获数千万元A轮融资,腾讯持续下注
  10. Linux系统CentOS 7配置Spring Boot运行环境