当前位置:首页 > 服务端 > 舒服,又偷学到一个高并发场景面试题的解决方案。

舒服,又偷学到一个高并发场景面试题的解决方案。

2022年11月09日 08:08:55服务端6

高并发 PV 问题

他的文章标题是这样的:

舒服,又偷学到一个高并发场景面试题的解决方案。 _ JavaClub全栈架构师技术笔记

首先他给出了一个业务场景:在一些需要统计 PV(Page View), 即页面浏览量或点击量高并发系统中,如:知乎文章浏览量,淘宝商品页浏览量等,需要统计相应数据做分析。

比如我的公众号阅读量大概在 3000 左右,如果要统计我这种小号主的 PV 其实就很简单,就用 Redis 的 incr 命令轻轻松松就实现了。

但是,假设微信公众号每天要统计 10 万篇文章,每篇文章的访问量 10 万,如果采用 Redis 的 incr命令来实现计数器的话,每天 Redis=100 亿次的写操作,按照每天高峰 12 小时来算,那么 Redis 大约 QPS=57万。

如此大的并发量,CPU 肯定满负载运行,网络资源消耗也巨大,所以直接使用 incr 命令这种技术方案是行不通的。

舒服,又偷学到一个高并发场景面试题的解决方案。 _ JavaClub全栈架构师技术笔记

假设这是一个面试场景题,你会怎么去回答呢?

其实你也别想的有多复杂,剥离开场景,这无外乎就是一个高并发的问题。

而高并发问题的解决方案,基本上逃不过这三板斧:缓存、拆分、加钱。

所以这个老哥给出的方案就是:缓存。

二级缓存

Redis 都已经是缓存了,那么再加缓存算什么回事呢?

那就算是二级缓存了。

而且这个缓存,就在 JVM 内存里面,比 Redis 还快。

其核心思想是减少 Redis 的访问量。这些理论的东西,大家应该都知道。

那么通过什么方案去减少 Redis 的访问量呢,这个二级缓存应该怎么去设计呢?

首先,文章服务采用了集群部署,在线上可以部署多台。

然后每个文章服务,增加一级 JVM 缓存,即用 Map 存储在 JVM,key 为当前请求所属的时间块。

就是这个意思:

Map<Long,Map<Integer,Integer>> = Map<时间块,Map<文章id,访问量>>

但是我觉得巧妙的地方在于这里提到的“时间块”的概念。

什么是时间块?

就是把时间切割为一块块,例如:一篇文章在1小时,30分钟、5分钟的时间内产生了多少阅读量。

如何切割时间块呢?

这里利用到了时间是不断增长的特性。

时间戳是自 1970 年1月1日(00:00:00 GMT)至当前时间的总数,通过确定时间块大小,算出当前请求所属的时间戳从 1970 年算起位于第几个时间块,这个算出来的第几个时间块就是小时 key ,即 map 的 key。

举个例子:

我们把时间按照“小时”的维度进行划分。

先把当前的时间转换为为毫秒的时间戳,然后除以一小时,即当前时间 T/1000*60*60=小时key,然后用这个小时序号作为key。

比如:

2021-12-26 15:00:00 = 1640502000000毫秒

那么小时key= 1640502000000/1000*60*60=455695,即是距离 1970 年开始算的第 455695 个时间块。

2021-12-26 15:10:00 = 1640502600000毫秒,那么算出来的 key =455695.1,向下取整,key 还是等于 455695。

意思是这段时间的时间块是一样的,所以统计到 JVM 内存中的 Map 的时候,对应的 key 是一样的。

画个图示意一下:

舒服,又偷学到一个高并发场景面试题的解决方案。 _ JavaClub全栈架构师技术笔记

上图中,在 2021-12-25 15:00:00 到 2021-12-25 15:59:59 时间段内产生的阅读量都会映射到 Map 的 key= 455695 的位置去。

在 2021-12-25 16:00:00 到 2021-12-25 16:59:59 时间段内产生的阅读量都会映射到 Map 的 key= 455696 的位置去。

以此类推,每一次PV操作时,先计算当前时间是那个时间块,然后存储Map中。

整体方案

当我们把数据缓存到内存中之后,就极大的减少了对于 Redis 的访问。

但是我们还是得把数据同步到 Redis 里面去,因为访问文章数据的时候还是得从 Redis 中获取数据。

所以,这里就涉及到一个问题:什么时候、怎么把数据同步到 Redis 呢?

看一下作者给出的方案设计:

舒服,又偷学到一个高并发场景面试题的解决方案。 _ JavaClub全栈架构师技术笔记

整体流程还是比较清楚,主要说一下里面的两个定时任务。

其中一级缓存定时器的逻辑是这样的:假设每 5 分钟(可以根据需求调整)从 JVM 的 Map 里面把时间块的阅读 PV 读取出来,然后 push 到 Redis 的 list 数据结构中。

list 存储的数据为 Map<文章Id,访问量PV>,即每个时间块的 PV 数据。

