• 网站文章的浏览量是怎么设计的?
  • 发布于 2个月前
  • 223 热度
    0 评论
前言

现在基本所有的网站,不管是新闻博客,还是电商,短视频都会有一个浏览量或者热度的属性,以显示文章或者商品的受欢迎程度。那么这个热度或者说浏览量该怎么设计才比较合理呢?没点击浏览一次就Update数据库的浏览量属性+1?本文就来探讨一下这个问题。


设计
通常情况下,我们只需要每次请求浏览量+1,但是这样真的好吗?或者更直白的讲,真实浏览数准确吗?
UPDATE blog SET views = views+1 WHERE id=?
参考了多个社区博客的设计,因为并不十分清楚其后端实现过程,只能从前端得出以下结论。
慕课网手记:无论是用户登录模式还是用户状态,每次刷新页面浏览数都会 +1。
51CTO博客:无论是用户登录模式还是用户状态,每次刷新页面浏览数都会 +1。
简书:用户登录模式下,无论如何刷新浏览数都不会新增,但是游客状态下每次刷新浏览数都会+1。
博客园:无论是用户登录模式还是用户状态,每次刷新页面浏览数都不变,即使隔天访问,也不变,没细测。
微信公众号:只能是用户登录状态,每次刷新浏览数基本不变,有时候会出现由多变少的情况,不知道大家有没有发现。

CSDN博客:无论是用户登录模式还是用户状态,每次刷新页面浏览数都不变,但是隔天访问,浏览数会+1,没细测。


基于以上社区的数据,直接 Pass 掉前两位,总结了以下几种方案,都是基于缓存标识实现。
1.如果游客或者登录用户访问,按照 IP + 文章 ID 维度增加浏览数,那局域网中怎么算?
2.如果是游客访问,按照 IP + 浏览器SessionId + 文章 ID 维度增加浏览数,可能解决局域网问题,那么关闭浏览器,重新打开又怎么算?
3.如果是登录用户,用户ID + 文章 ID 维度增加浏览数,那么游客在登录后算不算一个浏览数,或者是用户换个 IP 登录算不算 ?
所以说,怎么算都不准确,浏览数本身就是一个不需要太精确的功能,不要想太多,直接使用 IP + 文章ID 维度即可。

方案
方案一
得到 GET 请求,在限流之后,缓存之前,判断缓存中是否存在 IP+ 文章ID是否存在 Key。

如果存在,说明之前浏览过,就什么也不做。如果没有,就加上这个 Key,根据业务设置缓存失效时间,然后更新数据库浏览量+1,下面是代码实现:
//堆代码 duidaima.com
//获取 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即可:
/**
 * 计数
 * @param key
 * @param value
 */
public void add(String key, Object... value) {
    redisTemplate.opsForHyperLogLog().add(key,valu);
}
/**
  * 获取总数
  * @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;
}
博文请求:
/**
  * 博文
  */
@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";
}
业务代码:
/**
  * 执行顺序
  * 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("计数落库结束");
}

小结
文章热度虽然是一个很小的功能,但是对于一些高并发访问量的网站而言,其设计就不能简单使用udpate数据库来实现了。如果你们有更好的解决方案,欢迎在评论区留言交流。
用户评论