短链接监控
统计短链接PV访问
mybatis中@Param的作用:
@Param的作用就是给参数命名,比如在mapper里面某方法A(int id),当添加注解后A(@Param("userId") int id),也就是说外部想要取出传入的id值,只需要取它的参数名userId就可以了。将参数值传如SQL语句中,通过#{userId}进行取值给SQL的参数赋值。
编写插入语句时,一直不知道是不是sql语句哪里错了,一直报错null,然后直接就去gitee复制的,
/**
* 记录基础访问监控数据
*/
@Insert("INSERT INTO " +
"t_link_access_stats (full_short_url, date, pv, uv, uip, hour, weekday, create_time, update_time, del_flag) " +
"VALUES( #{linkAccessStats.fullShortUrl}, #{linkAccessStats.date}, #{linkAccessStats.pv}, #{linkAccessStats.uv}, #{linkAccessStats.uip}, #{linkAccessStats.hour}, #{linkAccessStats.weekday}, NOW(), NOW(), 0) " +
"ON DUPLICATE KEY UPDATE pv = pv + #{linkAccessStats.pv}, uv = uv + #{linkAccessStats.uv}, uip = uip + #{linkAccessStats.uip};")
void shortLinkStats(@Param("linkAccessStats") LinkAccessStatsDO linkAccessStatsDO);
结果这个代码就会出现一个问题,唯一索引存在时,不会更新,而是直接插入,检查后发现是因为没有加入gid的值,更改后代码为:
/**
* 记录基础访问监控数据
*/
@Insert("INSERT INTO " +
"t_link_access_stats (gid,full_short_url, date, pv, uv, uip, hour, weekday, create_time, update_time, del_flag) " +
"VALUES(#{linkAccessStats.gid},#{linkAccessStats.fullShortUrl}, #{linkAccessStats.date}, #{linkAccessStats.pv}, #{linkAccessStats.uv}, #{linkAccessStats.uip}, #{linkAccessStats.hour}, #{linkAccessStats.weekday}, NOW(), NOW(), 0) " +
"ON DUPLICATE KEY UPDATE pv = pv + #{linkAccessStats.pv}, uv = uv + #{linkAccessStats.uv}, uip = uip + #{linkAccessStats.uip};")
void shortLinkStats(@Param("linkAccessStats") LinkAccessStatsDO linkAccessStatsDO);
统计短链接UV访问
根据cookie判断是否是同一个用户
AtomicBoolean:
uvFirstFlag是一个AtomicBoolean类型的变量,它是线程安全的。AtomicBoolean提供了原子操作,可以确保多个线程之间的可见性和一致性。在addResponseCookieTask任务中,uvFirstFlag被设置为Boolean.TRUE,而在另一个线程中通过uvFirstFlag.get()来读取它的值。由于AtomicBoolean的特性,这个读取操作是安全的,不会受到其他线程的影响。因此,不会出现脏数据的问题。uvFirstFlag的值在多个线程之间是同步和可靠的。
Java并发基础:原子类之AtomicBoolean全面解析-优快云博客
解释:
Arrays.stream(cookies)
:将cookies
数组转换为一个流(Stream),以便进行链式操作。.filter(each->Objects.equals(each.getName(),"uv"))
:过滤流中的元素,只保留那些其getName()
方法返回"uv"的Cookie
对象。.findFirst()
:返回流中的第一个元素(如果有的话),这里指的是第一个名为"uv"的cookie。.map(Cookie::getValue)
:将找到的cookie对象映射为其值(即cookie的内容)。.ifPresentOrElse(each, {...}, addResponseCookieTask)
:这是一个Optional
类的方法,用于处理上一步map
操作的结果。如果Optional
包含值(即找到了名为"uv"的cookie),则执行第一个lambda表达式(将cookie的值添加到Redis集合中,并设置uvFirstFlag
标志);如果不包含值(即没有找到名为"uv"的cookie),则执行第二个参数addResponseCookieTask
,这是一个Runnable
,用于添加一个新的"uv" cookie到HTTP响应中。
如果cookies
数组为空,则直接执行addResponseCookieTask.run()
,即添加一个新的"uv" cookie到HTTP响应中。
AtomicBoolean uvFirstFlag=new AtomicBoolean();
Cookie[] cookies = ((HttpServletRequest) request).getCookies();
try {
Runnable addRespronseCookieTask=()->{
String uv = UUID.fastUUID().toString();
Cookie uvCookie = new Cookie("uv", uv);
uvCookie.setMaxAge(60*60*24*30);
//得到后缀
uvCookie.setPath(StrUtil.sub(fullShortUrl,fullShortUrl.indexOf("/"),fullShortUrl.length()));
((HttpServletResponse) response).addCookie(uvCookie);
uvFirstFlag.set(Boolean.TRUE);
stringRedisTemplate.opsForSet().add("short-link:stats:uv:"+fullShortUrl,uv);
};
if(ArrayUtil.isNotEmpty(cookies)){
Arrays.stream(cookies)
.filter(each->Objects.equals(each.getName(),"uv"))
.findFirst()
.map(Cookie::getValue)
.ifPresentOrElse(each->{
Long added=stringRedisTemplate.opsForSet().add("short-link:stats:uv:"+fullShortUrl,each);
uvFirstFlag.set(added!=null&&added>0L);
},addRespronseCookieTask);
}else{
addRespronseCookieTask.run();
}
...
}catch (Throwable ex){
log.error("短链接访问量统计异常",ex);
}
统计短链接IP访问
String remoteAddr=LinkUtil.getIp((HttpServletRequest) request);
Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
//uipAdded 的返回值是一个 Long 类型的值,表示成功添加到集合中的新元素的数量,而 uipFirstFlag 则用于判断 remoteAddr 是否是首次被添加到集合中。
boolean uipFirstFlag=uipAdded!=null&&uipAdded>0L;
/**
* 获取请求的 IP 地址
* @param request
* @return
*/
public static String getIp(HttpServletRequest request) {
String ipAddress = request.getHeader("X-Forwarded-For");
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
return ipAddress;
}
统计短链接地区访问
- 支持国内外IP地址
- 但是使用API限制较多
IP定位-API文档-开发指南-Web服务 API | 高德地图API
- 仅支持国内IP地址
- 使用API限制宽松
最终决定使用高德 IP API 定位接口。
@Value
:
@Value
是 Spring 框架中的一个非常有用的注解,它主要用于将配置文件中的值注入到 Spring 管理的 Bean 的字段中。这个注解通常与 Spring 的表达式语言(Spring Expression Language, SpEL)一起使用,来访问配置文件中的值、环境变量、系统属性等。
统计短链接操作系统访问
LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
.os(LinkUtil.getOs((HttpServletRequest) request))
.cnt(1)
.fullShortUrl(fullShortUrl)
.gid(gid)
.date(new Date())
.build();
linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
/**
* 获取用户访问操作系统
*
* @param request 请求
* @return 访问操作系统
*/
public static String getOs(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
if (userAgent.toLowerCase().contains("windows")) {
return "Windows";
} else if (userAgent.toLowerCase().contains("mac")) {
return "Mac OS";
} else if (userAgent.toLowerCase().contains("linux")) {
return "Linux";
} else if (userAgent.toLowerCase().contains("unix")) {
return "Unix";
} else if (userAgent.toLowerCase().contains("android")) {
return "Android";
} else if (userAgent.toLowerCase().contains("iphone")) {
return "iOS";
} else {
return "Unknown";
}
}
统计短链接浏览器访问
同上