业务需求
统计小程序的用户停留时长
不需要实时统计,所以按照天为维度
使用Redis的hash形式存并使用计数器累加时长,凌晨定时持久化前一天的数据到DB
注:一些其它统计也可以使用此种方式来
使用Redis实现的优点,速度快,减少数据库压力,使用计数器特性已经对数据做了累加。利用Redis有序集合可以达到分页处理的效果。
表设计
CREATE TABLE user_stand_info
(
id
BIGINT ( 20 ) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ‘ID’,
user_id
VARCHAR ( 64 ) NOT NULL COMMENT ‘用户id’,
stand_count_total
BIGINT ( 20 ) NOT NULL COMMENT ‘今日统计总时长(单位秒)’,
stand_count_date
BIGINT ( 20 ) NOT NULL COMMENT ‘统计日期’,
create_time
datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘新建时间’,
PRIMARY KEY ( id
),
UNIQUE KEY uniq_idx
( stand_count_date
, user_id
) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT = ‘用户小程序停留时长记录表’;
接口实现说明
上报用户停留接口入参
userId:人员id
Long standStart:停留开始时间戳
Long standEnd:停留结束时间戳
逻辑图示
Redis 存储实现
使用Hash形式存储,这里还用到了Hash形式的计数器
外层Key: 自定义前缀+yyyyMMdd
Hash内层Key:用户id
value:累加增量的时长毫秒数
每一天的访问用户均存储在这个Hash中,但是Hash缺点不能分页提取
为了防止数据量大一次获取所有hash造成redis锁死。影响其它功能
所以同时存储有序集合,以便做分页提取
2.同时存储有序集合 sorted set
使用命令 zadd
key:自定义固定字符串常量key
value:userId
排序值sore :默认0即可
注:使用有序集合而非集合的好处是
根据相同的value(这里就是userId),集合会执行更新。避免相同用户重复记录多条
接口代码
上报到Redis实现
/**
* 用户小程序停留时长记录接口
*/
public interface UserStandPutService {
/**
* 上报用户停留时间至 redis
*
* @param userId 人员id
* @param standStart 开始时间戳
* @param standEnd 结束时间戳
*/
void putUserStand(String userId, Long standStart, Long standEnd);
}
import com.boot.redis.constant.DemoConstant;
import com.boot.redis.jredis.RedisCache;
import com.boot.redis.service.UserStandPutService;
import com.boot.redis.util.DemoDateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* 用户小程序停留时长记录接口 实现
*/
@Service
public class UserStandPutServiceImpl implements UserStandPutService {
private Logger LOGGER = LoggerFactory.getLogger(UserStandPutServiceImpl.class);
@Autowired
private RedisCache redisCache;
/**
* 上报用户停留时间至 redis
*
* @param userId 人员id
* @param standStart 开始时间戳
* @param standEnd 结束时间戳
*/
@Override
public void putUserStand(String userId, Long standStart, Long standEnd) {
//处理时间
Date startDateTime = DemoDateUtil.parseDateTime(standStart);
Date endDateTime = DemoDateUtil.parseDateTime(standEnd);
//判断是否是同一天
boolean isSameDate = DemoDateUtil.isSameDate(startDateTime, endDateTime);
if (isSameDate) {
LOGGER.info("用户停留时间上报起止时间是同一天 userId={}", userId);
this.sameDateSave(startDateTime, endDateTime, userId);
} else {
this.doSaveNotSameDay(startDateTime, endDateTime, userId);
LOGGER.info("用户停留时间上报起止时间是不是同一天 userId={}", userId);
}
}
/**
* 时间在同一天
*
* @param startDateTime
* @param endDateTime
*/
private void sameDateSave(Date startDateTime, Date endDateTime, String userId) {
//使用截止时间格式化为 yyyyMMdd
//注:因为可能存在跨天所以使用 截止时间
String reqDate = DemoDateUtil.formatReqDate(endDateTime);
LOGGER.info("当前统计时间区间 reqDate => {}", reqDate);
//计算间隔秒数
long second = DemoDateUtil.betweenMs(startDateTime, endDateTime) / 1000;
LOGGER.info("计算间隔秒数 second => {}", second);
//hash缓存key
String hashKey = DemoConstant.USER_STAND_HASH_KEY.concat(reqDate);
LOGGER.debug("用户小程序停留时长统计Hash Key => {}", hashKey);
redisCache.hashIncrBy(hashKey, userId, second);
//list缓存key
String listKey = DemoConstant.USER_STAND_LIST_KEY.concat(reqDate);
LOGGER.debug("用户小程序停留时长统计List Key => {}", listKey);
redisCache.zAddByScore(listKey, userId, 0);
}
/**
* 时间非同一天
*
* @param startDateTime
* @param endDateTime
*/
private void doSaveNotSameDay(Date startDateTime, Date endDateTime, String userId) {
// 计算起点的结束时间 当天的 23:59:59
Date startDayEndDate = DemoDateUtil.endOfDay(startDateTime);
// 保存前一天的停留时长
this.sameDateSave(startDayEndDate, endDateTime, userId);
// 保存当天的停留时长
this.sameDateSave(startDateTime, startDayEndDate, userId);
}
}
定时持久化到DB
import com.boot.redis.constant.DemoConstant;
import com.boot.redis.persistence.entity.UserStandInfo;
import com.boot.redis.jredis.RedisCache;
import com.boot.redis.service.UserStandProcessService;
import com.boot.redis.util.DemoDateUtil;
import com.boot.redis.util.PageUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 用户小程序停留时长处理接口
* 实际应用中做定时凌晨执行
*/
@Service
public class UserStandProcessServiceImpl implements UserStandProcessService {
private Logger LOGGER = LoggerFactory.getLogger(UserStandProcessServiceImpl.class);
@Autowired
private RedisCache redisCache;
/**
* 分页页容量
*/
private static Integer MAX_BATCH_PAGE_SIZE = 10;
/**
* 定时处理用户停留时间
* 持久化到数据库
* 注:非自定义执行区间,默认定时凌晨统计前一天的
* 此处我未添加定时逻辑,请自行根据逻辑添加
*/
@Override
public void scheduledRunUserStand() {
//时间往前偏移一天
Date lastDay = DemoDateUtil.offsetDay(new Date(), -1);
//格式化时间为 yyyyMMdd格式
String reqDate = DemoDateUtil.formatReqDate(lastDay);
//定时处理用户数据
this.scheduledRunUserStand(reqDate);
}
/**
* 定时处理用户停留时间
* 持久化到数据库
*
* @param reqDate 自定义执行区间 yyyyMMdd格式
*/
@Override
public void scheduledRunUserStand(String reqDate) {
//拼装缓存key
String hashKey = DemoConstant.USER_STAND_HASH_KEY.concat(reqDate);
LOGGER.info("缓存 hashKey={}", hashKey);
String listKey = DemoConstant.USER_STAND_LIST_KEY.concat(reqDate);
LOGGER.info("缓存 listKey={}", listKey);
//查询缓存集合大小
int zSize = (int) redisCache.zCard(listKey);
LOGGER.info("总容量 zSize={}", zSize);
//计算总页数
int totalPage = PageUtil.getTotalPage(zSize, MAX_BATCH_PAGE_SIZE);
//从第一页开始
int pageNo = 1;
LOGGER.info("执行开始");
while (pageNo <= totalPage) {
System.out.println("******华丽的分割线 " + pageNo + " ******");
LOGGER.info(" 当前页开始 paeNo={},totalPage={}", pageNo, totalPage);
//计算当前页开始于结束位置
int start = PageUtil.getStart(pageNo, MAX_BATCH_PAGE_SIZE);
int end = PageUtil.getEnd(start, MAX_BATCH_PAGE_SIZE);
LOGGER.info(" 当前页开始 start={}", start);
List<String> stringList = redisCache.zRange(listKey, String.class, start, end);
List<UserStandInfo> userStandInfoList = new ArrayList<>(stringList.size());
stringList.forEach(sKey -> {
//根据 key 获取 hash 中的 统计值
Integer hashValue = redisCache.getHash(hashKey, sKey, Integer.class);
System.out.println("key=" + sKey + " hashValue=" + hashValue);
//构建统计记录信息
UserStandInfo userStandInfo = new UserStandInfo();
userStandInfo.setUserId(sKey);
userStandInfo.setStandCountTotal(hashValue);
userStandInfo.setStandCountDate(Integer.valueOf(reqDate));
userStandInfo.setCreateTime(new Date());
userStandInfoList.add(userStandInfo);
});
LOGGER.info("此处模拟批量新增到数据库操作");
LOGGER.info(" 当前页结束 paeNo={},totalPage={}", pageNo, totalPage);
pageNo++;
}
//执行完毕设置效期过期时间 3 天
//设置效期
redisCache.expire(hashKey, 3, TimeUnit.DAYS);
//设置效期
redisCache.expire(listKey, 3, TimeUnit.DAYS);
LOGGER.info("执行结束");
}
}