本项目基于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');
在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;}
}
同样的道理,建立mood,user_mood_praise_rel类,和数据库字段类似,这里就不再放代码了。
数据库表和持久化类创建完成后,继续创建对应的Dao类和Mapper映射文件。
在ay.dao创建对应的Dao类:
这里注意Repository(一般用于接口层):该注解的作用不只是将类识别为Bean,同时它还能将所标注的类中抛出的数据访问异常封装为 Spring 的数据访问异常类型。 Spring本身提供了一个丰富的并且是与具体的数据访问技术无关的数据访问异常结构,用于封装不同的持久层框架抛出的异常,使得异常独立于底层的框架。
@Repository
public interface UserDao {User find(String id);
}
在mapper目录下创建user表对应的UserMapper.xml:
id,name,account
其中MoodDao.java:
@Repository
public interface MoodDao {List findAll();
}
以及UserMoodPraiseRelDao.java:
@Repository
public interface UserMoodPraiseRelDao {boolean save(@Param("userMoodPraiseRel")UserMoodPraiseRel userMoodPraiseRel);
}
相关的mapper.xml就不再重复写了。
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;}
}
ay.dto\UserDTO.java:
package com.ay.dto;import com.ay.model.User;
public class UserDTO extends User {
}
MoodDTO和UserDTO用于前端展示用的DTO对象,内容比较简单。
接着在service包下面:UserService.java:
public interface UserService {UserDTO find(String id);
}
MoodService.java:
public interface MoodService {//传统查询List findAll();
}
接着实现UserServiceImpl.java:
@Service
public class UserServiceImpl implements UserService {@Resourceprivate 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;}
}
MoodServiveImpl.java:
@Service
public class MoodServiveImpl implements MoodService {@Resourceprivate MoodDao moodDao;@Resourceprivate UserDao userDao;public List findAll() {List moodList = moodDao.findAll();return converModel2DTO(moodList);}private List converModel2DTO(List moodList) {if (CollectionUtils.isEmpty(moodList)) return Collections.EMPTY_LIST;List moodDTOList = new ArrayList();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;}
}
然后实现UserMoodPraiseRelService.java:
public interface UserMoodPraiseRelService {boolean save(UserMoodPraiseRel userMoodPraiseRel);
}
接着实现类:
@Service
public class UserMoodPraiseRelServiceImpl implements UserMoodPraiseRelService {@Resourceprivate UserMoodPraiseRelDao userMoodPraiseRelDao;public boolean save(UserMoodPraiseRel userMoodPraiseRel) {return userMoodPraiseRelDao.save(userMoodPraiseRel);}
}
ay.controller\UserController.java:
@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate UserService userService;
}
MoodController.java:
@Controller
@RequestMapping("/mood")
public class MoodController {@Resourceprivate MoodService moodService;@RequestMapping("/findAll")public String findAll(Model model) {List moodDTOList = moodService.findAll();model.addAttribute("moods",moodDTOList);return "mood";}
}
在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" %>
Getting Started: Serving Web Content
说说列表:
------------------------------------
用户:${mood.userName}
说说内容:${mood.content}
发表时间:${mood.publishTime}
点赞数:${mood.praiseNum}
进行测试后,浏览器输入80/mood/findAll:

设计思路示例图:

service层主要做两件事:1.保存点赞用户和被点赞的关联关系,关系保存在rel表中 2.更新说说点赞数。
service层处理过程中,请求数据库获取连接,执行相关的数据库操作归还数据库连接,最终返回数据给用户。
如果在高并发情况下,如果半小时点赞数高达20万,那么QPS高达111(QPS=每秒请求数/事务数量),也就是说后端服务每秒创建111个线程来处理点赞请求。数据库的连接数量有限的会导致响应时间长,处理慢,这就是传统实现的弊端。
代码实现:
MoodMapper.xml:
update moodcontent = #{mood.content}, praise_num = #{mood.praiseNum}, WHERE id = #{mood.id}
findById用来根据id查询说说实体 mood,update用来更新说说数据。
在MoodDao.java添加:
@Repository
public interface MoodDao {List findAll();boolean update(@Param("mood") Mood mood);Mood findById(String id);
}
在MoodService.java添加:
public interface MoodService {//传统查询List findAll();//传统点赞boolean praiseMood(String userId, String moodId);boolean update(@Param("mood") Mood mood);Mood findById(String id);}
MoodService 接口主要添加三个方法,praiseMood()处理用户点赞,update()更新说说内容,findById()查说说:
向MoodServiceImpl.java添加:
@Service
public class MoodServiveImpl implements MoodService {@Resourceprivate MoodDao moodDao;@Resourceprivate UserDao userDao;@Resourceprivate 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);}}
MoodController.java添加如下代码:
@Controller
@RequestMapping("/mood")
public class MoodController {@Resourceprivate 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 moodDTOList = moodService.findAll();model.addAttribute("moods", moodDTOList);model.addAttribute("isPraise", isPraise);return "mood";}
}
mood.jsp代码不变。
下面进行测试:
输入80/mood/findAll:
点击 赞 会将点赞数+1

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

