hexo icarus、golang、google analytics自行实现访问量统计(一)

前言

此前摸索了一波Icarus的jsx模板,也了解了golang如何使用google analytics reporting api。现在打算结合起来自行实现访问量的统计,特此记录一下折腾的过程。
目前是打算用golang实现一个轻量的服务,服务启动时会拉取analytics的统计数据,并缓存在内存中,可以根据pathname获取访问量数据。数据更新的话,打算维护两个变量,定时扫描最新访问时间与最新数据更新时间,当新访问时间大于最新数据更新时间,且大于一个时间间隔更新,或着在这种情况下距离当前时间太远更新。
前端的话,只需要在页面加载完成后请求后端,并将获取到的值通过dom操作更新页面。

后端coding

缓存结构

首先定义了内存缓存结构体,PvMap是以页面path为key pv为value的map,到时候具体页面的访问量查询就是靠他了,TotalUv与TotalPv就是字面意思了。因为是运行在64位机上,所以int也够用了。

1
2
3
4
5
6
type Data struct {
    PvMap         map[string]int
    TotalUv       int
    TotalPv       int
}

获取数据并写入缓存

由于请求的内容是固定的,所以我把GetReportsRequest在服务启动时就生成了,此外更新操作是单线程的复用一个ReportsService也够了。
请求数据的时候如果发生错误,忽略即可,毕竟访问量并不是啥重要的数据。
我构造的GetReportsRequest的第一个度量是ga:newUsers、第二个是ga:pageViews,所以我就直接这样硬编码获取数据了。具体的返回内容在上上一篇博文中有提到,并且序列化地打印了出来,可以参考一下。

构造完缓存数据后,指针直接指向新缓存,并刷新更新时间。即使在并发的情况下取到旧的缓存,golang的gc也不会销毁还有引用的对象,因此这样也不用加锁,还是前面那句话,访问量并不是啥重要的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (s *analyticsReportService) getReports() {
    response, err := s.reportsService.BatchGet(s.request).Do()
    if err != nil {
        return
    }
    if len(response.Reports) == 0 {
        return
    }
    report := response.Reports[0]
    data := &Data{
        PvMap: make(map[string]int, report.Data.RowCount),
    }
    data.TotalUv, _ = strconv.Atoi(report.Data.Totals[0].Values[0])
    data.TotalPv, _ = strconv.Atoi(report.Data.Totals[0].Values[1])
    for _, item := range report.Data.Rows {
        data.PvMap[item.Dimensions[0]], _ = strconv.Atoi(item.Metrics[0].Values[1])
    }
    s.data = data
    s.lastUpdateTime = time.Now().Unix()
    log.Print("updated data")
}

定时更新缓存

这就是根据维护的两个时间变量调用上面的函数了,tick时间略小于更新时间。

1
2
3
4
5
6
7
8
9
10
11
func (s *analyticsReportService) start() {
    go func() {
        for range time.Tick(5 * time.Second) {
            if s.lastVisitTime > s.lastUpdateTime {
                if s.lastVisitTime - s.lastUpdateTime > 10 || s.lastUpdateTime + 10 < time.Now().Unix() {
                    s.getReports()
                }
            }
        }
    }()
}

构建http服务

由于golang自带了http服务,这里直接用原生的就够用了。
实现handler,当path为/时,返回的是total的数值。浏览器window.location.pathname获取到的值是urlencode过的,不确定不同的环境下浏览器decode会不会有什么神奇的bug,所以decode就放在后端做了。
每次访问过后,顺带更新一下访问时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func Handler(w http.ResponseWriter, r *http.Request) {
    allBytes, err := ioutil.ReadAll(r.Body)
    if err != nil {
        return
    }
    req := Req{}
    err = json.Unmarshal(allBytes, &req)
    if err != nil {
        return
    }
    path, _ := url.QueryUnescape(req.Path)
    uv := 0
    pv := 0
    if path == "/" {
        uv = service.data.TotalUv
        pv = service.data.TotalPv
    } else {
        pv = service.data.PvMap[path]
    }
    bytes, _ := json.Marshal(map[string]int{
        "uv": uv,
        "pv": pv,
    })
    w.Write(bytes)
    service.lastVisitTime = time.Now().Unix()
}

postman测试后端

测试根路径可以看到返回了总pv与uv。

具体的文章路径返回了pv,目前没做具体页面的uv信息。

内存占用

可以看到本服务在加载了内存缓存后,内存占用也只有个位数,非常轻量。

小结

目前实现了访问量统计的后端部分,数据全部缓存在内存中,实现上无锁,响应快速。
间歇更新也不会超出analytics reporting api一天5万次的请求上限与10qps/ip。
从wordpress迁移到hexo后,本人的主机上就不运行php cgi了,相比node.js或者php需要单独的运行环境,个位数的内存占用也对主机非常友好。

作者

ZhongHuihong

发布于

2021-10-04

更新于

2021-10-04

许可协议