之前写了一篇博文,简单的介绍了下如何利用Redis配合Spring搭建一个web的访问计数器,之前的内容比较初级,现在考虑对其进行扩展,新增访问者记录

  • 记录当前站点的总访问人数(根据Ip或则设备号)
  • 记录当前访问者在总访问人数中的排名
  • 记录每个子页面的访问计数,记录站点的总访问计数

推荐博文:

I. 数据结构设计

首先根据上面的几个数据维度进行划分,首先每个站点有自己独立的数据结构,其中访问者记录和每个页面对应的访问计数,肯定是不一样的,下面分别进行说明

1. 访问记录

要求记录每个访问者的IP或者设备号,以此来计算总得访问人数,以及当前的访问者在总得访问人数中的位置

List数据结构是否可行?

  • 每次新来一个访问者,需要与所有的访问者进行对比,判断是否是新的访问者,是则插入列表;不是则查出其对应的位置

如果对redis的数据结构有一点了解,会直到有一个ZSet(有序的集合)正好适合这种场景

  • 确保不会插入重复的数据,每个数据对应的score就是该访问者的首次访问排序

具体的结构类似

  1. -- ip (score)
  2. 127.0.0.1 (1)
  3. 127.0.0.2 (2)
  4. 127.0.0.3 (3)
  5. ...

2. url计数

依然沿用之前的Hash数据结构,每个应用申请一个APPKEY,作为hash结构的Key,然后field则为具体的请求域名

具体的结构类似

  1. appKey: // appKey
  2. blog.hhui.top: 1314 // 站点对应的总访问数
  3. blog.hhui.top/index: 1303 // 具体的页面对应的访问数
  4. blog.hhui.top/about: 11 // 具体的页面对应的访问数
  5. appKey:
  6. blog.hhui.top: 1314
  7. blog.hhui.top/index: 1303
  8. blog.hhui.top/about: 11

II. 实现

具体的实现其实没有什么特别需要注意的地方,简单说一下几个关键点,一个是Redis的Hash和Zset两个数据结构的访问修改方法;一个则是如何获取访问者的IP

1. 获取客户端IP

在Spring中如何获取客户端IP呢?因为我个人的服务器是走的Nginx进行反向代理,所以需要在Nginx层添加一行配置,避免将客户端IP吃掉了

在nginx.con的配置中,转发的地方添加下面的一行

  1. location / {
  2. proxy_set_header X-real-ip $remote_addr;
  3. }

然后就可以在代码层,通过解析HttpServletRequest参数,获取真实IP,这段代码网上比较多,直接拿来使用(我这里是放在了一个Filter层,在这里获取服务端关心的一些参数,供整个请求链路使用)

获取客户端IP方法

  1. /**
  2. * 获取Ip地址
  3. * @param request
  4. * @return
  5. */
  6. private static String getIpAdrress(HttpServletRequest request) {
  7. String Xip = request.getHeader("X-Real-IP");
  8. String XFor = request.getHeader("X-Forwarded-For");
  9. if(StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)){
  10. //多次反向代理后会有多个ip值,第一个ip才是真实ip
  11. int index = XFor.indexOf(",");
  12. if(index != -1){
  13. return XFor.substring(0,index);
  14. }else{
  15. return XFor;
  16. }
  17. }
  18. XFor = Xip;
  19. if(StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)){
  20. return XFor;
  21. }
  22. if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
  23. XFor = request.getHeader("Proxy-Client-IP");
  24. }
  25. if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
  26. XFor = request.getHeader("WL-Proxy-Client-IP");
  27. }
  28. if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
  29. XFor = request.getHeader("HTTP_CLIENT_IP");
  30. }
  31. if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
  32. XFor = request.getHeader("HTTP_X_FORWARDED_FOR");
  33. }
  34. if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
  35. XFor = request.getRemoteAddr();
  36. }
  37. return XFor;
  38. }

2. Redis操作

接下来就是redis数据结果的操作了,关于Spring中如何配置和简单使用RedisTemplate可以参考 《180611-Spring之RedisTemplate配置与使用》

下面简单贴一下核心的Redis操作代码, 关于Hash的访问就没啥好说的,参考上一篇博文即可

  1. /**
  2. * 获取redis中指定value的score
  3. *
  4. * @param key 唯一key
  5. * @param value 存在redis中的实际值(计数组件中value即为客户端IP)
  6. * @return
  7. */
  8. public static Long zScore(String key, String value) {
  9. return template.execute((RedisCallback<Long>) con -> {
  10. Double ans = con.zScore(toBytes(key), toBytes(value));
  11. return ans == null ? 0 : ans.longValue();
  12. });
  13. }
  14. /**
  15. * 表示新增一条记录
  16. *
  17. * @param key
  18. * @param value 对应客户端ip
  19. * @param score 对应客户端访问的排名
  20. * @return 当set中没有记录时,返回true;否则返回false
  21. */
  22. public static Boolean zAdd(String key, String value, long score) {
  23. return template.execute((RedisCallback<Boolean>) con -> con.zAdd(toBytes(key), score, toBytes(value)));
  24. }
  25. /**
  26. * 获取zset中最大的score,即在计数组件中,这个值就是总得访问人数
  27. * @param key
  28. * @return
  29. */
  30. public static Long zMaxScore(String key) {
  31. return template.execute((RedisCallback<Long>) con -> {
  32. Set<RedisZSetCommands.Tuple> set = con.zRangeWithScores(toBytes(key), -1, -1);
  33. if (CollectionUtils.isEmpty(set)) {
  34. return 0L;
  35. }
  36. Double score = set.stream().findFirst().get().getScore();
  37. return score.longValue();
  38. });
  39. }