另外一个二级缓存定时器的逻辑是这样的:每 6 分钟(需要比一级缓存的时间长),从 Redis 的 list 数据结构中 pop 出数据,即Map<文章Id,访问量PV>。

然后把对应的数据同步到 DB 和 Redis 中。

代码实战

代码主要分为四个步骤,我也把代码粘过来给大家看看。

步骤1:PV请求处理逻辑

//保存时间块和pv数据的map
public  static final  Map<Long, Map<Integer,Integer>> PV_MAP=new ConcurrentHashMap();

/**
 * pv请求调用:
 * 即当前时间T/1000*60*60=小时key,然后用这个小时序号作为key。
 * 例如:
 * 2021-11-09 15:30:00 = 1636443000000毫秒 
 * 小时key=1636443000000/1000\*60\*60=454567.5=454567
 *
 * 每一次PV操作时,先计算当前时间是那个时间块,然后存储Map中。
 * @param id 文章id
 */
public void addPV(Integer id) {
    //生成环境:时间块为5分钟
    //为了方便测试 改为1分钟 时间块
    int timer=1;
    long m1=System.currentTimeMillis()/(1000*60*timer);
    //拿出这个时间块的所有文章数据
    Map<Integer,Integer> mMap=Constants.PV_MAP.get(m1);
    if (CollectionUtils.isEmpty(mMap)){
        mMap=new ConcurrentHashMap();
        mMap.put(id,new Integer(1));
        //<1分钟的时间块,Map<文章Id,访问量>>
        Constants.PV_MAP.put(m1, mMap);
    }else {
        //通过文章id 取出浏览量
        Integer value=mMap.get(id);
        if (value==null){
            mMap.put(id,new Integer(1));
        }else{
            mMap.put(id,value+1);
        }
    }
}

步骤2:一级缓存定时器消费

定时(5分钟)从 JVM 的  Map 把时间块的阅读 PV 取出来,然后 push 到 Reids 的 list 数据结构中,list 的存储的数据为 Map<文章id,访问量PV> 即每个时间块的 PV 数据

/**
 * 一级缓存定时器消费调用方法:
 * 定时器,定时(5分钟)从jvm的map把时间块的阅读pv取出来,
 * 然后push到reids的list数据结构中,list的存储的书为Map<文章id,访问量PV>即每个时间块的pv数据
 */
public void consumePV(){
    //为了方便测试 改为1分钟 时间块
    long m1=System.currentTimeMillis()/(1000*60*1);
    Iterator<Long> iterator= Constants.PV_MAP.keySet().iterator();
    while (iterator.hasNext()){
        //取出map的时间块
        Long key=iterator.next();
        //小于当前的分钟时间块key ,就消费
        if (key<m1){
            //先push
            Map<Integer,Integer> map=Constants.PV_MAP.get(key);
            //push到reids的list数据结构中,list的存储的书为Map<文章id,访问量PV>即每个时间块的pv数据
            this.redisTemplate.opsForList().leftPush(Constants.CACHE_PV_LIST,map);
            //后remove
            Constants.PV_MAP.remove(key);
            log.info("push进{}",map);
        }
    }
}

步骤3:二级缓存定时器消费

定时(5分钟),从 Redis 的 list 数据结构 pop 弹出 Map<文章id,访问量PV>,弹出来做了2件事:

  • 先把 Map<文章id,访问量PV>,保存到数据库

  • 再把 Map<文章id,访问量PV>,同步到 Redis 缓存的计数器 incr

步骤4:查看浏览量

用了一级缓存,所有的高并发流量都收集到了本地 JVM,然后 5 分钟同步给二级缓存,从而给 Redis 降压。

@GetMapping(value = "/view")
public String view(Integer id) {
   //文章pv的key
    String key= Constants.CACHE_ARTICLE+id;
    //调用redis的get命令
    String n=this.stringRedisTemplate.opsForValue().get(key);
    log.info("key={},阅读量为{}",key, n);
    return n;
}

对应视频

另外,我在 B 站也找到这篇文章对应的视频:

https://www.bilibili.com/video/BV1PY411p7MG?p=1

舒服,又偷学到一个高并发场景面试题的解决方案。 _ JavaClub全栈架构师技术笔记

舒服,又偷学到一个高并发场景面试题的解决方案。 _ JavaClub全栈架构师技术笔记

如果大家有没有看明白的地方,可以去 B 站看一下对应的视频,讲的还是很清楚的。

整体方案是没有问题的,时间块的设计也非常的巧妙。

当然了如果你非要找方案的瑕疵的话,那就是数据时效性和数据一致性的问题了。

其实我了解到这个方案之后,我还是觉得万变不离其宗,这个方案就是一种合并提交的理念。

比如我之前写过的这篇文章,就聊到了请求合并的这个概念,有兴趣的可以去看看:

《面试官问我:什么是高并发下的请求合并?》

荒腔走板

前几天趁着圣诞节这个机会,顺便求了个婚:

