从SpringBoot构建十万博文聊聊高并发文章浏览量设计

前言
在经历了,缓存、限流、布隆穿透等等一系列加强功能,十万博客基本算是成型,网站上线以后也加入了百度统计来见证十万+ 的整个过程。
但是百度统计并不能对每篇博文进行详细的浏览量统计,如果做一些热点博文排行、48小时排行之类统计,还需要引入浏览量统计功能。
设计
通常情况下,我们只需要每次请求浏览量+1,但是这样真的好吗?或者更直白的讲,真实浏览数准确吗?
UPDATE blog SET views = views+1 WHERE id=?
参考了多个社区博客的设计,因为并不十分清楚其后端实现过程,只能从前端得出以下结论。
慕课网手记:无论是用户登录模式还是用户状态,每次刷新页面浏览数都会 +1。
51CTO博客:无论是用户登录模式还是用户状态,每次刷新页面浏览数都会 +1。
简书:用户登录模式下,无论如何刷新浏览数都不会新增,但是游客状态下每次刷新浏览数都会+1。
博客园:无论是用户登录模式还是用户状态,每次刷新页面浏览数都不变,即使隔天访问,也不变,没细测。
微信公众号:只能是用户登录状态,每次刷新浏览数基本不变,有时候会出现由多变少的情况,不知道大家有没有发现。
CSDN博客:无论是用户登录模式还是用户状态,每次刷新页面浏览数都不变,但是隔天访问,浏览数会+1,没细测。
基于以上社区的数据,直接 Pass 掉前两位,总结了以下几种方案,都是基于缓存标识实现。
如果游客或者登录用户访问,按照 IP + 文章 ID 维度增加浏览数,那局域网中怎么算?
如果是游客访问,按照 IP + 浏览器SessionId + 文章 ID 维度增加浏览数,可能解决局域网问题,那么关闭浏览器,重新打开又怎么算?
如果是登录用户,用户ID + 文章 ID 维度增加浏览数,那么游客在登录后算不算一个浏览数,或者是用户换个 IP 登录算不算 ?
所以说,怎么算都不准确,浏览数本身就是一个不需要太精确的功能,不要想太多,直接使用 IP + 文章ID 维度即可。
方案
方案一
得到 GET 请求,在限流之后,缓存之前,判断缓存中是否存在 IP+ 文章ID是否存在 Key。
如果存在,说明之前浏览过,就什么也不做。如果没有,就加上这个 Key,根据业务设置缓存失效时间,然后更新数据库浏览量+1,下面是代码实现:
//获取 Key
String key = IPUtils.getIpAddr()+":blog:"+id;
//判断是否存在
boolean flag = redisUtil.hasKey(key);
if(!flag){
//设置缓存标识并更新数据库
redisUtil.set(key,"true",36000);
String nativeSql = "UPDATE blog SET views = views+1 WHERE id=?";
dynamicQuery.nativeExecuteUpdate(nativeSql,new Object[]{id});
}
方案二
这样基本能保证真实的博文浏览量,你以为就这么结束了吗?我们做的可是一个高并发的博客,直接落库,显得不是逼格太 Low 了!
为了进一步提升性能力,来做下一步优化,判断不存在之后,先不急于更新数据库,先在 Redis 里给这篇文章的浏览量+1,Key 为 viewCount:articleId,value 为缓存的浏览量。然后设置一个定时任务,定时更新 Redis 缓存数据到数据库。
这样,是不是逼格一下子提升了好几个档次!!!下面来介绍一款更有逼格的第三方计数工具。
方案三
一款高并发计数神器 Redis HyperLogLog,她是用来做基数统计的算法,优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
什么是基数?比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。
为了校验准确性,博主特意测试了一下,分别测试了,20000 和 100000 的数据量,基本上用了 12KB。
在测试之前 info 查询一下:
used_memory_human:910.14K
测试之后,可以说基本差不多:
used_memory_human:922.27K
下面我们通过代码来实现,引入 redis starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
这里,我们只需要两个API即可:
/**
* 计数
* https://blog.52itstyle.vip
* @param key
* @param value
*/
public void add(String key, Object... value) {
redisTemplate.opsForHyperLogLog().add(key,valu);
}
/**
* 获取总数
* https://blog.52itstyle.vip
* @param key
*/
public Long size(String key) {
return redisTemplate.opsForHyperLogLog().size(key);
}
然后写个AOP:
@Around("ServiceAspect()")
public Object around(ProceedingJoinPoint joinPoint) {
Object[] object = joinPoint.getArgs();
Object blogId = object[0];
Object obj = null;
try {
String value = IPUtils.getIpAddr();
String key = "viewCount:" + blogId;
// key 为 文章ID,Value 为请求IP地址
redisUtil.add(key,value);
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return obj;
}
博文请求:
/**
* 博文
* https://blog.52itstyle.vip
*/
@RequestMapping("{id}.shtml")
public String page(@PathVariable("id") Long id, ModelMap model) {
try{
Blog blog = blogService.getById(id);
String key = "viewCount:"+id;
Long views = redisUtil.size(key);
//直接从缓存中获取并与之前的数量相加
blog.setViews(views+blog.getViews());
model.addAttribute("blog",blog);
} catch (Throwable e) {
return "error/404";
}
return "article";
}
业务代码:
/**
* https://blog.52itstyle.vip
* 执行顺序
* 1)限流
* 2)布隆
* 3)计数
* 4) 缓存
* @param id
* @return
*/
@Override
@ServiceLimit(limitType= ServiceLimit.LimitType.IP)
@BloomLimit
@HyperLogLimit
@Cacheable(cacheNames ="blog")
public Blog getById(Long id) {
String nativeSql = "SELECT * FROM blog WHERE id=?";
return dynamicQuery.nativeQuerySingleResult(Blog.class,nativeSql,new Object[]{id});
}
最后,写个定时任务,夜间入库:
@Scheduled(cron = "0 30 23 * * ?")
public void createHyperLog() {
logger.info("计数落库开始");
String nativeSql = "SELECT id FROM blog";
List<Object> list = dynamicQuery.query(nativeSql,new Object[]{});
list.forEach(blogId ->{
String key = "viewCount:"+blogId;
Long views = redisUtil.size(key);
if(views>0){
String updateSql = "UPDATE blog SET views=views+? WHERE id=?";
dynamicQuery.nativeExecuteUpdate(updateSql,new Object[]{views,blogId});
redisUtil.del(key);
}
});
logger.info("计数落库结束");
}
小结
撸完计数功能,作为一个个人博客基本上差不多了已经,前后端框架、连接池、限流、缓存、计数、动静分离,HTTPS安全认证、百度收录等等,后面会追加后台管理,模板、插件等等一系列功能,有兴趣的小伙伴可以一起参与进来啊啊啊啊啊啊......
案例
源码:https://gitee.com/52itstyle/spring-boot-blog
列表:https://blog.52itstyle.top/index
博文:https://blog.52itstyle.top/51.html
参考
从SpringBoot构建十万博文聊聊高并发文章浏览量设计的更多相关文章
- 从SpringBoot构建十万博文聊聊缓存穿透
前言 在博客系统中,为了提升响应速度,加入了 Redis 缓存,把文章主键 ID 作为 key 值去缓存查询,如果不存在对应的 value,就去数据库中查找 .这个时候,如果请求的并发量很大,就会对后 ...
- 从SpringBoot构建十万博文聊聊限流特技
前言 在开发十万博客系统的的过程中,前面主要分享了爬虫.缓存穿透以及文章阅读量计数等等.爬虫的目的就是解决十万+问题:缓存穿透是为了保护后端数据库查询服务:计数服务解决了接近真实阅读数以及数据库服务的 ...
- 从SpringBoot构建十万博文聊聊Tomcat集群监控
前言 在十万博文终极架构中,我们使用了Tomcat集群,但这并不能保证系统不会出问题,为了保证系统的稳定运行,我们还需要对 Tomcat 进行有效的运维监控手段,不至于问题出现或者许久一段时间才知道. ...
- 聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类
这篇说说java.util.concurrent.atomic包里的类,总共12个.网上有非常多文章解析这几个类.这里挑些重点说说. watermark/2/text/aHR0cDovL2Jsb2cu ...
- 聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore
前几篇分析了一下AQS的原理和实现.这篇拿Semaphore信号量做样例看看AQS实际是怎样使用的. Semaphore表示了一种能够同一时候有多个线程进入临界区的同步器,它维护了一个状态表示可用的票 ...
- 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁
上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和基本的方法,显示了怎样 ...
- 聊聊高并发(三十四)Java内存模型那些事(二)理解CPU快速缓存的工作原理
在上一篇聊聊高并发(三十三)从一致性(Consistency)的角度理解Java内存模型 我们说了Java内存模型是一个语言级别的内存模型抽象.它屏蔽了底层硬件实现内存一致性需求的差异,提供了对上层的 ...
- 聊聊高并发(三十二)实现一个基于链表的无锁Set集合
Set表示一种没有反复元素的集合类,在JDK里面有HashSet的实现,底层是基于HashMap来实现的.这里实现一个简化版本号的Set,有下面约束: 1. 基于链表实现.链表节点依照对象的hashC ...
- 聊聊高并发(十八)理解AtomicXXX.lazySet方法
看过java.util.concurrent.atomic包里面各个AtomicXXX类实现的同学应该见过lazySet方法.比方AtomicBoolean类的lazySet方法 public fin ...
随机推荐
- Spring Boot2(十三):整合定时任务发送邮件
一.前言 主要玩一下SpringBoot的定时任务和发送邮件的功能.定时发送邮件,这在实际生成环境下主要用户系统性能监控时,当超过设定的阙值,就发送邮件通知预警功能.这里只通过简单的写个定时结合邮件通 ...
- springboot项目问题记录one
上面三个方法描述如下: 首先有个业务,我是需要调取第三方一个sdk,然后sdk里面封装的kafka,也就是说,需要用sdk内置的连接kafka去消费消息,然后又有一个类需要实现Message,此Mes ...
- 【CYH-02】NOIp考砸后虐题赛:成绩:题解
这道题挺送水了吧... 两种做法. 其实空间都不需要那么大,我来提交界面一看一堆MLE的,于是还是良心的放开了时限. 这么简单,就不做解释了. 下面放出几种代码. 代码: 两个数组: #include ...
- mongo去重统计
表名:parkUserCost id: patkId: userId: phone: costVal: 适合特定条件下,对某些字段进行去重筛选.(比如限定抢购) 第一种,使用\(first操作符.\) ...
- 小白学python之整型,布尔值,十进制二进制转换和字符串详解for循环!
整型与字符串转化 十进制转二进制. python2,存在int 整型和long(长整型),在python3里就是int/获取的是浮点数 小数 print(bin(15)) 这样可以通过代码来计算十进制 ...
- 个人永久性免费-Excel催化剂功能第81波-指定单元格区域内容及公式填充
在日常数据处理过程中,需要对缺失数据进行填充时,按一定逻辑规则进行处理,实现快速填充,规范数据源.此篇给大家带来多种填充数据的场景. 业务使用场景 对各种系统中导出的数据,很多时候存在数据缺失的情况, ...
- [LeetCode] 107 Binary Tree Level Order Traversal II (easy)
原题 层序遍历,从自底向上按层输出. 左→右→中 解法一 : DFS,求出自顶向下的,最后返回时反转一下. class Solution { public: vector<vector<i ...
- Visual Studio 调试系列2 基本调试方法
系列目录 [已更新最新开发文章,点击查看详细] 在 Visual Studio 上下文中,当调试应用时,这通常意味着你在附加了调试器的情况下(即在调试器模式下)运行应用程序. 执行此操作时,调 ...
- mySQL相关函数的使用
获取执行SQL指令被影响的记录数或字段数 ·mysqlo_num_rows()函数:适用于执行SELECT语句,可以返回被筛选出来的记录数. 其语法如下,参数result为资源标识符 mysqlo_n ...
- PHP-- B/S结构
B/S结构(Browser/Server,浏览器/服务器模式),是WEB兴起后的一种网络结构模式,WEB浏览器是客户端最主要的应用软件.这种模式统一了客户端,将系统功能实现的核心部分集中到服务器上,简 ...