启动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");}
}
首先在pom.xml引入所需要的依赖:
org.springframework.data spring-data-redis ${spring.redis.version} org.apache.commons commons-pool2 ${commons.version} redis.clients jedis ${jedis.version}
接着在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=
resources创建spring-redis.xml配置文件:
在applicationContext.xml导入spring-redis.xml:
配置完成后,在test文件夹进行RedisTest.java进行测试:
public class RedisTest extends BaseJunit4Test{@Resourceprivate RedisTemplate redisTemplate;@Testpublic void testRedis() {redisTemplate.opsForValue().set("name","ay1");String name =(String)redisTemplate.opsForValue().get("name");System.out.println(name);}
}
Redis数据结构采用多个Set类型的结合来存放。用Set集合来存放所有被点赞说说的id,key可以是自己约定的唯一的key即可,而value为所有被点赞的说说id,用n个set集合来存放每条说说用户点赞的记录。如果需要获取某个说说被点赞的次数,只要统计set集合size()即可。

在MoodService.java添加:
boolean praiseMoodForRedis(String userId, String moodId);List findAllForRedis();
在MoodServiceImpl.java:
@Resourceprivate 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;}@Resourceprivate UserService userService;public List findAllForRedis() {List moodList = moodDao.findAll();if (CollectionUtils.isEmpty(moodList)) {return Collections.EMPTY_LIST;}List moodDTOList = new ArrayList();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;}
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) {//方便使用,随机生成用户idRandom random = new Random();userId = random.nextInt(100) + "";boolean isPraise = moodService.praiseMoodForRedis(userId, moodId);//查询所有的说说数据List moodDTOList = moodService.findAllForRedis();model.addAttribute("moods", moodDTOList);model.addAttribute("isPraise", isPraise);return "mood";}
将mood.jsp的点赞路径改一下:
赞
现在运行80/mood/findAll:
左边是相应的mysql:发现确实是不一样的数据(说明此时做到了缓存)

当你再次输入80/mood/findAll的时候,你会发现点赞数还是143,但是你点击点赞的时候就会变成156,说明数据在redis做到了缓存:

下面要做的是定时从redis读数据进入数据库保存
可以用Test进行测试:
@Testpublic void testRedis() {//redisTemplate.opsForValue().set("htf", "ayccc");Set set = redisTemplate.opsForSet().members("springmv.mybatis.boot.mood.id.list.key");System.out.println("value of name is:" + set);}
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 .....");}@Resourceprivate RedisTemplate redisTemplate;private static final String PRAISE_HASH_KEY = "springmv.mybatis.boot.mood.id.list.key";@Resourceprivate UserMoodPraiseRelService userMoodPraiseRelService;@Resourceprivate MoodService moodService;//每10秒执行一次,真实项目当中,我们可以把定时器的执行计划时间设置长一点//比如说每天晚上凌晨2点跑一次。@Scheduled(cron = "*/10 * * * * * ")public void savePraiseDataToDB2() {//获取所有被点赞的说说idSet 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 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);}
}
下面开始测试:输入80/mood/findAll,点击点赞按钮会发现10秒后数据库内容将进行更新,注意我开启定时的时候会将缓存数据删除。
本次解决的是对点赞功能同步处理。消息队列的特点:异步、削峰、解耦。解决方案:
把数据放到消息队列叫做生产者
从消息队列里边取数据叫做消费者

ActiveMQ安装
MQ:MessageQueue,消息队列,是一个消息的接收和转发的容器,用于消息推送。ActiveMQ是一个开源的消息系统,完全采用Java实现,所以很好支持JMS规范。
直接去官网下载:ActiveMQ
进入win64包打开activemq.bat:

浏览器输入http://localhost:8161/admin/ 账号密码均为admin

首先在pom.xml引入依赖:
org.springframework spring-jms ${spring.version} org.apache.activemq activemq-all 5.11.2 spring-context org.springframework org.apache.geronimo.specs geronimo-jms_1.1_spec javax.jms javax.jms-api 2.0.1
在resources 目录下创建Active MQ配置文件spring-jms.xml:
JMS连接工厂 队列模式模型
resources创建activemq.properties:
### active mq服务器地址
activemq_url=tcp://localhost:61616
### 服务器用户名
activemq_username=admin
### 服务器密码
activemq_password=admin
在applicationContext.xml 引入jms.xml:
在上面集成了ActiveMQ消息中间件,同时开发了相关的配置文件,这一节主要利用ActiveMQ实现点赞功能的异步处理。
在ay\mq目录下创建MoodProducer
@Component
public class MoodProducer {@Resourceprivate 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);}
}
提供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";@Resourceprivate 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);}}
}
实现了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;}
下面进行测试:输入80/mood/findAll:
表明异步消费开发成功。

以上就是全部内容了。
上一篇:古代四字成语及其解释全集
下一篇:求别人帮忙。你用什么词