舒服,又偷学到一个高并发场景面试题的解决方案。 _ JavaClub全栈架构师技术笔记

为什么是顺便呢?

因为 Merry Christmas,里面有 Marry me,所以可以假借圣诞节之名,行求婚之实。本来我的计划是不经意间把 Merry Christmas 变化成 Marry me 的。

但是她一回家就发现了,然后对我说:你知道吗,其实你可以把 Merry Christmas 变成 Marry me,这样就变成求婚了。

她边说就开始边操作。

虽然这不在我的计划内,但是谁摆都是摆,所以等她摆字母的时候,我已经单膝跪地,她一转头才发现原来这就是一场求婚。

求婚只需要一分钟,但是这一分钟会是宝贵的回忆。

作者:小兮雯学Java
来源链接:https://blog.csdn.net/slw20010213/article/details/122344121

版权声明:
1、JavaClub(https://www.javaclub.cn)以学习交流为目的,由作者投稿、网友推荐和小编整理收藏优秀的IT技术及相关内容,包括但不限于文字、图片、音频、视频、软件、程序等,其均来自互联网,本站不享有版权,版权归原作者所有。

2、本站提供的内容仅用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯相关权利人及本网站的合法权利。
3、本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站(javaclubcn@163.com),我们将第一时间核实后及时予以删除。


本文链接:https://www.javaclub.cn/server/68653.html

标签: 高并发面试
分享给朋友:

“舒服,又偷学到一个高并发场景面试题的解决方案。” 的相关文章

Spring Cloud面试问题

Spring Cloud面试问题

问:什么是Spring Cloud?     答: Spring Cloud Stream App Starters是基于Spring Boot的Spring Integration应用程序,提供与外部系统的集成。Spring Cloud Task。...

并发编程|说完AQS,面试官为何不淡定了?

并发编程|说完AQS,面试官为何不淡定了?

你能说下什么是AQS AQS是队列同步器AbstractQueueSynchronizer的简写,它是用来构建锁和其他同步组件的基础框架,它定义了一个全局的int 型的state变量,通过内置的FIFO(先进先出)队列来完成资源竞...

Java探针-Java Agent技术-阿里面试题

Java探针-Java Agent技术-阿里面试题

 Java探针参考:Java探针技术在应用安全领域的新突破     最近面试阿里,面试官先是问我类加载的流程,然后问了个问题,能否在加载类的时候,对字节码进行修改 我懵逼了,答曰不知道,面试官说可以的,使用Java探针技术,能够实现...

看完这篇Exception 和 Error,和面试官扯皮就没问题了

看完这篇Exception 和 Error,和面试官扯皮就没问题了

在 Java 中的基本理念是 结构不佳的代码不能运行,发现错误的理想时期是在编译期间,因为你不用运行程序,只是凭借着对 Java 基本理念的理解就能发现问题。但是编译期并不能找出所有的问题,有一些 NullPointerException 和 ClassNotFoundExceptio...

JAVA面试精选【Java基础第二部分】

  上一篇,我们给出了大概35个题目,都是基础知识,有童鞋反映题目过时了,其实不然,这些是基础中的基础,但是也是必不可少的,面试题目中还是有一些基础题目的,我们本着先易后难的原则,逐渐给出不同级别的题目,猛料还在后头呢,继续关注哦。   这一章我们继续接下来的35个题目,这些题目...

Spring Boot面试题汇总,含答案

Spring Boot面试题汇总,含答案

1 什么是springboot ? 用来简化spring应用的初始搭建以及开发过程 使用特定的方式来进行配置(properties或yml文件) 创建独立的spring引用程序 main方法运行  嵌入的Tomcat 无需部署war文件&nb...

Java高级面试题整理(附答案)

这是我收集的10道高级Java面试问题列表。这些问题主要来自 Java 核心部分 ,不涉及 Java EE 相关问题。你可能知道这些棘手的 Java 问题的答案,或者觉得这些不足以挑战你的 Java 知识,但这些问题都是容易在各种 Java 面试中被问到...

5道必问的Python爬虫面试题及答案

5道必问的Python爬虫面试题及答案

爬虫是Python的重要应用方向之一,也是学习Python的学员求职的主要方向。为了帮助学员更快更好的通过企业面试,我悉心整理了5道Python爬虫面试题及答案,希望能够给大家提供帮助! 1、简要介绍下scrapy框架及其优势 scrapy是...

Java面试题_第四阶段

Java面试题_第四阶段

1.1 电商行业特点 1.分布式   垂直拆分:根据功能模块进行拆分   水平拆分:根据业务层级进行拆分 2.高并发      用户单位时间内访问服务器数量,是电商行业中面临的主要问题 3....

【golang 必备算法】链表篇

【golang 必备算法】链表篇

203.移除链表元素 力扣链接 创建一个虚拟头节点 func removeElements(head *ListNode, val int) *ListNode { p:=&ListNode{} p.Next=head q:=p for p!=nil&am...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。