目录
一、实现目标
统计网站的日访问量与日活量,日访问量是每天页面被访问的次数,日活量也是被访问的次数,但重复的ip不作记录。
二、windows版本redis下载与安装
windows版本的redis安装配置很简单,这里给一个安装过程的参考链接。
https://blog.csdn.net/junge1545/article/details/80842787
三、springboot集成redis
集成redis所需的依赖为
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>1.4.1.RELEASE</version>
</dependency>
引入该依赖,同时需要修改springboot 的配置文件application.yml
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
timeout: 2000
jedis:
pool:
max-idle: 8
max-wait:
min-idle: 0
max-active: 8
这里host要redis的地址,我们是在本地安装的,所以填127.0.0.1即可,端口默认为6379,密码默认为空。
之后我们创建一个RedisUtils工具类,目的是将redis的一些基础操作进行封装,可以更简单方便的实现,redis中的一些操作。
这里我们贴出一个比较完整的RedisUtils,里面包含了redis的大部分常见操作。
package com.quan.redistest.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public Set<String> keys(String keys) {
try {
return redisTemplate.keys(keys);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
如果还需要对redis进行一些配置,可以新建一个RedisConfig,通过Bean注入RedisTemplate。
这里我们配置一下RedisTemplate的序列化:
未配置的话无法使用incr等操作,输入的数字会被解析为字符串
package com.quan.redistest.Config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
/**
* 设置 redisTemplate 的序列化设置
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建 redisTemplate 模版
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 2.关联 redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
// 3.创建 序列化类
GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
// 6.序列化类,对象映射设置
// 7.设置 value 的转化格式和 key 的转化格式
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
四、springboot集成mybatis
相关依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.30</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
集成通用mapper
这里是三个基础依赖,如果想要使用通用Mapper中的api,可以再引入tk.mybatis依赖:
<!-- 通用Mapper -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
引入这个依赖之后,让Mapper继承相应的类即可,如下图所示。
public interface MkyAccessLogMapper extends Mapper<MkyAccessLog> {
}
这里注意Mapper的泛型要是实体类,同时实体类要按规范来写。
需要加@Entity
与@Table
标签,table中的name要填数据库中表名。每一个字段都要加@Column(name="create_datetime")
注解,name是字段名,同时主键需要添加@Id
注解。
package com.quan.redistest.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "mky_access_log")
public class MkyAccessLog implements java.io.Serializable {
private static final long serialVersionUID = -1943961352036134112L;
@Id
@Column(name="id")
private String id;
@Column(name="count")
private String count;
@Column(name="create_datetime")
private String createDatetime;
@Column(name="type")
private String type;
@Column(name="del_flag")
private String delFlag;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCount() {
return count;
}
public void setCount(String count) {
this.count = count;
}
public String getCreateDatetime() {
return createDatetime;
}
public void setCreateDatetime(String createDatetime) {
this.createDatetime = createDatetime;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getDelFlag() {
return delFlag;
}
public void setDelFlag(String delFlag) {
this.delFlag = delFlag;
}
}
这么一套操作我们可以实现什么功能呢?
我们可以对mapper类调一些api,这样一些基础的增删改查操作就不必去xml里写sql语句了。如:
MkyAccessLog mal = new MkyAccessLog();
IdWorker iw = new IdWorker();
String id = String.valueOf(iw.nextId());
mal.setId(id);
mal.setCount(num+"");
mal.setType("1");
mal.setDelFlag("0");
//保存当天访问量
mkyAccessLogMapper.insert(mal);
可以很容易的实现插入操作,还有一些如selectByPrimaryKey(根据主键进行查询)等也非常的好用。
之后我们继续来说springboot集成mybatis,我们需要在application.yml中添加配置:
spring:
datasource:
url: jdbc:mysql://xxx:3306/zfc_test?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
mybatis:
mapperLocations: classpath:mapper/*Mapper.xml
这里xxx要填你数据库的地址,如果是本机就是127.0.0.1,username填写用户名,password填写数据库密码。
mybatis的相应配置,是设置xml文件所在的位置。
同时我们需要在springboot的启动类上加mapper的扫描注解:
@MapperScan("com.quan.redistest.Mapper")
注意,如果配置了上面说的tk.mybatis通用mapper,这里依赖需要引入:
import tk.mybatis.spring.annotation.MapperScan;
未配置的话正常引入依赖即可。
在这个项目中我们还集成了org.mybatis.generator来实现mybatis逆向工程,就是通过数据库中的表来逆向生成entity实体类,mapper接口,和xml文件。就不展开细说了,感兴趣的同学可以百度一下。
五、实现日访问量
首先我们搞一个web页面,引入相应的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
Controller:
@Controller
@RequestMapping("portal")
public class PortalController {
@RequestMapping("index")
public String index(){
return "portal/index";
}
}
index.html页面:
放在templates目录下
页面简单填充一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
</body>
<script language="JavaScript" src="../../js/jquery1.11.1.min.js"></script>
<script type="text/javascript">
$.ajax({
method: 'get',
url: '/test/count',
contentType: false,
processData: false,
success: function(res){
console.log(res);
}
});
</script>
</html>
这里可以看到下面调了一个ajax,每访问一次页面,都会进行一次计数。
接下来就来看一下这个计数接口吧:
Controller层:
@RestController
@RequestMapping("test")
public class TestRestController {
@Autowired
private TestService testService;
@RequestMapping("count")
public String saveUserActivity(){
String str = testService.saveUserActivity();
return str;
}
}
Service层:
public String saveUserActivity(){
//访问量
String userAccount = (String)redisUtils.get("userAccount");
if(userAccount==null){
redisUtils.set("userAccount","1");
}else{
redisUtils.incr("userAccount",1);
}
return (String)redisUtils.get("userAccount");
}
这里的逻辑非常简单,我们做的操作是:拿到Redis中的userAccount,如果为空就赋值为1,否则的话进行自增。
@EnableScheduling定时任务
因为我们要统计的是每日的访问量,所以这里我们用到了定时任务。
@EnableScheduling是spring自带的定时任务,所以我们不需要引入额外的依赖。
想要使用定时任务,首先我们要再springbootApplication启动类上加注解:@EnableScheduling
;之后要在相应的service类上加注解:@EnableScheduling
;最后要在方法上加注解:@Scheduled(cron = "0 0 0 * * ?")
。
这样方法就会按我们规定的定时时间来执行了,具体的话通过cron表达式来实现,上面这个表达式的意思是每天0点执行,即我们在每天的0点来将日访问量入库,然后将redis中的日访问量清空。
具体代码如下:
@Scheduled(cron = "0 0 0 * * ?")
//@Scheduled(cron = "*/5 * * * * ?")
public void saveUserAccessLog(){
//获取redis中的userAccount
String userAccount = (String)redisUtils.get("userAccount");
int num = Integer.parseInt(userAccount);
MkyAccessLog mal = new MkyAccessLog();
IdWorker iw = new IdWorker();
String id = String.valueOf(iw.nextId());
mal.setId(id);
mal.setCount(num+"");
Calendar calendar= Calendar.getInstance();
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy-MM-dd :hh:mm:ss");
String createDatetime = dateFormat.format(calendar.getTime());
mal.setCreateDatetime(createDatetime);
mal.setType("1");
mal.setDelFlag("0");
//保存当天访问量
mkyAccessLogMapper.insert(mal);
//将redis中的访问量初始化
redisUtils.set("userAccount","0");
}
整体思路也很简单,就是拿到Redis中的userAccount,然后保存到数据库中的mky_access_log表中,最后再把Redis中的userAccount值初始化即可。
六、实现日活量
日活量和日访问量的区别就在于重复的ip不计数,这样的话我们可以直接使用redis中的set数据结构,在里面存入ip地址,这样统计的时候我们直接获取它的size就可以了。
获取IP地址
首先我们要解决的是如何获取用户的真实IP地址。
这里我们借助一个工具类:
public class IPUtil {
/**
* 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址。
* 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
* 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)){
//根据网卡取本机配置的IP
InetAddress inet=null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ip= inet.getHostAddress();
}
}
return ip;
}
}
传入的参数HttpServletRequest
我们从controller直接获取,这里我们简单看一下这个工具类,核心思想就是从X-Forwarded-For
里获取真实ip,如果没有再去其他的请求头中获取。这里简单说一下X-Forwarded-For
:
X-Forwarded-For是一个拓展头,用来获取真实ip,格式很简单:
X-Forwarded-For:client, proxy1, proxy2
最前面的就是真实ip,之后是各个代理服务器,用逗号分割。
之后我们继续来看日活量的代码实现:
//日活量
String ip = IPUtil.getIpAddress(request);
redisUtils.sSet("userLiveCount",ip);
我们把获取到的ip存入set集合中即可。
接下来我们看定时任务中的实现:
//日活量
long len = redisUtils.sGetSetSize("userLiveCount");
MkyAccessLog mal1 = new MkyAccessLog();
IdWorker iw1 = new IdWorker();
String id1 = String.valueOf(iw1.nextId());
mal1.setId(id1);
mal1.setCount(len+"");
mal1.setCreateDatetime(createDatetime);
mal1.setType("2");
mal1.setDelFlag("0");
//保存当天日活量
mkyAccessLogMapper.insert(mal1);
//设置缓存时间为1秒,变向清除set
redisUtils.expire("userLiveCount",1);
保存当天的日活量数据,然后清除set。
七、项目github地址
https://github.com/KD-oauth/redis_test