180713-Spring之借助Redis设计访问计数器之扩展篇

之前写了一篇博文,简单的介绍了下如何利用Redis配合Spring搭建一个web的访问计数器,之前的内容比较初级,现在考虑对其进行扩展,新增访问者记录
- 记录当前站点的总访问人数(根据Ip或则设备号)
- 记录当前访问者在总访问人数中的排名
- 记录每个子页面的访问计数,记录站点的总访问计数
推荐博文:
I. 数据结构设计
首先根据上面的几个数据维度进行划分,首先每个站点有自己独立的数据结构,其中访问者记录和每个页面对应的访问计数,肯定是不一样的,下面分别进行说明
1. 访问记录
要求记录每个访问者的IP或者设备号,以此来计算总得访问人数,以及当前的访问者在总得访问人数中的位置
List数据结构是否可行?
- 每次新来一个访问者,需要与所有的访问者进行对比,判断是否是新的访问者,是则插入列表;不是则查出其对应的位置
如果对redis的数据结构有一点了解,会直到有一个ZSet(有序的集合)正好适合这种场景
- 确保不会插入重复的数据,每个数据对应的score就是该访问者的首次访问排序
具体的结构类似
-- ip (score)
127.0.0.1 (1)
127.0.0.2 (2)
127.0.0.3 (3)
...
2. url计数
依然沿用之前的Hash数据结构,每个应用申请一个APPKEY,作为hash结构的Key,然后field则为具体的请求域名
具体的结构类似
appKey: // appKey
blog.hhui.top: 1314 // 站点对应的总访问数
blog.hhui.top/index: 1303 // 具体的页面对应的访问数
blog.hhui.top/about: 11 // 具体的页面对应的访问数
appKey:
blog.hhui.top: 1314
blog.hhui.top/index: 1303
blog.hhui.top/about: 11
II. 实现
具体的实现其实没有什么特别需要注意的地方,简单说一下几个关键点,一个是Redis的Hash和Zset两个数据结构的访问修改方法;一个则是如何获取访问者的IP
1. 获取客户端IP
在Spring中如何获取客户端IP呢?因为我个人的服务器是走的Nginx进行反向代理,所以需要在Nginx层添加一行配置,避免将客户端IP吃掉了
在nginx.con的配置中,转发的地方添加下面的一行
location / {
proxy_set_header X-real-ip $remote_addr;
}
然后就可以在代码层,通过解析HttpServletRequest参数,获取真实IP,这段代码网上比较多,直接拿来使用(我这里是放在了一个Filter层,在这里获取服务端关心的一些参数,供整个请求链路使用)
获取客户端IP方法
/**
* 获取Ip地址
* @param request
* @return
*/
private static String getIpAdrress(HttpServletRequest request) {
String Xip = request.getHeader("X-Real-IP");
String XFor = request.getHeader("X-Forwarded-For");
if(StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)){
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = XFor.indexOf(",");
if(index != -1){
return XFor.substring(0,index);
}else{
return XFor;
}
}
XFor = Xip;
if(StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)){
return XFor;
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getRemoteAddr();
}
return XFor;
}
2. Redis操作
接下来就是redis数据结果的操作了,关于Spring中如何配置和简单使用RedisTemplate可以参考 《180611-Spring之RedisTemplate配置与使用》
下面简单贴一下核心的Redis操作代码, 关于Hash的访问就没啥好说的,参考上一篇博文即可
/**
* 获取redis中指定value的score
*
* @param key 唯一key
* @param value 存在redis中的实际值(计数组件中value即为客户端IP)
* @return
*/
public static Long zScore(String key, String value) {
return template.execute((RedisCallback<Long>) con -> {
Double ans = con.zScore(toBytes(key), toBytes(value));
return ans == null ? 0 : ans.longValue();
});
}
/**
* 表示新增一条记录
*
* @param key
* @param value 对应客户端ip
* @param score 对应客户端访问的排名
* @return 当set中没有记录时,返回true;否则返回false
*/
public static Boolean zAdd(String key, String value, long score) {
return template.execute((RedisCallback<Boolean>) con -> con.zAdd(toBytes(key), score, toBytes(value)));
}
/**
* 获取zset中最大的score,即在计数组件中,这个值就是总得访问人数
* @param key
* @return
*/
public static Long zMaxScore(String key) {
return template.execute((RedisCallback<Long>) con -> {
Set<RedisZSetCommands.Tuple> set = con.zRangeWithScores(toBytes(key), -1, -1);
if (CollectionUtils.isEmpty(set)) {
return 0L;
}
Double score = set.stream().findFirst().get().getScore();
return score.longValue();
});
}
主要的redis操作是上面三个方法,那么怎么调用的呢?直接看下面的逻辑即可,比较清晰
- 获取站点的总访问人数
- 尝试获取访问者的排名
- 如果没有获取到排名,表示首次访问,则需要新插入一条记录
- 获取到排名,则直接返回
public CountDTO visit(String appKey, String url) {
String visitKey = visitKey(appKey);
// 首先是获取站点的总访问人数
long visitTotalNum = QuickRedisClient.zMaxScore(visitKey);
// 获取访问者在总访问人数中的排名,如果为0,表示该用户没有访问过
long visitIndex = QuickRedisClient.zScore(visitKey, ReqInfoContext.getReqInfo().getClientIp());
if (visitIndex == 0) {
// 不存在(即用户没有访问过),则需要添加一条访问记录
visitTotalNum += 1;
visitIndex = visitTotalNum;
QuickRedisClient.zAdd(visitKey, ReqInfoContext.getReqInfo().getClientIp(), visitIndex);
}
// 构建DO对象
}
看到上面这一段逻辑的实现,如果一点疑问都没有,那我不得不怀疑是否真的看了这篇博文了,或者说就是单纯的看了而已,却没有一点的收货
重点说明,上面的实现有并发问题、并发问题、并发问题,重要的事情说三遍,至于为什么以及该如何解决,欢迎讨论
一个实际使用这个计数器的case,就是个人的博客网站了,欢迎点击查看:
- 小灰灰blog: https://blog.hhui.top/
- 小灰灰blog: https://liuyueyi.github.io/hexblog/

