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
每点击一次赞,相应的点赞数会加一。这种传统的点赞实现可以用以下流程图来表示:
那么我们分析一下这种方式会有什么问题?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中。
现在解决方案如下:
将数据缓存到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上,如果急需源码的话现可以私聊我~