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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: Spring Boot实战解决高并发数据入库: Redis 缓存+MySQL 批量入库

强烈推荐一个大神的人工智能的教程:http://www.captainai.net/zhanghan


前言


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

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

特点:

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

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

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

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

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


架构设计


  • 架构图:

670e0c1ee355d808aa2d57209800c662_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png


  • 时序图

4bdeb086507261618711a15932e65262_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png

  • 记录基础数据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;
    @Autowired
    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
     */
    @Override
    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)) {
                return WrapMapper.ok();
            }
            xArticleCountMapper.batchAdd(beforeArticleCountDtoList);
            Integer delSize = beforeArticleCountDtoList.size();
            strRedisTemplate.opsForList().trim(zhArticleCountRedisKey, delSize, -1L);
        }
    }
}


测试


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

a29acf29874c018c4be6aa627d6231d5_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png

  • 查看缓存中访问数据

3233622959b2006371a9862950a57157_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png

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

830029c80e46fbaf208ca39c71fe385f_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png

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

20200808140648327.png

  • 查看数据库表结构

bff105ca5dcb17d78e741868fb03c3ad_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png


总结


  • 项目中定时任务
  • 问演示方便用http代替定时任务调度;实际项目中用XXL-job,参考:定时任务的选型及改造
  • 定时任务项目中用redis锁防止并发(定时任务调度端多次调度等),参考:Redis实现计数器—接口防刷—升级版(Redis+Lua)
  • 后期运营数据可以从阅读记录表中拉数据进行相关分析
  • 访问量大:可以将MySQL中的阅读记录表定时迁移走(MySQL建历史表,MongoDB等)

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1天前
|
SQL 存储 关系型数据库
不允许你不知道的 MySQL 优化实战(三)
不允许你不知道的 MySQL 优化实战(三)
27 1
|
1天前
|
关系型数据库 MySQL
【MySQL实战笔记】07 | 行锁功过:怎么减少行锁对性能的影响?-01
【4月更文挑战第18天】MySQL的InnoDB引擎支持行锁,而MyISAM只支持表锁。行锁在事务开始时添加,事务结束时释放,遵循两阶段锁协议。为减少锁冲突影响并发,应将可能导致最大冲突的锁操作放在事务最后。例如,在电影票交易中,应将更新影院账户余额的操作安排在事务末尾,以缩短锁住关键行的时间,提高系统并发性能。
15 4
|
1天前
|
SQL 关系型数据库 MySQL
不允许你不知道的 MySQL 优化实战(二)
不允许你不知道的 MySQL 优化实战(二)
27 2
|
1天前
|
缓存 NoSQL Java
17:缓存机制-Java Spring
17:缓存机制-Java Spring
41 5
|
1天前
|
XML 存储 缓存
Spring缓存是如何实现的?如何扩展使其支持过期删除功能?
总之,Spring的缓存抽象提供了一种方便的方式来实现缓存功能,并且可以与各种缓存提供商集成以支持不同的过期策略。您可以根据项目的具体需求选择适合的方式来配置和扩展Spring缓存功能。
19 0
|
1天前
|
缓存 关系型数据库 MySQL
MySQL数据库性能优化实战
【4月更文挑战第30天】本文探讨了MySQL性能优化实战技巧,包括硬件与配置优化(如使用SSD、增加内存和调整配置参数)、索引优化(创建合适索引、使用复合索引及定期维护)、查询优化(避免全表扫描、减少JOIN和使用LIMIT)、分区与分片(表分区和数据库分片),以及使用缓存、定期清理数据库和监控诊断。通过这些方法,可以提升数据库性能和响应速度。
|
1天前
|
存储 关系型数据库 MySQL
MySQL数据库实战:从入门到精通
本文介绍了MySQL的使用和优化,适合Web开发者阅读。首先,确保安装并配置好MySQL,熟悉SQL基础。接着,通过命令行客户端连接数据库,执行创建、查询、添加、修改和删除数据等操作。学习数据类型并创建表存储数据。最后,探讨了数据库优化,包括查询优化和索引使用,以提升性能。
27 2
|
1天前
|
安全 关系型数据库 MySQL
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
25 3
|
1天前
|
SQL 关系型数据库 MySQL
不允许你不知道的 MySQL 优化实战(一)
不允许你不知道的 MySQL 优化实战(一)
26 2
|
1天前
|
关系型数据库 MySQL 中间件
【MySQL实战笔记】07 | 行锁功过:怎么减少行锁对性能的影响?-02 死锁和死锁检测
【4月更文挑战第19天】在高并发环境下,死锁发生在多个线程间循环等待资源时,导致无限期等待。MySQL中,死锁可通过`innodb_lock_wait_timeout`参数设置超时或`innodb_deadlock_detect`开启死锁检测来解决。默认的50s超时可能不适用于在线服务,而频繁检测会消耗大量CPU。应对热点行更新引发的性能问题,可以暂时关闭死锁检测(风险是产生大量超时),控制并发度,或通过分散记录减少锁冲突,例如将数据分拆到多行以降低死锁概率。
24 1
http://www.vxiaotou.com