主要的redis操作是上面三个方法,那么怎么调用的呢?直接看下面的逻辑即可,比较清晰

  • 获取站点的总访问人数
  • 尝试获取访问者的排名
  • 如果没有获取到排名,表示首次访问,则需要新插入一条记录
  • 获取到排名,则直接返回
  1. public CountDTO visit(String appKey, String url) {
  2. String visitKey = visitKey(appKey);
  3. // 首先是获取站点的总访问人数
  4. long visitTotalNum = QuickRedisClient.zMaxScore(visitKey);
  5. // 获取访问者在总访问人数中的排名,如果为0,表示该用户没有访问过
  6. long visitIndex = QuickRedisClient.zScore(visitKey, ReqInfoContext.getReqInfo().getClientIp());
  7. if (visitIndex == 0) {
  8. // 不存在(即用户没有访问过),则需要添加一条访问记录
  9. visitTotalNum += 1;
  10. visitIndex = visitTotalNum;
  11. QuickRedisClient.zAdd(visitKey, ReqInfoContext.getReqInfo().getClientIp(), visitIndex);
  12. }
  13. // 构建DO对象
  14. }

看到上面这一段逻辑的实现,如果一点疑问都没有,那我不得不怀疑是否真的看了这篇博文了,或者说就是单纯的看了而已,却没有一点的收货

重点说明,上面的实现有并发问题、并发问题、并发问题,重要的事情说三遍,至于为什么以及该如何解决,欢迎讨论

一个实际使用这个计数器的case,就是个人的博客网站了,欢迎点击查看:

III. 其他

0. 相关博文

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

180713-Spring之借助Redis设计访问计数器之扩展篇的更多相关文章

  1. 180626-Spring之借助Redis设计一个简单访问计数器

    文章链接:https://liuyueyi.github.io/hexblog/2018/06/26/180626-Spring之借助Redis设计一个简单访问计数器/ Spring之借助Redis设 ...

  2. 探索Redis设计与实现9:数据库redisDb与键过期删除策略

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  3. 探索Redis设计与实现15:Redis分布式锁进化史

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  4. 探索Redis设计与实现14:Redis事务浅析与ACID特性介绍

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  5. 探索Redis设计与实现13:Redis集群机制及一个Redis架构演进实例

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  6. 探索Redis设计与实现8:连接底层与表面的数据结构robj

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  7. 探索Redis设计与实现6:Redis内部数据结构详解——skiplist

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  8. 探索Redis设计与实现5:Redis内部数据结构详解——quicklist

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  9. 探索Redis设计与实现4:Redis内部数据结构详解——ziplist

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

随机推荐

  1. 使用jsonp获取天气情况

    在这里使用的是百度天气: 整体代码如下: js: <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js ...

  2. 解决 Visual Studio 2017 打开项目提示项目不兼容

    这应该算是VS2017的一个bug,昨天做好的.net core项目还能好好如初,今天打开就提示项目不兼容,未能加载...... 解决办法也是超级简单,但是往往越简单的办法越是想不到: 右键解决方案, ...

  3. 十七、IntelliJ IDEA 中的 Maven 项目初体验及搭建 Spring MVC 框架

    我们已经将 IntelliJ IDEA 中的 Maven 项目的框架搭建完成.接着上文,在本文中,我们更近一步,利用 Tomcat 运行我们的 Web 项目. 如上图所示,我们进一步扩展了项目的结构, ...

  4. PAT——年会抽奖(错位 排序)

    题目描述 今年公司年会的奖品特别给力,但获奖的规矩却很奇葩: 1. 首先,所有人员都将一张写有自己名字的字条放入抽奖箱中:2. 待所有字条加入完毕,每人从箱中取一个字条:3. 如果抽到的字条上写的就是 ...

  5. P1018 乘积最大(高精度加/乘)

    P1018 乘积最大 一道dp题目.比较好像的dp题目. 然而他需要高精度计算. 所以,他从我开始学oi,到现在.一直是60分的状态. 今天正打算复习模板.也就有借口解决了这道题目. #include ...

  6. iOS数据存储类型 及 堆(heap)和栈(stack)

    iOS数据存储类型 及 堆(heap)和栈(stack) 一般认为在c中分为这几个存储区: 1栈 --  由编译器自动分配释放. 2堆 --  一般由程序员分配释放,若程序员不释放,程序结束时可能由O ...

  7. 100个常用的linux命令(转)

    来源:JavaRanger – javaranger.com   http://www.javaranger.com/archives/907 1,echo “aa” > test.txt 和 ...

  8. iOS:UICollectionView流式布局及其在该布局上的扩展的线式布局

    UICollectionViewFlowLayout是苹果公司做好的一种单元格布局方式,它约束item的排列规则是:从左到右依次排列,如果右边不够放下,就换一行重复上面的方式排放,,,,,   常用的 ...

  9. var let const的一些区别

    var let const 都是来定义变量的. var let 作用域有些区别. const 类似于java中的常量的概念.即:只能给一个变量赋值一次,即指定一个引用. 举例来说: function ...

  10. git 码云 使用记录

    使用了码云的私有仓库. 一.首先下载安装git 安装完成后,在开始菜单里找到“Git”->“Git Bash”,蹦出一个类似命令行窗口的东西,就说明Git安装成功! 二.创建版本库 什么是版本库 ...