业务需求

统计小程序的用户停留时长
不需要实时统计,所以按照天为维度
使用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);@Autowiredprivate RedisCache redisCache;/*** 上报用户停留时间至 redis** @param userId     人员id* @param standStart 开始时间戳* @param standEnd   结束时间戳*/@Overridepublic 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缓存keyString hashKey = DemoConstant.USER_STAND_HASH_KEY.concat(reqDate);LOGGER.debug("用户小程序停留时长统计Hash Key => {}", hashKey);redisCache.hashIncrBy(hashKey, userId, second);//list缓存keyString 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:59Date 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);@Autowiredprivate RedisCache redisCache;/*** 分页页容量*/private static Integer MAX_BATCH_PAGE_SIZE = 10;/*** 定时处理用户停留时间* 持久化到数据库* 注:非自定义执行区间,默认定时凌晨统计前一天的* 此处我未添加定时逻辑,请自行根据逻辑添加*/@Overridepublic void scheduledRunUserStand() {//时间往前偏移一天Date lastDay = DemoDateUtil.offsetDay(new Date(), -1);//格式化时间为 yyyyMMdd格式String reqDate = DemoDateUtil.formatReqDate(lastDay);//定时处理用户数据this.scheduledRunUserStand(reqDate);}/*** 定时处理用户停留时间* 持久化到数据库** @param reqDate 自定义执行区间 yyyyMMdd格式*/@Overridepublic void scheduledRunUserStand(String reqDate) {//拼装缓存keyString 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("执行结束");}
}

Redis计数器统计小程序用户停留时长相关推荐

  1. 使用Scala编写Spark程序求基站下移动用户停留时长TopN

    使用Scala编写Spark程序求基站下移动用户停留时长TopN 1. 需求:根据手机基站日志计算停留时长的TopN 我们的手机之所以能够实现移动通信,是因为在全国各地有许许多多的基站,只要手机一开机 ...

  2. android获取小程序音频时长,最新微信小程序获取音频时长与实时获取播放进度...

    #微信小程序获取音频时长与实时获取播放进度 在小程序官方文档中 audio 注意:1.6.0 版本开始,该组件不再维护.建议使用能力更强的 wx.createInnerAudioContext 接口 ...

  3. 最新微信小程序获取音频时长与实时获取播放进度

    #微信小程序获取音频时长与实时获取播放进度 在小程序官方文档中 audio 注意:1.6.0 版本开始,该组件不再维护.建议使用能力更强的 wx.createInnerAudioContext 接口 ...

  4. android获取小程序音频时长,微信小程序获取音频时长与实时获取播放进度

    首先在没有播放音频之前,居然拿不到总时长 但是在播放之后也需要设置setTimeout来获取 所以在监听音频播放进度更新事件中获取.顺便获取当前播放进度 按照官方的写法 audioPlayed: fu ...

  5. 如何精确统计用户在页面的停留时长?

    作者:今日头条技术 链接:https://techblog.toutiao.com/2018/06/05/ru-he-jing-que-tong-ji-ye-mian-ting-liu-shi-cha ...

  6. webform窗体怎么实现session唯一标识_微信小程序用户登录和登录态维护的实现_javascript技巧...

    这篇文章主要介绍了微信小程序用户登录和登录态维护的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 让用户登录,标识用户和获取 ...

  7. 小程序用户行为数据监测与分析以及案例分享

    小程序现在火了.但是应该如何监测小程序的数据呢?相信这是一个大家都关心的问题.这篇文章对这个问题进行解答. 一共包括两个部分: 1. 如何获得小程序相关数据(常规数据和自定义事件数据): 2. 如何利 ...

  8. 微信小程序 用户协议和隐私协议

    <template><view class="myOne"><view class="my-div"><text cl ...

  9. react利用useEffect记录用户在当前页面停留时长

    项目场景: 小程序项目中需要记录用户在当前页面停留的时长,下意识想到在useState中记录一个时间戳,在useEffect中的return中获取一个时间戳,减去state中记录的时间戳,用得到的值取 ...

最新文章

  1. OpenCV最经典的3种颜色空间(cv2.cvtColor)及互相转换
  2. img to image_tag
  3. 裸奔浏览器_大概是最好用的隐私浏览器 - Firefox Focus
  4. c++字符前面的L和_T
  5. c语言入门教程文库,C语言入门教程(全集)课件
  6. Mybatis-plus之RowBounds实现分页查询
  7. 课程学习:程序设计与算法
  8. vhdl和c语言,VHDL语言中的信号、变量与常量异同比较(转)
  9. Core Data 和 sqlite3的性能对比【图】3gs,iPhone4,4s,5的性能测试。
  10. 正则表达式学习笔记004--连字符和范围描述符的认识与应用
  11. 使用dotenv管理环境变量
  12. faster rcnn resnet_张航、李沐等人提出ResNet最强改进版:性能提高3%,参数不增
  13. 洛谷1966 火柴排队
  14. word转pdf公式乱码_word转pdf乱码
  15. 去NM的OKR,大坑,得把人逼疯!
  16. android七牛短视频sdk源码,使用七牛开发短视频
  17. 如何在Win10中使用Windows图片查看器打开图片
  18. ADI官方解释在SPI通信期间,数据的发送(串行移出到MOSI/SDO总线上)和接收(采样或读入总线(MISO/SDI)上的数据)
  19. getActionCommand()用法
  20. Abp.Zero 手机号免密登录验证与号码绑定功能的实现(一):验证码模块

热门文章

  1. web APP的特点
  2. python wraps_python wraps那点儿事儿
  3. C++的 lamda函数
  4. linux内核ip路由表逐条释义,Linux 路由表
  5. Andriod-解决签名错误导致wifi 扫描不到ap
  6. Shader入门精要-2-shader入门/基础光照模型/纹理
  7. 二级计算机考生报名登记表,2020年9月计算机二级报名流程
  8. 按关键字搜索商品接口
  9. 【C#控件】MenuStrip控件(菜单控件)
  10. 微擎后台数据库表关系