Spring Boot+MyBatis+Redis+ActiveMQ+MySQL+Thymeleaf实现简单的高并发点赞功能(下)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Spring Boot+MyBatis+Redis+ActiveMQ+MySQL+Thymeleaf实现简单的高并发点赞功能(下)

5.Web层

package com.zhongger.highconcurrentpaise.controller;
import com.zhongger.highconcurrentpaise.domain.MoodDTO;
import com.zhongger.highconcurrentpaise.service.MoodService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
 * @Author Zhongger
 * @Description
 * @Date
 */
@Controller
public class MoodController {
    @Autowired
    private MoodService moodService;
    @GetMapping("/moods")
    public String findAllMood(Model model){
        List<MoodDTO> moodDTOList = moodService.findAllMood();
        model.addAttribute("moodDTOs",moodDTOList);
        return "index";
    }
    @GetMapping("/{moodId}/praise")
    public String praise(Model model, @PathVariable("moodId")String moodId, @RequestParam("userId")String userId){
        boolean isPraise = moodService.praiseMood(moodId, userId);
        List<MoodDTO> moodDTOList = moodService.findAllMood();
        model.addAttribute("moodDTOs",moodDTOList);
        model.addAttribute("isPraise",isPraise);
        return "index";
    }
}

前端页面

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>微博列表</title>
</head>
<body>
<table>
    <thead>
    <tr>
        <th>用户</th>
        <th>    </th>
        <th>微博内容</th>
        <th>    </th>
        <th>发表时间</th>
        <th>    </th>
        <th>点赞数</th>
        <th>    </th>
        <th>操作</th>
    </tr>
    <tr th:each="moodDTO:${moodDTOs}">
        <td th:text="${moodDTO.userName}"></td>
        <td>            </td>
        <td th:text="${moodDTO.content}"></td>
        <td>            </td>
        <td th:text="${moodDTO.publishTime}"></td>
        <td>            </td>
        <td th:text="${moodDTO.praiseNum}"></td>
        <td>            </td>
        <td><a th:href="@{'/'+${moodDTO.id}+'/praise?userId='+${moodDTO.userId}}">赞</a></td>
    </tr>
    </thead>
</table>
</body>
</html>

6.项目启动运行


访问连接:http://localhost:8888/HighConcurrentPraise/moods



每点击一次赞,相应的点赞数会加一。这种传统的点赞实现可以用以下流程图来表示:

20200228175139201.png



那么我们分析一下这种方式会有什么问题?Service层处理的过程中,请求数据库获取连接,执行相关的数据库操作后归还数据库连接池,最终返回数据给前端页面。但是,如果在高并发的情况下,有些微博会在半个小时内点赞数量高达20万,那么QPS(QPS=每秒请求数/事务数)可能会高达111,这意味着后端需要每秒创建111个线程来处理点赞请求,而每次创建数据库连接或从数据库连接池中获取连接,数据库连接的数量是有限的,那么高的线程请求数和数据库连接数对服务器来说压力非常大,会导致服务器响应时间长,处理缓慢甚至宕机。


7.集成Redis


利用Redis的以下特点:

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

可以解决部分高并发的问题,把点赞相关的数据先保存到Redis中,然后通过Quartz创建定时计划,再把Redis缓存中的数据保存到MySQL中。


现在解决方案如下:


20200228182418145.png


将数据缓存到Redis的核心代码如下:

MoodServiceImpl.java


  @Override
    public boolean praiseMoodForRedis(String userId, String moodId) {
        redisTemplate.opsForSet().add(PRAISE_HASH_KEY,moodId);
        redisTemplate.opsForSet().add(moodId,userId);
        return false;
    }
    @Override
    public List<MoodDTO> findAllForRedis() {
        List<Mood> moodList = moodMapper.findAll();
        if (CollectionUtils.isEmpty(moodList)){
            return Collections.EMPTY_LIST;
        }
        List<MoodDTO> moodDTOS = new ArrayList<>();
        for (Mood mood:moodList) {
            MoodDTO moodDTO = new MoodDTO();
            moodDTO.setId(mood.getId());
            moodDTO.setUserId(mood.getUserId());
            //总的点赞数=数据库的点赞数量+redis的点赞数量
            moodDTO.setPraiseNum(mood.getPraiseNum()+redisTemplate.opsForSet().size(mood.getId()).intValue());
            moodDTO.setPublishTime(mood.getPublishTime());
            moodDTO.setContent(mood.getContent());
            UserDTO user = userService.findUserById(mood.getUserId());
            moodDTO.setUserId(user.getId());
            moodDTO.setUserName(user.getName());
            moodDTO.setUserAccount(user.getAccount());
            moodDTOS.add(moodDTO);
        }
        return moodDTOS;
    }

