Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题


Posted in Redis onFebruary 12, 2022

前言

最近在做阅读类的业务,需要记录用户的PV,UV;

项目状况:前期尝试业务阶段;

特点:

快速实现(不需要做太重,满足初期推广运营即可)快速投入市场去运营

收集用户的原始数据,三要素:

谁在什么时间阅读哪篇文章

提到PV,UV脑海中首先浮现特点:

需要考虑性能(每个客户每打开一篇文章进行记录)允许数据有较小误差(少部分数据丢失)

架构设计

架构图:

Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题

时序图

Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题

记录基础数据MySQL表结构

CREATE TABLE `zh_article_count` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `bu_no` varchar(32) DEFAULT NULL COMMENT '业务编码',
  `customer_id` varchar(32) DEFAULT NULL COMMENT '用户编码',
  `type` int(2) DEFAULT '0' COMMENT '统计类型:0APP内文章阅读',
  `article_no` varchar(32) DEFAULT NULL COMMENT '文章编码',
  `read_time` datetime DEFAULT NULL COMMENT '阅读时间',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `param1` int(2) DEFAULT NULL COMMENT '预留字段1',
  `param2` int(4) DEFAULT NULL COMMENT '预留字段2',
  `param3` int(11) DEFAULT NULL COMMENT '预留字段3',
  `param4` varchar(20) DEFAULT NULL COMMENT '预留字段4',
  `param5` varchar(32) DEFAULT NULL COMMENT '预留字段5',
  `param6` varchar(64) DEFAULT NULL COMMENT '预留字段6',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uk_zh_article_count_buno` (`bu_no`),
  KEY `key_zh_article_count_csign` (`customer_id`),
  KEY `key_zh_article_count_ano` (`article_no`),
  KEY `key_zh_article_count_rtime` (`read_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章阅读统计表';

技术实现方案

SpringBoot

Redis

MySQL

代码实现

完整代码(GitHub,欢迎大家Star,Fork,Watch)

https://github.com/dangnianchuntian/springboot

主要代码展示

Controller

/*
 * Copyright (c) 2020. zhanghan_java@163.com All Rights Reserved.
 * 项目名称:Spring Boot实战解决高并发数据入库: Redis 缓存+MySQL 批量入库
 * 类名称:ArticleCountController.java
 * 创建人:张晗
 * 联系方式:zhanghan_java@163.com
 * 开源地址: https://github.com/dangnianchuntian/springboot
 * 博客地址: https://zhanghan.blog.csdn.net
 */

