• [Spring MVC 8]高并发实战小Demo


    本项目基于Spring MVC进行关于点赞项目的开发,从传统的点赞到高并发缓存开发最后到消息队列异步开发,可谓是令人大开眼界。
    本篇博客全部代码已经放出,本博客重点是后端操作,所以对于前端就十分简单的页面。讲述了关于Redis,Quartz定时器、ActiveMQ消息队列等相关内容,需要好好掌握。
    关于Spring MVC基本上内容也都完结了,后期还继续深化Spring 体系,特别是源码系统,都需要好好掌握。

    这里就高并发稍微说明一下,我们常见的高并发场景有:淘宝的双11、春运时的抢票、微博大V的热点新闻等。除了这些典型事情,每秒几十万请求的秒杀系统、每天千万级的订单系统、每天亿级日活的信息流系统等,都可以归为高并发。

    而传统的项目大多直接操作数据库,这会造成数据库承受不住,所以引入了Redis进行缓存。

    项目概述

    在社交网站或者App中,点赞场景有很多,比如微信说说点赞、微博点赞等。普通人发的说说点赞数比较少,所以并发数少,而一些名人发的微博,由于粉丝多,可能短时间点赞数高达数百万。面对如此高并发的点赞,如果没有设计好项目那么会导致服务器和数据库压力过大出现异常。很多公司后端架构必会采取很多措施来解决这种高并发场景,比如引入缓存提升读的性能、使用MQ队列进行异步处理。
    本章没有复杂的前端页面和业务,主要是部分功能,理解高并发的架构设计。

    数据库表与持久化类

    表的内容已经在ay_user数据库中
    创建user表(用户),mood表(说说),user_mood_praise_rel 表(点赞关联表)
    user和mood是一对多的关系,关联表主要记录用户和说说的关联关系,即哪些说说被哪些用户点赞。

    create table user (
        id varchar(32) not null ,
        name varchar(20) default  null,
        account varchar(20) default  null,
        primary key (id)
    #     key 'user_name_index' (name) using btree ,
    #     key 'user_account_index' (account) using btree
    ) engine = InnoDB default charset = utf8;
    
    create table mood(
        id varchar(32) not null  ,
        content varchar(256) default null,
        user_id varchar(32) default null,
        publish_time datetime default  null,
        praise_num int(11) default null,
        primary key (id)
    )engine = InnoDB default charset = utf8;
    
    create table user_mood_praise_rel (
        id bigint(32) not null  AUTO_INCREMENT,
        user_id varchar(32) default null,
        mood_id varchar(32) default  null,
        primary key (id)
    )engine = InnoDB default charset = utf8;
    
    insert into user (id, name, account) values ('1','Jacin','ay');
    insert into user (id, name, account) values ('2','ali','a1');
    
    insert into mood (id, content, user_id, publish_time, praise_num) values ('1','Good','1','2022-11-12','100');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在ay.model包创建user表的持久化对象,具体代码如下:

    public class User implements Serializable {
        private String id;
        private String name;
       
        private String account;
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    
        public String getAccount() {
            return account;
        }
    
        public void setAccount(String account) {
            this.account = account;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    同样的道理,建立mood,user_mood_praise_rel类,和数据库字段类似,这里就不再放代码了。

    DAO层和Mapper映射文件

    数据库表和持久化类创建完成后,继续创建对应的Dao类和Mapper映射文件。
    在ay.dao创建对应的Dao类:
    这里注意Repository(一般用于接口层):该注解的作用不只是将类识别为Bean,同时它还能将所标注的类中抛出的数据访问异常封装为 Spring 的数据访问异常类型。 Spring本身提供了一个丰富的并且是与具体的数据访问技术无关的数据访问异常结构,用于封装不同的持久层框架抛出的异常,使得异常独立于底层的框架。

    @Repository
    public interface UserDao {
        User find(String id);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在mapper目录下创建user表对应的UserMapper.xml:

    
    DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.ay.dao.UserDao">
        <cache/>
        <resultMap id="userMap" type="com.ay.model.User">
            <id property="id" column="id"/>
            <result property="name" column="name"/>
            <result property="account" column="account"/>
        resultMap>
        
        <sql id="table_column">
            id,
            name,
            account
        sql>
    
        <select id="find" resultMap="userMap">
            select
            <include refid="table_column"/>
            from user
            <where>
                id = #{id}
            where>
        select>
    
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    其中MoodDao.java:

    @Repository
    public interface MoodDao {
        List<Mood> findAll();
    }
    
    • 1
    • 2
    • 3
    • 4

    以及UserMoodPraiseRelDao.java:

    @Repository
    public interface UserMoodPraiseRelDao {
        boolean save(@Param("userMoodPraiseRel")UserMoodPraiseRel userMoodPraiseRel);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    相关的mapper.xml就不再重复写了。

    service层和DTO类

    DTO是数据传输对象,用于extends Model类。
    通过DTO我们实现了表现层与Model之间的解耦,表现层不引用Model,DTO extends Model层。
    ay.dto\MoodDTO.java:

    public class MoodDTO extends Mood implements Serializable {
        
        private String userName;
    
        private String userAccount;
    
        public String getUserName() {
            return userName;
        }
    
        public void setUserName(String userName) {
            this.userName = userName;
        }
    
        public String getUserAccount() {
            return userAccount;
        }
    
        public void setUserAccount(String userAccount) {
            this.userAccount = userAccount;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    ay.dto\UserDTO.java:

    package com.ay.dto;
    
    import com.ay.model.User;
    public class UserDTO extends User {
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    MoodDTO和UserDTO用于前端展示用的DTO对象,内容比较简单。
    接着在service包下面:UserService.java:

    public interface UserService {
        UserDTO find(String id);
    }
    
    
    • 1
    • 2
    • 3
    • 4

    MoodService.java:

    public interface MoodService {
        //传统查询
        List<MoodDTO> findAll();
    }
    
    • 1
    • 2
    • 3
    • 4

    接着实现UserServiceImpl.java:

    
    @Service
    public class UserServiceImpl implements UserService {
        @Resource
        private UserDao userDao;
        public UserDTO find(String id) {
            User user = userDao.find(id);
            return converModel2DTO(user);
        }
        private UserDTO converModel2DTO(User user) {
            UserDTO userDTO = new UserDTO();
            userDTO.setId(user.getId());
            userDTO.setAccount(user.getAccount());
            userDTO.setName(user.getName());
            return userDTO;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    MoodServiveImpl.java:

    @Service
    public class MoodServiveImpl implements MoodService {
        @Resource
        private MoodDao moodDao;
        @Resource
        private UserDao userDao;
    
    
        public List<MoodDTO> findAll() {
            List<Mood> moodList = moodDao.findAll();
            return converModel2DTO(moodList);
        }
    
        private List<MoodDTO> converModel2DTO(List<Mood> moodList) {
            if (CollectionUtils.isEmpty(moodList)) return Collections.EMPTY_LIST;
            List<MoodDTO> moodDTOList = new ArrayList<MoodDTO>();
            for (Mood mood : moodList) {
                MoodDTO moodDTO = new MoodDTO();
                moodDTO.setId(mood.getId());
                moodDTO.setContent(mood.getContent());
                moodDTO.setPraiseNum(mood.getPraiseNum());
                moodDTO.setPublishTime(mood.getPublishTime());
                moodDTO.setUserId(mood.getUserId());
                moodDTOList.add(moodDTO);
                //设置用户信息
                User user = userDao.find(mood.getUserId());
                moodDTO.setUserName(user.getName());
                moodDTO.setUserAccount(user.getAccount());
            }
            return moodDTOList;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    然后实现UserMoodPraiseRelService.java:

    public interface UserMoodPraiseRelService {
    
        boolean save(UserMoodPraiseRel userMoodPraiseRel);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接着实现类:

    @Service
    public class UserMoodPraiseRelServiceImpl implements UserMoodPraiseRelService {
    
        @Resource
        private UserMoodPraiseRelDao userMoodPraiseRelDao;
    
        public boolean save(UserMoodPraiseRel userMoodPraiseRel) {
            return userMoodPraiseRelDao.save(userMoodPraiseRel);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Controller层和前端页面

    ay.controller\UserController.java:

    @RestController
    @RequestMapping("/user")
    public class UserController {
        @Resource
        private UserService userService;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    MoodController.java:

    @Controller
    @RequestMapping("/mood")
    public class MoodController {
        @Resource
        private MoodService moodService;
    
        @RequestMapping("/findAll")
        public String findAll(Model model) {
            List<MoodDTO> moodDTOList = moodService.findAll();
            model.addAttribute("moods",moodDTOList);
            return "mood";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在views/mood.jsp:

    <%@page language="java" contentType="text/html; charset=UTF-8"
            pageEncoding="UTF-8" isELIgnored="false" %>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
    <%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>
    <!DOCTYPE HTML>
    <html>
    <head>
        <title>Getting Started: Serving Web Content</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    </head>
    <body>
    
    <div id="moods">
        <b>说说列表:</b><br>
        <c:forEach items="${moods}" var="mood">
            ------------------------------------
            <br>
            <b>用户:</b><span id="account">${mood.userName}</span><br>
            <b>说说内容:</b><span id="content">${mood.content}</span><br>
            <b>发表时间:</b>
            <span id="publish_time">
                    ${mood.publishTime}
            </span><br>
            <b>点赞数:</b><span id="praise_num">${mood.praiseNum}</span><br>
            <div style="margin-left: 350px">
                   <a id="praise" href="/mood/${mood.id}/praise?userId=${mood.userId}"></a>
                
            </div>
        </c:forEach>
    </div>
    </body>
    <script></script>
    </html>
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    进行测试后,浏览器输入80/mood/findAll:
    在这里插入图片描述

    传统点赞实现

    设计思路示例图:
    在这里插入图片描述
    service层主要做两件事:1.保存点赞用户和被点赞的关联关系,关系保存在rel表中 2.更新说说点赞数。
    service层处理过程中,请求数据库获取连接,执行相关的数据库操作归还数据库连接,最终返回数据给用户。
    如果在高并发情况下,如果半小时点赞数高达20万,那么QPS高达111(QPS=每秒请求数/事务数量),也就是说后端服务每秒创建111个线程来处理点赞请求。数据库的连接数量有限的会导致响应时间长,处理慢,这就是传统实现的弊端。

    代码实现:
    MoodMapper.xml:

    <select id="findById" resultMap="moodMap">
            select
            <include refid="table_column"/>
            from mood
            <where>
                id = #{id}
            where>
        select>
    
        <update id="update">
            update mood
            <set>
                <if test="mood.content != null and mood.content != ''">
                    content = #{mood.content},
                if>
                <if test="mood.praiseNum != null and mood.praiseNum != ''">
                    praise_num = #{mood.praiseNum},
                if>
            set>
            WHERE id = #{mood.id}
        update>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    findById用来根据id查询说说实体 mood,update用来更新说说数据。
    在MoodDao.java添加:

    @Repository
    public interface MoodDao {
        List<Mood> findAll();
        boolean update(@Param("mood") Mood mood);
    
        Mood findById(String id);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在MoodService.java添加:

    public interface MoodService {
        //传统查询
        List<MoodDTO> findAll();
    
        //传统点赞
        boolean praiseMood(String userId, String moodId);
    
        boolean update(@Param("mood") Mood mood);
    
        Mood findById(String id);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    MoodService 接口主要添加三个方法,praiseMood()处理用户点赞,update()更新说说内容,findById()查说说:
    向MoodServiceImpl.java添加:

    @Service
    public class MoodServiveImpl implements MoodService {
        @Resource
        private MoodDao moodDao;
        @Resource
        private UserDao userDao;
    
        @Resource
        private UserMoodPraiseRelDao userMoodPraiseRelDao;
       // DTO处理代码已经省略
      
        public boolean praiseMood(String userId, String moodId) {
            //保存关联关系
            UserMoodPraiseRel userMoodPraiseRel = new UserMoodPraiseRel();
            userMoodPraiseRel.setUserId(userId);
            userMoodPraiseRel.setMoodId(moodId);
            userMoodPraiseRelDao.save(userMoodPraiseRel);
            //更新说说的点赞数量
            Mood mood = this.findById(moodId);
            mood.setPraiseNum(mood.getPraiseNum() + 1);
            this.update(mood);
    
            return Boolean.TRUE;
        }
    
        public boolean update(Mood mood) {
            return moodDao.update(mood);
        }
    
        public Mood findById(String id) {
            return moodDao.findById(id);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    MoodController.java添加如下代码:

    @Controller
    @RequestMapping("/mood")
    public class MoodController {
        @Resource
        private MoodService moodService;
        // 省略find代码
        @GetMapping(value = "/{moodId}/praise")
        public String praise(Model model, @PathVariable(value = "moodId") String moodId,
                             @RequestParam(value = "userId") String userId) {
            boolean isPraise = moodService.praiseMood(userId, moodId);
            List<MoodDTO> moodDTOList = moodService.findAll();
            model.addAttribute("moods", moodDTOList);
            model.addAttribute("isPraise", isPraise);
            return "mood";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    mood.jsp代码不变。
    下面进行测试:
    输入80/mood/findAll:
    点击 赞 会将点赞数+1
    在这里插入图片描述
    注意此时url已经改变。

    集成Redis 缓存

    传统的点赞功能出现的问题很多:1. 高并发请求下,服务器频繁创建线程 2. 高并发请求下,数据库连接池连接数有限 3.高并发请求下,点赞功能是同步处理
    我们引入Redis缓存,每次点赞请求不是直接和MySQL数据库进行交互,而是和Redis进行交互,即把点赞相关的数据保存到Redis缓存,最后通过Quartz 创建定时计划,再把缓存的数据保存到数据库中。
    在这里插入图片描述

    Redis使用

    启动redis服务器:redis-server.exe.打开redis-cli.exe 进入Redis客户端:
    端口是6379,固定为Redis端口。
    字符串类型的增删改查:

    在这里插入图片描述

    List 集合的增删改查:
    在这里插入图片描述
    Set集合的增删改查:
    在这里插入图片描述
    Hash 集合的增删改查:
    在这里插入图片描述
    可以在java写个程序测试redis连接性:
    使用Java操作Redis需要jedis-2.1.0.jar,下载地址:http://files.cnblogs.com/liuling/jedis-2.1.0.jar.zip

    import redis.clients.jedis.Jedis;
    
    public class TestRedis {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("127.0.0.1");
            jedis.set("namc","a1");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    集成Redis缓存

    首先在pom.xml引入所需要的依赖:

    
            <dependency>
                <groupId>org.springframework.datagroupId>
                <artifactId>spring-data-redisartifactId>
                <version>${spring.redis.version}version>
            dependency>
    
            <dependency>
                <groupId>org.apache.commonsgroupId>
                <artifactId>commons-pool2artifactId>
                <version>${commons.version}version>
            dependency>
    
            <dependency>
                <groupId>redis.clientsgroupId>
                <artifactId>jedisartifactId>
                <version>${jedis.version}version>
            dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    接着在resources目录创建redis.properties:
    redis密码默认为空

    redis.maxIdle=300
    redis.minIdle=100
    redis.maxWaitMillis=3000
    redis.testOnBorrow=true
    redis.maxTotal=500
    redis.host=127.0.0.1
    redis.port=6379
    redis.password=
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    resources创建spring-redis.xml配置文件:

    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="
          http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
          http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
        <context:property-placeholder location="classpath:*.properties"/>
        
        <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
            <property name="maxIdle" value="${redis.maxIdle}">property>
            <property name="minIdle" value="${redis.minIdle}">property>
            <property name="maxTotal" value="${redis.maxTotal}">property>
            <property name="maxWaitMillis" value="${redis.maxWaitMillis}">property>
            <property name="testOnBorrow" value="${redis.testOnBorrow}">property>
        bean>
        
        <bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
            <property name="hostName" value="${redis.host}">property>
            <property name="port" value="${redis.port}">property>
            <property name="password" value="${redis.password}">property>
            <property name="poolConfig" ref="poolConfig">property>
        bean>
    
        <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
              p:connection-factory-ref="redisConnectionFactory">
        bean>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    在applicationContext.xml导入spring-redis.xml:

    <import resource="spring-redis.xml"/>
    
    • 1

    配置完成后,在test文件夹进行RedisTest.java进行测试:

    
    public class RedisTest extends  BaseJunit4Test{
        @Resource
        private RedisTemplate redisTemplate;
    
        @Test
        public void testRedis() {
            redisTemplate.opsForValue().set("name","ay1");
            String name =(String)redisTemplate.opsForValue().get("name");
            System.out.println(name);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    设计Redis 数据结构

    Redis数据结构采用多个Set类型的结合来存放。用Set集合来存放所有被点赞说说的id,key可以是自己约定的唯一的key即可,而value为所有被点赞的说说id,用n个set集合来存放每条说说用户点赞的记录。如果需要获取某个说说被点赞的次数,只要统计set集合size()即可。
    在这里插入图片描述
    在MoodService.java添加:

    	boolean praiseMoodForRedis(String userId, String moodId);
    
        List<MoodDTO> findAllForRedis();
    
    • 1
    • 2
    • 3

    在MoodServiceImpl.java:

    	@Resource
        private RedisTemplate redisTemplate;
    	// key命名规范: 项目名称+模块名称+具体内容
        private static final String PRAISE_HASH_KEY = "springmv.mybatis.boot.mood.id.list.key";
         public boolean praiseMoodForRedis(String userId, String moodId) {
            //1.存放到hashset中
            redisTemplate.opsForSet().add(PRAISE_HASH_KEY , moodId);
            //2.存放到set中
            redisTemplate.opsForSet().add(moodId,userId);
            return false;
        }
    	@Resource
        private UserService userService;
    
        public List<MoodDTO> findAllForRedis() {
            List<Mood> moodList = moodDao.findAll();
            if (CollectionUtils.isEmpty(moodList)) {
                return Collections.EMPTY_LIST;
            }
            List<MoodDTO> moodDTOList = new ArrayList<MoodDTO>();
            for (Mood mood : moodList) {
                MoodDTO moodDTO = new MoodDTO();
                moodDTO.setId(mood.getId());
                moodDTO.setUserId(mood.getUserId());
                //right = 总点赞数量 : 数据库的点赞数量 + redis的点赞数量
                moodDTO.setPraiseNum(mood.getPraiseNum() + redisTemplate.opsForSet().size(mood.getId()).intValue());
                moodDTO.setPublishTime(mood.getPublishTime());
                moodDTO.setContent(mood.getContent());
                //通过userID查询用户
                User user = userService.find(mood.getUserId());
                //用户名
                moodDTO.setUserName(user.getName());
                //账户
                moodDTO.setUserAccount(user.getAccount());
                moodDTOList.add(moodDTO);
            }
            return moodDTOList;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    MoodServiceImpl 类实现了MoodService 接口中的方法,处理逻辑较为简单:1.保存mood_id到Set集合 2.保存mood_id 和user_id 到Set集合中。
    注意这里有多少说说被点赞在Redis缓存就有多少个Set集合。
    在MoodController.java:
    这里为了简单起见,user_id就直接随机生成了。

    @GetMapping(value = "/{moodId}/praiseForRedis")
        public String praiseForRedis(Model model, @PathVariable(value = "moodId") String moodId,
                                     @RequestParam(value = "userId") String userId) {
            //方便使用,随机生成用户id
            Random random = new Random();
            userId = random.nextInt(100) + "";
    
            boolean isPraise = moodService.praiseMoodForRedis(userId, moodId);
            //查询所有的说说数据
            List<MoodDTO> moodDTOList = moodService.findAllForRedis();
            model.addAttribute("moods", moodDTOList);
            model.addAttribute("isPraise", isPraise);
            return "mood";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    将mood.jsp的点赞路径改一下:

    <a id="praise" href="/mood/${mood.id}/praiseForRedis?userId=${mood.userId}">a>
    
    • 1

    现在运行80/mood/findAll:
    左边是相应的mysql:发现确实是不一样的数据(说明此时做到了缓存)
    在这里插入图片描述
    当你再次输入80/mood/findAll的时候,你会发现点赞数还是143,但是你点击点赞的时候就会变成156,说明数据在redis做到了缓存:
    在这里插入图片描述
    下面要做的是定时从redis读数据进入数据库保存
    可以用Test进行测试:

     @Test
        public void testRedis() {
            //redisTemplate.opsForValue().set("htf", "ayccc");
            Set<String> set = redisTemplate.opsForSet().members("springmv.mybatis.boot.mood.id.list.key");
            System.out.println("value of name is:" + set);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    集成 Quartz定时器

    Quartz是一个Java 编写的开源任务调度的框架,通过触发器设置作业定时运行规则,控制作业的运行时间。定时器作用很多,比如,定时发送信息和定时生成报表等。
    Quartz 框架主要核心组件包括调度器、触发器和作业。调度器作为作业的总指挥,触发器作为作业的操作者,作业为应用的功能模块。
    在这里插入图片描述
    Job 是一个接口,只有一个execute,被调度的作业需要实现该接口中execute()。
    下面在ay\job创建PraiseDataSaveDBJob.java:

    
    @Component
    
    @Configurable
    // 相当于配置文件,被Spring 扫描初始化
    @EnableScheduling
    // 开启对计划任务的支持,在执行的任务上注解@Scheduled,声明是一个计划任务。
    public class PraiseDataSaveDBJob {
    
        //每5秒执行一次
        @Scheduled(cron = "*/60 * *  * * * ")
        public void savePraiseDataToDB() {
            System.out.println("run .....");
        }
    
        @Resource
        private RedisTemplate redisTemplate;
        private static final String PRAISE_HASH_KEY = "springmv.mybatis.boot.mood.id.list.key";
        @Resource
        private UserMoodPraiseRelService userMoodPraiseRelService;
        @Resource
        private MoodService moodService;
    
        //每10秒执行一次,真实项目当中,我们可以把定时器的执行计划时间设置长一点
        //比如说每天晚上凌晨2点跑一次。
        @Scheduled(cron = "*/10 * *  * * * ")
        public void savePraiseDataToDB2() {
    
            //获取所有被点赞的说说id
            Set<String> moods = redisTemplate.opsForSet().members(PRAISE_HASH_KEY);
            if (CollectionUtils.isEmpty(moods)) {
                return;
            }
            for (String moodId : moods) {
                if (redisTemplate.opsForSet().members(moodId) == null) {
                    continue;
                } else {
                    //通过说说id获取所有点赞的用户id列表
                    Set<String> userIds = redisTemplate.opsForSet().members(moodId);
                    if (CollectionUtils.isEmpty(userIds)) {
                        continue;
                    } else {
                        for (String userId : userIds) {
                            UserMoodPraiseRel userMoodPraiseRel = new UserMoodPraiseRel();
                            userMoodPraiseRel.setMoodId(moodId);
                            userMoodPraiseRel.setUserId(userId);
                            //保存说说与用户关联关系
                            userMoodPraiseRelService.save(userMoodPraiseRel);
                        }
                        Mood mood = moodService.findById(moodId);
                        //更新说说点赞数量
                        //说说的总点赞数量 = redis 点赞数量 + 数据库的点赞数量
                        mood.setPraiseNum(mood.getPraiseNum() + redisTemplate.opsForSet().size(moodId).intValue());
                        moodService.update(mood);
                        //清除缓存数据
                        redisTemplate.delete(moodId);
                    }
                }
            }
            //清除缓存数据
            redisTemplate.delete(PRAISE_HASH_KEY);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    下面开始测试:输入80/mood/findAll,点击点赞按钮会发现10秒后数据库内容将进行更新,注意我开启定时的时候会将缓存数据删除。

    集成ActiveMQ

    本次解决的是对点赞功能同步处理。消息队列的特点:异步、削峰、解耦。解决方案:
    把数据放到消息队列叫做生产者
    从消息队列里边取数据叫做消费者
    在这里插入图片描述
    ActiveMQ安装
    MQ:MessageQueue,消息队列,是一个消息的接收和转发的容器,用于消息推送。ActiveMQ是一个开源的消息系统,完全采用Java实现,所以很好支持JMS规范。
    直接去官网下载:ActiveMQ
    进入win64包打开activemq.bat:
    在这里插入图片描述
    浏览器输入http://localhost:8161/admin/ 账号密码均为admin
    在这里插入图片描述

    集成ActiveMQ

    首先在pom.xml引入依赖:

    
            <dependency>
                <groupId>org.springframeworkgroupId>
                <artifactId>spring-jmsartifactId>
                <version>${spring.version}version>
            dependency>
    
            <dependency>
                <groupId>org.apache.activemqgroupId>
                <artifactId>activemq-allartifactId>
                <version>5.11.2version>
                <exclusions>
                    <exclusion>
                        <artifactId>spring-contextartifactId>
                        <groupId>org.springframeworkgroupId>
                    exclusion>
                    <exclusion>
                        <groupId>org.apache.geronimo.specsgroupId>
                        <artifactId>geronimo-jms_1.1_specartifactId>
                    exclusion>
                exclusions>
            dependency>
    
            <dependency>
                <groupId>javax.jmsgroupId>
                <artifactId>javax.jms-apiartifactId>
                <version>2.0.1version>
            dependency>
            
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在resources 目录下创建Active MQ配置文件spring-jms.xml:

    
    <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
           xmlns:jms="http://www.springframework.org/schema/jms"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                            http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/context/spring-context-4.0.xsd
    				        http://www.springframework.org/schema/jms
    				        http://www.springframework.org/schema/jms/spring-jms-4.0.xsd">
        <bean id="connectionFactory"
              class="org.springframework.jms.connection.CachingConnectionFactory">
            <description>JMS连接工厂description>
            <property name="targetConnectionFactory">
                <bean class="org.apache.activemq.spring.ActiveMQConnectionFactory">
                    <property name="brokerURL" value="${activemq_url}"/>
                    <property name="userName" value="${activemq_username}"/>
                    <property name="password" value="${activemq_password}"/>
                bean>
            property>
            <property name="sessionCacheSize" value="100"/>
        bean>
    
        
        <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
            <description>队列模式模型description>
            <constructor-arg ref="connectionFactory"/>
            <property name="receiveTimeout" value="10000"/>
    
            <property name="pubSubDomain" value="false"/>
        bean>
    
        
        
        <jms:listener-container destination-type="queue"
                                container-type="default" connection-factory="connectionFactory"
                                acknowledge="auto">
            
            <jms:listener destination="ay.queue.high.concurrency.praise"
                          ref="moodConsumer"/>
        jms:listener-container>
        
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    resources创建activemq.properties:

    ### active mq服务器地址
    activemq_url=tcp://localhost:61616
    ### 服务器用户名
    activemq_username=admin
    ### 服务器密码
    activemq_password=admin
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在applicationContext.xml 引入jms.xml:

        <import resource="spring-jms.xml"/>
    
    • 1

    ActiveMQ异步消费

    在上面集成了ActiveMQ消息中间件,同时开发了相关的配置文件,这一节主要利用ActiveMQ实现点赞功能的异步处理。
    在ay\mq目录下创建MoodProducer

    
    @Component
    public class MoodProducer {
    
        @Resource
        private JmsTemplate jmsTemplate;
    
        private Logger log = Logger.getLogger(this.getClass());
    
        public void sendMessage(Destination destination, final MoodDTO mood) {
            log.info("生产者--->>>用户id:" + mood.getUserId() + " 给说说id:" + mood.getId() + " 点赞");
            //mood实体需要实现Serializable序列化接口
            jmsTemplate.convertAndSend(destination, mood);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    提供sendMessage 用来发送消息,方法的第一个参数是destination,主要用来指定队列的名称,第二个参数就是mood说说实体。需要注意的是,MoodDTO需要实现序列化接口。
    在ay\mq创建MoodConsumer.java:

    
    public class MoodConsumer implements MessageListener {
    
        private static final String PRAISE_HASH_KEY = "springmv.mybatis.boot.mood.id.list.key";
    
        @Resource
        private RedisTemplate redisTemplate;
    
        private Logger log = Logger.getLogger(this.getClass());
    
        public void onMessage(Message message) {
            try {
                MoodDTO mood = (MoodDTO) ((ActiveMQObjectMessage) message).getObject();
                //1.存放到set中
                redisTemplate.opsForSet().add(PRAISE_HASH_KEY, mood.getId());
                //2.存放到set中
                redisTemplate.opsForSet().add(mood.getId(), mood.getUserId());
                log.info("消费者--->>>用户id:" + mood.getUserId() + " 给说说id:" + mood.getId() + " 点赞");
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    实现了MessageListener 接口,完成对消息的监听和接收,消息两种接收方法:同步接收和异步接收。
    修改MoodServiceImpl 的praiseMoodForRedis方法:

    public boolean praiseMoodForRedis(String userId, String moodId) {
            MoodDTO moodDTO = new MoodDTO();
            moodDTO.setUserId(userId);
            moodDTO.setId(moodId);
            moodProducer.sendMessage(destination, moodDTO);
    
    //        //1.存放到hashset中
    //        redisTemplate.opsForSet().add(PRAISE_HASH_KEY , moodId);
    //        //2.存放到set中
    //        redisTemplate.opsForSet().add(moodId,userId);
            return false;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    下面进行测试:输入80/mood/findAll:
    表明异步消费开发成功。
    在这里插入图片描述

    以上就是全部内容了。

  • 相关阅读:
    go语言接口之接口类型
    【Vue.js】使用ElementUI实现增删改查(CRUD)及表单验证
    手写编程语言-如何为 GScript 编写标准库
    IceRPC之调用管道与传出请求->快乐的RPC
    JMeter录制HTTPS脚本解决办法
    中国能源统计年鉴面板数据-分省市主要污染物排放指标(包含ECXEL2020年中国统计年鉴)
    FFmpeg入门详解之9:Audacity音频工具
    FFmpeg:打印音/视频信息(Meta信息)
    你应该知道的 7 个很棒的 Java 项目
    flex布局与float布局
  • 原文地址:https://blog.csdn.net/QAZJOU/article/details/127826806