将数据定时地从Redis中取出,并将数据持久化入MySQL的核心代码如下:

package com.zhongger.highconcurrentpaise.utils;
import com.zhongger.highconcurrentpaise.domain.Mood;
import com.zhongger.highconcurrentpaise.domain.UserMood;
import com.zhongger.highconcurrentpaise.service.MoodService;
import com.zhongger.highconcurrentpaise.service.UserMoodPraiseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Set;
/**
 * @Author Zhongger
 * @Description 定时器,不定时地把redis中的数据取出写入到数据库中
 * @Date 2020.2.28
 */
@Component
@Configurable //相当于xml配置文件,可以被Spring扫描初始化
@EnableScheduling //开启对计划任务的支持
public class PraiseDataSaveDBJob {
    @Autowired
    private RedisTemplate redisTemplate;
    private static final String PRAISE_HASH_KEY="HighConcurrentPraise.mood.id.list.key";
    @Autowired
    private UserMoodPraiseService userMoodPraiseService;
    @Autowired
    private MoodService moodService;
    @Scheduled(cron = "*/10 * * * * *")//声明为定时任务,每10秒一次。工程上一般是指定凌晨2点左右进行持久化
    public void savePraiseDataToMySQL(){
        //1.在redis缓存中所有被点赞的说说
        Set<String> moods = redisTemplate.opsForSet().members(PRAISE_HASH_KEY);
        if (CollectionUtils.isEmpty(moods)){
            return;
        }
        for (String moodId:moods) {
            if (redisTemplate.opsForSet().members(moodId)==userMoodPraiseService){
                continue;
            }else {
                //2.从redis缓存中,提高微博id来获取所有点赞的用户id列表
                Set<String> userIds = redisTemplate.opsForSet().members(moodId);
                if (CollectionUtils.isEmpty(userIds)){
                    continue;
                }else {
                    //3.循环保存moodId和userId到user_mood表中
                    for (String userId:userIds) {
                        UserMood userMood = new UserMood();
                        userMood.setMoodId(moodId);
                        userMood.setUserId(userId);
                        //保存到MySQL
                        userMoodPraiseService.saveUserMood(userMood);
                    }
                    Mood mood = moodService.findMoodById(moodId);
                    //4.更新点赞数
                    mood.setPraiseNum(mood.getPraiseNum()+redisTemplate.opsForSet().size(moodId).intValue());
                    moodService.update(mood);
                    //5.清除缓存中的数据
                    redisTemplate.delete(moodId);
                }
            }
        }
        //6.清除缓存中的数据
        redisTemplate.delete(PRAISE_HASH_KEY);
    }
}

8.集成ActiveMQ


为了解决高并发请求下,点赞功能同步处理所带来的服务器压力(Redis缓存的压力或数据库压力),引入ActiveMQ中间件进行异步处理,用户每次点赞都会把消息push到MQ的Queue中,这样用户的点赞请求就能及时结束,避免了点赞请求线程占用时间长的问题。当MQ接收到消息后,会使用异步或同步的方式进行消费,把数据缓存入Redis中。此外,可以使用MQ来限制流量并异步处理等。

生产者:


package com.zhongger.highconcurrentpaise.mq;
import com.zhongger.highconcurrentpaise.domain.MoodDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
import javax.jms.Destination;
import java.util.logging.Logger;
/**
 * @Author Zhongger
 * @Description 生产者
 * @Date
 */
@Component
public class MoodProducer {
    @Autowired
    private JmsTemplate jmsTemplate;
    private Logger logger=Logger.getLogger(String.valueOf(this.getClass()));
    public void sendMessage(Destination destination, final MoodDTO moodDTO){
        logger.info("生产者--->>>用户id:"+moodDTO.getUserId()+"给微博id:"+moodDTO.getId()+"点赞");
        jmsTemplate.convertAndSend(destination,moodDTO);
    }
}

消费者:这里的消费者实现了MessageListener接口中的onMessage方法,该方法会使消费者异步地消费在队列中的消息。

package com.zhongger.highconcurrentpaise.mq;
import com.zhongger.highconcurrentpaise.domain.MoodDTO;
import org.apache.activemq.command.ActiveMQObjectMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import java.util.logging.Logger;
/**
 * @Author Zhongger
 * @Description 消费者
 * @Date
 */
