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


    logo

    之前写了一篇博文,简单的介绍了下如何利用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,就是个人的博客网站了,欢迎点击查看:

    showcase

    III. 其他

    0. 相关博文

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

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

    2. 声明

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

    3. 扫描关注

    QrCode

  • 相关阅读:
    解决:oracle+myBatis ResultMap 类型为 map 时,表字段类型有 Long/Blob/Clob 时报错
    总结:独立开发 jar 包组件——功能主要是支持查询数据库的所有表数据
    解决 iframe 后退不是主页面后退(浏览器 history)问题
    解决访问 jar 包里面的字体报错:OTS parsing error: incorrect file size in WOFF header
    html 如何访问 jar 包里面的静态资源(js、css、字体等)
    css3 实现打字机效果
    js 图形验证码
    input 设置 flex:1不起作用
    vue 样式加scoped不起作用
    node-mongoose开发中常见警告或问题-持续更新
  • 原文地址:https://www.cnblogs.com/yihuihui/p/9307884.html
一二三 - 开发者的网上家园