III. 其他
0. 相关博文
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明
尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840
3. 扫描关注

180713-Spring之借助Redis设计访问计数器之扩展篇的更多相关文章
- 180626-Spring之借助Redis设计一个简单访问计数器
文章链接:https://liuyueyi.github.io/hexblog/2018/06/26/180626-Spring之借助Redis设计一个简单访问计数器/ Spring之借助Redis设 ...
- 探索Redis设计与实现9:数据库redisDb与键过期删除策略
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 探索Redis设计与实现15:Redis分布式锁进化史
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 探索Redis设计与实现14:Redis事务浅析与ACID特性介绍
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 探索Redis设计与实现13:Redis集群机制及一个Redis架构演进实例
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 探索Redis设计与实现8:连接底层与表面的数据结构robj
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 探索Redis设计与实现6:Redis内部数据结构详解——skiplist
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 探索Redis设计与实现5:Redis内部数据结构详解——quicklist
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 探索Redis设计与实现4:Redis内部数据结构详解——ziplist
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
随机推荐
- Sublime EMMET使用技巧
1.使用tab键生成标签: 2.使用+创建兄弟元素,使用>创建子元素,使用+号还可以创建元素基本结构: 3.使用*号后面跟数字可以重复创建相同元素: 4.使用^返回上一级元素: 5.使用()进行 ...
- 【Linux资源管理】一款优秀的linux监控工具——nmon
(一)nmon工具概述 nmon是以一个用来做linux服务器监控的工具,通过nmon,可以实现对以下参数的监控: --CPU使用率 --内存.交换空间使用率 --网络使用情况 --磁盘I/O,读写速 ...
- percona数据库监控工具的安装部署
Percona Monitoring and Management 安装 PMM是一个开源,免费的mysql管理监控平台,他可以用来分析mysql,mariadb和mongodb的服务器性能. 安装步 ...
- mqtt使用二(集成到java代码中)
1.我采用的是springboot,首先pom文件中添加mqtt需要用到的依赖 <dependency> <groupId>org.springframework.boot&l ...
- php http_build_query stream_context_create post请求
<?php function send_post($url, $post_data) { $postdata = http_build_query($post_data); $options = ...
- yii学习笔记(6),连接数据库,创建活动记录类
创建数据库用于测试 配置数据库连接 打开yii的配置文件目录下的数据库配置文件config/db.php <?php return [ 'class' => 'yii\db\Connect ...
- pyhton实现简单的木马程序
十一的晚上,平时都在写工作的代码,好久没有专门看一些知识了,感觉想刚开始学c一样,搞到半夜 还是<python网络编程基础>,写了小脚本,没有任何结构,一句一句的往下写的,反正是可以实现想 ...
- SST-超级简单任务调度器结构分析
SST(Super Simple Task) 是一个基于任务优先级.抢占式.事件驱动.RTC.单堆栈的超级简单任务调度器,它基于Rober Ward一篇论文的思想,Miro Samek用C重新编程实现 ...
- centos升级数据库
Centos下升级MySQL数据库 备份数据 $ mysqldump -u xxx -h xxx -P 3306 -p --all-databases > databases.sql 查看版本 ...
- 清华大学《C++语言程序设计进阶》线上课程笔记06---继承、派生、多态性
类的继承 保持已有类的特性而构造新类的过程称为继承; 实现设计与代码的重用. 在已有类的基础上新增自己的特性而产生新类的过程称为派生 当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程 ...