//@Component
public class MoodConsumer implements MessageListener {
    //@Autowired
    private RedisTemplate redisTemplate;
    private static final String PRAISE_HASH_KEY="HighConcurrentPraise.mood.id.list.key";
    private Logger logger=Logger.getLogger(String.valueOf(this.getClass()));
    @Override
    public void onMessage(Message message) {
        try {
            //1.从message对象中获取到微博实体
            MoodDTO moodDTO = (MoodDTO)((ActiveMQObjectMessage) message).getObject();
            //2.存放到set中
            redisTemplate.opsForSet().add(PRAISE_HASH_KEY,moodDTO.getId());
            //3.存放到set中
            redisTemplate.opsForSet().add(moodDTO.getId(),moodDTO.getUserId());
            //4.记录日志
            logger.info("生产者--->>>用户id:"+moodDTO.getUserId()+"给微博id:"+moodDTO.getId()+"点赞");
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}

最后在MoodServiceImpl中做修改,把点赞信息进行异步处理。

@Override
    public boolean praiseMoodForRedis(String userId, String moodId) {
        //修改为一异步处理方式
        MoodDTO moodDTO = new MoodDTO();
        moodDTO.setUserId(userId);
        moodDTO.setId(moodId);
        //发消息
        moodProducer.sendMessage(destination,moodDTO);
       /* redisTemplate.opsForSet().add(PRAISE_HASH_KEY,moodId);
        redisTemplate.opsForSet().add(moodId,userId);*/
        return false;
    }

9.总结


项目到这里就已经结束了,通过这个小项目,让我初步地了解到了如何在高并发的场景下尽最大可能地提升服务器的性能,如何保证用户的良好体验,并学习了当今比较主流的解决方案。当然,这个项目只是体验高并发的解决方案,有些不太合适的业务逻辑,多多包含。此外,也发现自己对于多线程、网络、IO等知识的掌握不够,在做项目的过程中有遇到一些理解上的问题,继续加油吧!非科班出身,就要花更多的努力!加油吧!这是我部署到阿里云上的链接,大家可以进去试试


http://101.37.13.188:8612/HighConcurrentPraise/moods

后期我会把代码发布到GitHub上,如果急需源码的话现可以私聊我~

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
27天前
|
存储 NoSQL Java
Redis助力高并发网站:在线用户统计不再是难题!
小米带你了解如何使用Redis高效统计网站的在线与并发用户数。通过维护用户的活跃时间,利用Redis有序集合(Sorted Set)特性,可实时更新在线用户列表并统计数量。具体实现包括记录用户上线时间、定期清理离线用户及统计特定时间窗口内的活跃用户数。这种方法适用于高并发场景,保证统计结果的实时性和准确性。跟着小米一起探索Redis的强大功能吧!
35 2
|
14天前
|
自然语言处理 关系型数据库 MySQL
mysql 全文搜索功能优缺点
mysql 全文搜索功能优缺点
|
9天前
|
NoSQL 关系型数据库 Redis
mall在linux环境下的部署(基于Docker容器),Docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongo
mall在linux环境下的部署(基于Docker容器),docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongodb、minio详细教程,拉取镜像、运行容器
mall在linux环境下的部署(基于Docker容器),Docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongo
|
9天前
|
缓存 NoSQL 关系型数据库
MySQL与Redis缓存一致性的实现与挑战
在现代软件开发中,MySQL作为关系型数据库管理系统,广泛应用于数据存储;而Redis则以其高性能的内存数据结构存储特性,常被用作缓存层来提升数据访问速度。然而,当MySQL与Redis结合使用时,确保两者之间的数据一致性成为了一个重要且复杂的挑战。本文将从技术角度分享MySQL与Redis缓存一致性的实现方法及其面临的挑战。
30 2
|
1月前
|
存储 关系型数据库 MySQL
基于python django 医院管理系统,多用户功能,包括管理员、用户、医生,数据库MySQL
本文介绍了一个基于Python Django框架开发的医院管理系统,该系统设计了管理员、用户和医生三个角色,具备多用户功能,并使用MySQL数据库进行数据存储和管理。
基于python django 医院管理系统,多用户功能,包括管理员、用户、医生,数据库MySQL
|
17天前
|
JavaScript 关系型数据库 MySQL
node连接mysql,并实现增删改查功能
【8月更文挑战第26天】node连接mysql,并实现增删改查功能
32 3
|
20天前
|
存储 缓存 NoSQL
Redis内存管理揭秘:掌握淘汰策略,让你的数据库在高并发下也能游刃有余,守护业务稳定运行!
【8月更文挑战第22天】Redis的内存淘汰策略管理内存使用,防止溢出。主要包括:noeviction(拒绝新写入)、LRU/LFU(淘汰最少使用/最不常用数据)、RANDOM(随机淘汰)及TTL(淘汰接近过期数据)。策略选择需依据应用场景、数据特性和性能需求。可通过Redis命令行工具或配置文件进行设置。
35 2
|
1月前
|
NoSQL 关系型数据库 MySQL
无法访问Docker 里的 mysql, redis
无法访问Docker 里的 mysql, redis
15 0
|
21天前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
2月前
|
Java 测试技术 数据库
Spring Boot中的项目属性配置
本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。