package com.zhanghan.zhredistodb.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.zhanghan.zhredistodb.controller.request.PostArticleViewsRequest;
import com.zhanghan.zhredistodb.service.ArticleCountService;
@RestController
public class ArticleCountController {
    @Autowired
    private ArticleCountService articleCountService;
   /**
    * 记录用户访问记录
    */
    @RequestMapping(value = "/post/article/views", method = RequestMethod.POST)
    public Object postArticleViews(@RequestBody @Validated PostArticleViewsRequest postArticleViewsRequest) {
        return articleCountService.postArticleViews(postArticleViewsRequest);
    }
    /**
     *  批量将缓存中的数据同步到MySQL(模拟定时任务操作)
     */
    @RequestMapping(value = "/post/batch", method = RequestMethod.POST)
    public Object postBatch() {
        return articleCountService.postBatchRedisToDb();
}

Service

/*
 * Copyright (c) 2020. zhanghan_java@163.com All Rights Reserved.
 * 项目名称:Spring Boot实战解决高并发数据入库: Redis 缓存+MySQL 批量入库
 * 类名称:ArticleCountServiceImpl.java
 * 创建人:张晗
 * 联系方式:zhanghan_java@163.com
 * 开源地址: https://github.com/dangnianchuntian/springboot
 * 博客地址: https://zhanghan.blog.csdn.net
 */

package com.zhanghan.zhredistodb.service.impl;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import com.alibaba.fastjson.JSON;
import com.zhanghan.zhredistodb.controller.request.PostArticleViewsRequest;
import com.zhanghan.zhredistodb.dto.ArticleCountDto;
import com.zhanghan.zhredistodb.mybatis.mapper.XArticleCountMapper;
import com.zhanghan.zhredistodb.service.ArticleCountService;
import com.zhanghan.zhredistodb.util.wrapper.WrapMapper;
import cn.hutool.core.util.IdUtil;
@Service
public class ArticleCountServiceImpl implements ArticleCountService {
    private static Logger logger = LoggerFactory.getLogger(ArticleCountServiceImpl.class);
    @Autowired
    private RedisTemplate<String, String> strRedisTemplate;
    private XArticleCountMapper xArticleCountMapper;
    @Value("${zh.article.count.redis.key:zh}")
    private String zhArticleCountRedisKey;
    @Value("#{T(java.lang.Integer).parseInt('${zh..article.read.num:3}')}")
    private Integer articleReadNum;
    /**
     * 记录用户访问记录
     */
    @Override
    public Object postArticleViews(PostArticleViewsRequest postArticleViewsRequest) {
        ArticleCountDto articleCountDto = new ArticleCountDto();
        articleCountDto.setBuNo(IdUtil.simpleUUID());
        articleCountDto.setCustomerId(postArticleViewsRequest.getCustomerId());
        articleCountDto.setArticleNo(postArticleViewsRequest.getArticleNo());
        articleCountDto.setReadTime(new Date());
        String strArticleCountDto = JSON.toJSONString(articleCountDto);
        strRedisTemplate.opsForList().rightPush(zhArticleCountRedisKey, strArticleCountDto);
        return WrapMapper.ok();
    }
     * 批量将缓存中的数据同步到MySQL
    public Object postBatchRedisToDb() {
        Date now = new Date();
        while (true) {
            List<String> strArticleCountList =
                    strRedisTemplate.opsForList().range(zhArticleCountRedisKey, 0, articleReadNum);
            if (CollectionUtils.isEmpty(strArticleCountList)) {
                return WrapMapper.ok();
            }
            List<ArticleCountDto> articleCountDtoList = new ArrayList<>();
            strArticleCountList.stream().forEach(x -> {
                ArticleCountDto articleCountDto = JSON.parseObject(x, ArticleCountDto.class);
                articleCountDtoList.add(articleCountDto);
            });
            //过滤出本次定时任务之前的缓存中数据,防止死循环
            List<ArticleCountDto> beforeArticleCountDtoList = articleCountDtoList.stream().filter(x -> x.getReadTime()
                    .before(now)).collect(Collectors.toList());
            if (CollectionUtils.isEmpty(beforeArticleCountDtoList)) {
            xArticleCountMapper.batchAdd(beforeArticleCountDtoList);
            Integer delSize = beforeArticleCountDtoList.size();
            strRedisTemplate.opsForList().trim(zhArticleCountRedisKey, delSize, -1L);
        }
}

测试

模拟用户请求访问后台(多次请求)

Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题

查看缓存中访问数据

Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题

模拟定时任务将缓存中数据同步到DB中

Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题

这时查看缓存中的数据已经没了

Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题

查看数据库表结构

Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题

总结

  • 项目中定时任务
  • 问演示方便用http代替定时任务调度;实际项目中用XXL-job
  • 定时任务项目中用redis锁防止并发(定时任务调度端多次调度等)
  • 后期运营数据可以从阅读记录表中拉数据进行相关分析
  • 访问量大:可以将MySQL中的阅读记录表定时迁移走(MySQL建历史表,MongoDB等)

到此这篇关于Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库的文章就介绍到这了,更多相关Spring Boot高并发数据入库内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
redis通过6379端口无法连接服务器(redis-server.exe闪退)
May 08 Redis
Redis高级数据类型Hyperloglog、Bitmap的使用
May 24 Redis
Java Socket实现Redis客户端的详细说明
May 26 Redis
Redis 哨兵集群的实现
Jun 18 Redis
解析高可用Redis服务架构分析与搭建方案
Jun 20 Redis
使用Redis实现实时排行榜功能
Jul 02 Redis
基于Redis结合SpringBoot的秒杀案例详解
Oct 05 Redis
关于SpringBoot 使用 Redis 分布式锁解决并发问题
Nov 17 Redis
Grafana可视化监控系统结合SpringBoot使用
Apr 19 Redis
Redis keys命令的具体使用
Jun 05 Redis
redis lua限流算法实现示例
Jul 15 Redis
基于Redis zSet实现滑动窗口对短信进行防刷限流的问题
Feb 12 #Redis
聊聊redis-dump工具安装问题
Jan 18 #Redis
redis的list数据类型相关命令介绍及使用
Jan 18 #Redis
关于使用Redisson订阅数问题
Jan 18 #Redis
Redis中缓存穿透/击穿/雪崩问题和解决方法
linux下安装redis图文详细步骤
Springboot/Springcloud项目集成redis进行存取的过程解析
You might like
B2K与车机的中波PK
2021/03/02 无线电
php你的验证码安全码?
2007/01/02 PHP
php魔术方法功能与用法实例分析
2016/10/19 PHP
PHP符合PSR编程规范的实例分享
2016/12/21 PHP
DWR Ext 加载数据
2009/03/22 Javascript
Exitjs获取DataView中图片文件名
2009/11/26 Javascript
让ie6也支持websocket采用flash封装实现
2013/02/18 Javascript
js中widow.open()方法使用详解
2013/07/30 Javascript
推荐25个超炫的jQuery网格插件
2014/11/28 Javascript
jquery实现可拖拽弹出层特效
2015/01/04 Javascript
JS输入用户名自动显示邮箱后缀列表的方法
2015/01/27 Javascript
JavaScript原生对象之Number对象的属性和方法详解
2015/03/13 Javascript
JavaScript中使用Callback控制流程介绍
2015/03/16 Javascript
js实现仿Windows风格选项卡和按钮效果实例
2015/05/13 Javascript
jQuery增加自定义函数的方法
2015/07/18 Javascript
BootStrap智能表单实战系列(十一)级联下拉的支持
2016/06/13 Javascript
只需五句话搞定JavaScript作用域(经典)
2016/07/26 Javascript
JavaScript中push(),join() 函数 实例详解
2016/09/06 Javascript
js实现九宫格的随机颜色跳转
2017/02/19 Javascript
vux uploader 图片上传组件的安装使用方法
2018/05/15 Javascript
JS获取子节点、父节点和兄弟节点的方法实例总结
2018/07/06 Javascript
JSONP原理及应用实例详解
2018/09/13 Javascript
vue商城中商品“筛选器”功能的实现代码
2020/07/01 Javascript
vue或react项目生产环境去掉console.log的操作
2020/09/02 Javascript
TensorFlow saver指定变量的存取
2018/03/10 Python
python实现图片文件批量重命名
2020/03/23 Python
python3.6使用pymysql连接Mysql数据库
2018/05/25 Python
Django 响应数据response的返回源码详解
2019/08/06 Python
解决Python在导入文件时的FileNotFoundError问题
2020/04/10 Python
Speedo美国:澳大利亚顶尖泳衣制造商
2016/08/03 全球购物
英国第一摩托车和摩托车越野配件商店:GhostBikes
2019/03/10 全球购物
是什么让J2EE适合用来开发多层的分布式的应用
2015/01/16 面试题
入党积极分子自我鉴定
2014/02/18 职场文书
2014村书记党建工作汇报材料
2014/11/02 职场文书
读《庄子》有感:美而不自知
2019/11/06 职场文书
SQLyog的下载、安装、破解、配置教程(MySQL可视化工具安装)
2022/09/23 MySQL