实现了一个功能完善的在线论坛,旨在为校园内提供一个“畅所欲言”的论坛环境,项目分别实现了用户模块、登录模块、发帖模块、和点赞关注模块等。
主要的技术点:
登录注册功能:使用kaptcha去生成验证码,使用SpringMail完成注册
Redis优化验证码的保存,解决分布式session问题
使用拦截器拦截用户请求,将用户信息绑定在ThreadLocal上
构建Trie数据结构,实现对发表帖子评论的敏感词过滤
支持对帖子评论,也支持对评论进行回复
利用Redis的zset并结合Redis实现点赞关注的功能
用户点赞关注后,使用kafka实现异步的发送系统通知
构建工具:Apache Maven 集成开发工具: IntelliJ IDEA 2021 数据库:MySQL、Redis 应用服务器:Apache Tomcat 框架:Spring、SpringMVC、Mybatis、SpringBoot 版本控制工具:Git
工作环境的配置,IDE使用IntellijIDEA,JDK使用JDK8,具体框架以及MySQL,Redis,kafka的版本与配置,维持当下较新的版本,文章主要介绍主要模块后端代码的实现部分。
注册和登录功能是每个项目最基本的功能,实现的主要难点在于怎么解决分布式Session问题,密码安全问题,以及怎么优化登录的问题。
用户表实现
id | username | password | salt | type | status | activation_code | header_url | create_time |
---|
为了保证安全,密码不能明文的在网络中进行传输,也不能以明文的形式存到数据库中。 存在数据库的密码 = MD5( 密码 + salt ) 防止密码泄露,salt为随机字符串
// MD5 加密
public static String md5(String key) {
if (StringUtils.isBlank(key)) {
return null;
}
return DigestUtils.md5DigestAsHex(key.getBytes());
}
application.properties对SpringMail进行配置
# mailProperties
spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=wfb18324952938@sina.cn
spring.mail.password=4681082336c1****
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
MailClient实现方法,进行发送邮件操作
public void sendMail(String to, String subject, String content) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("发送邮件失败: " + e.getMessage());
}
}
由于Http是无状态的,每次的http请求之间信息不共享,为了保证用户每次请求不用重新输入账号密码,保存用户的登录状态,就会有session和cookie这样的机制,去保存用户登录信息,但是在分布式部署的时候就会存在session共享的一个问题。
现在网站基本是多台服务器分布式部署的,如果将用户信息存到session中,而session是存到服务器上,在分布式环境下,由于各个服务器主机之间的信息并不共享,将用户信息存到服务器1上,同一个用户的下一个请求过来的时候,由于nginx的负载均衡策略,去请求了服务器2,就找不到之前的session了。
解决办法:
将客户端会话数据不存到Session中而是存到数据库中
考虑到关系型数据库性能较慢,项目中采用的方式是存到redis中
利用Kaptcha类实现生成随机字符,生成图片(验证码功能)
public Producer kaptchaProducer(){
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width","100");
properties.setProperty("kaptcha.image.height","40");
properties.setProperty("kaptcha.textproducer.font.size","32");
properties.setProperty("kaptcha.textproducer.font.color","black");
properties.setProperty("kaptcha.textproducer.char.string","0123456789ABCDEFGHJKLMNOPQRSTUVWXYZ");
properties.setProperty("kaptcha.textproducer.char.length","4");
properties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
DefaultKaptcha kaptcha = new DefaultKaptcha();
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
本项目中先采用将用户登录信息存到数据库的login_ticket表中,后续采用存到redis中优化。 V1 将用户登录凭证ticket存到mysql的login_ticket表中 登陆成功的时候生成登录凭证,生成Loginticket往数据库login_ticket存,并且被设置为cookie,下次用户登录的时候会带上这个ticket,ticket是个随机的UUID字符串,有过期的时间expired和有效的状态status
LoginTicket表
id | user_id | ticket | status | expired |
---|
V2: 使用Redis优化登录模块
Key | Value |
---|---|
Kaptcha:owner | String |
直接将验证码字符串存到session当中,每次都是从session中获取验证码字符串的值在进行判断会出现分布式session的问题,比如说刷新验证码是一次请求,此次请求将验证码存到了服务器A的session当中,但在点击登录按钮,去触发登录请求s时,将此次请求转到了服务器B,而服务器B并没有存储验证码的session,就会出现无法判断的问题。
// 验证码的归属
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
// 将验证码存入Redis
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);
// 生成登录凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
// 舍弃将LoginTicket通过Mapper存入MySQL,转而放入Redis里
// loginTicketMapper.insertLoginTicket(loginTicket);
String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
// loginTicket会序列化为JSON字符串
redisTemplate.opsForValue().set(redisKey, loginTicket);
map.put("ticket", loginTicket.getTicket());
return map;
使用Redis缓存用户信息
处理每次请求的时候,都要根据登录凭证查询用户信息,访问的频率非常高(每次请求的时候需要根据凭证中的用户id查询用户)
查询User的时候,先尝试从缓存中取值,如果没有的话,就需要初始化,有些地方会改变用户数据,需要更新缓存,可以直接把该用户的缓存删除,下一次请求的时候发现没有用户的信息,就会重新查一次再放到缓存中
// 1.有限从缓存中取值
private User getCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}
// 2.取不到时初始化缓存数据
private User initCache(int userId) {
User user = userMapper.selectById(userId);
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
return user;
}
// 3.当数据变更时,清除缓存数据
private void clearCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}
声明拦截器(实现HandleInterceptor)并在spring注解@Configuration中配置拦截信息
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
使用拦截器Interceptor来拦截所有的用户请求,判断请求中的cookie是否存在有效的ticket,如果有的话就将查询用户信息并将用户的信息写入ThreadLocal在本次请求中持有用户,将每个线程的threadLocal都存到一个叫做hostHolder的实例中,根据这个实例就可以在本次请求中全局任意的位置获取用户信息。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从cookie中获取凭证
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket != null) {
// 查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 检查凭证是否有效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
// 在本次请求中持有用户
hostHolder.setUser(user);
}
}
return true;
}
拦截器的应用:
使用拦截器:
在方法前标注自定义注解
拦截所有请求,只处理带有该注解的方法
// 元注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
在setting和upload等方法上,必须要求用户登录方可进行操作
@LoginRequired
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage() {
return "/site/setting";
}
@LoginRequired
@RequestMapping(path = "/upload", method = RequestMethod.POST)
public String uploadHeader(MultipartFile headerImage, Model model) {
前缀树:
发布帖子的时候需要对帖子的标题和内容进行敏感词,通过Trie实现敏感词过滤算法,过滤敏感词首先需要建立一颗字典树,并且读取一份保存敏感词的文本文件,并用文件初始化字典树,最后将敏感词作为一个服务,让需要过滤敏感词的服务进行调用即可。
// 替换符
private static final String REPLACEMENT = "***";
// 初始化根节点
private TrieNode rootNode = new TrieNode();
@PostConstruct // 标识这是初始化方法
public void init() {
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
) {
String keyword;
while ((keyword = reader.readLine()) != null) {
// 添加敏感词到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感词文件失败" + e.getMessage());
}
}
AJAX
使用HTMLUtils的方法来防止xss注入
评论表:
id | user_id | entity_type | entity_id | target_id | content | status | create_time |
---|
其中:
添加评论: (将添加评论和更新评论数量放在一个事务中)使用spring声明式事务管理@Transactional实现
AOP实现对service层所有的业务方法记录日志
Redis是一款基于键值对的NoSQL数据库,它的值支持多种数据结构:字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人。 同时,Redis还可以将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。
Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等。
访问Redis:
redisTemplate.opsForValue()
redisTemplate.opsForHash()
redisTemplate.opsForList()
redisTemplate.opsForSet()
redisTemplate.opsForZSet()
Key | Value |
---|---|
like:entity:entityType:entityId | Stringset(userId) |
value使用set集合存放userId是为了能看对谁点了赞。
点赞时同样需要记录点赞实体的用户id
某个用户收到的赞
Key | Value |
---|---|
like:user:userId | int |
使用Redis实现了每一个用户的粉丝列表,以及每一个用户的关注列表。 Redis set实现共同关注 :取交集 : sinter myset2 myset3
某个用户关注的实体
Key | Value |
---|---|
followee:userId:entityType | int |
使用zset以当前时间作为分数排序
某个实体拥有的粉丝
Key | Value |
---|---|
follower:entityType:entityId | zset(userId,now) |
在项目中,会有一些不需要实时执行但是是非常频繁的操作或者任务,为了提升网站的性能,可以使用异步消息的形式进行发送,再次消息队列服务器kafka来实现。
评论,点赞,关注等事件是非常频繁的操作,发送关系其的系统通知却并不是需要立刻执行的。主要实现分为下面几步:
// Event类
private String topic;
private int userId;
private int entityType;
private int entityUserId;
private Map<String,object> data;
//处理事件(发送事件)
public void fireEvent(Event event){
//将事件发布到指定的主题
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
}
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
public void handleCommentMessage(ConsumerRecord record) {
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
//发送站内的通知
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());//comment like follow
message.setCreateTime(new Date());
message.setContent(JSONObject.toJSONString(content));
System.out.println(content);
//调用messageService添加到数据库中
messageService.addMessage(message);
}
======= @TOC
实现了一个功能完善的在线论坛,旨在为校园内提供一个“畅所欲言”的论坛环境,项目分别实现了用户模块、登录模块、发帖模块、和点赞关注模块等。
主要的技术点:
登录注册功能:使用kaptcha去生成验证码,使用SpringMail完成注册
Redis优化验证码的保存,解决分布式session问题
使用拦截器拦截用户请求,将用户信息绑定在ThreadLocal上
构建Trie数据结构,实现对发表帖子评论的敏感词过滤
支持对帖子评论,也支持对评论进行回复
利用Redis的zset并结合Redis实现点赞关注的功能
用户点赞关注后,使用kafka实现异步的发送系统通知
构建工具:Apache Maven 集成开发工具: IntelliJ IDEA 2021 数据库:MySQL、Redis 应用服务器:Apache Tomcat 框架:Spring、SpringMVC、Mybatis、SpringBoot 版本控制工具:Git
工作环境的配置,IDE使用IntellijIDEA,JDK使用JDK8,具体框架以及MySQL,Redis,kafka的版本与配置,维持当下较新的版本,文章主要介绍主要模块后端代码的实现部分。
注册和登录功能是每个项目最基本的功能,实现的主要难点在于怎么解决分布式Session问题,密码安全问题,以及怎么优化登录的问题。
用户表实现
id | username | password | salt | type | status | activation_code | header_url | create_time |
---|
为了保证安全,密码不能明文的在网络中进行传输,也不能以明文的形式存到数据库中。 存在数据库的密码 = MD5( 密码 + salt ) 防止密码泄露,salt为随机字符串
// MD5 加密
public static String md5(String key) {
if (StringUtils.isBlank(key)) {
return null;
}
return DigestUtils.md5DigestAsHex(key.getBytes());
}
application.properties对SpringMail进行配置
# mailProperties
spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=wfb18324952938@sina.cn
spring.mail.password=4681082336c1****
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
MailClient实现方法,进行发送邮件操作
public void sendMail(String to, String subject, String content) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("发送邮件失败: " + e.getMessage());
}
}
由于Http是无状态的,每次的http请求之间信息不共享,为了保证用户每次请求不用重新输入账号密码,保存用户的登录状态,就会有session和cookie这样的机制,去保存用户登录信息,但是在分布式部署的时候就会存在session共享的一个问题。
现在网站基本是多台服务器分布式部署的,如果将用户信息存到session中,而session是存到服务器上,在分布式环境下,由于各个服务器主机之间的信息并不共享,将用户信息存到服务器1上,同一个用户的下一个请求过来的时候,由于nginx的负载均衡策略,去请求了服务器2,就找不到之前的session了。
解决办法:
将客户端会话数据不存到Session中而是存到数据库中
考虑到关系型数据库性能较慢,项目中采用的方式是存到redis中
利用Kaptcha类实现生成随机字符,生成图片(验证码功能)
public Producer kaptchaProducer(){
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width","100");
properties.setProperty("kaptcha.image.height","40");
properties.setProperty("kaptcha.textproducer.font.size","32");
properties.setProperty("kaptcha.textproducer.font.color","black");
properties.setProperty("kaptcha.textproducer.char.string","0123456789ABCDEFGHJKLMNOPQRSTUVWXYZ");
properties.setProperty("kaptcha.textproducer.char.length","4");
properties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
DefaultKaptcha kaptcha = new DefaultKaptcha();
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
本项目中先采用将用户登录信息存到数据库的login_ticket表中,后续采用存到redis中优化。 V1 将用户登录凭证ticket存到mysql的login_ticket表中 登陆成功的时候生成登录凭证,生成Loginticket往数据库login_ticket存,并且被设置为cookie,下次用户登录的时候会带上这个ticket,ticket是个随机的UUID字符串,有过期的时间expired和有效的状态status
LoginTicket表
id | user_id | ticket | status | expired |
---|
V2: 使用Redis优化登录模块
Key | Value |
---|---|
Kaptcha:owner | String |
直接将验证码字符串存到session当中,每次都是从session中获取验证码字符串的值在进行判断会出现分布式session的问题,比如说刷新验证码是一次请求,此次请求将验证码存到了服务器A的session当中,但在点击登录按钮,去触发登录请求s时,将此次请求转到了服务器B,而服务器B并没有存储验证码的session,就会出现无法判断的问题。
// 验证码的归属
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
// 将验证码存入Redis
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);
// 生成登录凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
// 舍弃将LoginTicket通过Mapper存入MySQL,转而放入Redis里
// loginTicketMapper.insertLoginTicket(loginTicket);
String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
// loginTicket会序列化为JSON字符串
redisTemplate.opsForValue().set(redisKey, loginTicket);
map.put("ticket", loginTicket.getTicket());
return map;
使用Redis缓存用户信息
处理每次请求的时候,都要根据登录凭证查询用户信息,访问的频率非常高(每次请求的时候需要根据凭证中的用户id查询用户)
查询User的时候,先尝试从缓存中取值,如果没有的话,就需要初始化,有些地方会改变用户数据,需要更新缓存,可以直接把该用户的缓存删除,下一次请求的时候发现没有用户的信息,就会重新查一次再放到缓存中
// 1.有限从缓存中取值
private User getCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}
// 2.取不到时初始化缓存数据
private User initCache(int userId) {
User user = userMapper.selectById(userId);
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
return user;
}
// 3.当数据变更时,清除缓存数据
private void clearCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}
声明拦截器(实现HandleInterceptor)并在spring注解@Configuration中配置拦截信息
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
使用拦截器Interceptor来拦截所有的用户请求,判断请求中的cookie是否存在有效的ticket,如果有的话就将查询用户信息并将用户的信息写入ThreadLocal在本次请求中持有用户,将每个线程的threadLocal都存到一个叫做hostHolder的实例中,根据这个实例就可以在本次请求中全局任意的位置获取用户信息。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从cookie中获取凭证
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket != null) {
// 查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 检查凭证是否有效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
// 在本次请求中持有用户
hostHolder.setUser(user);
}
}
return true;
}
拦截器的应用:
使用拦截器:
在方法前标注自定义注解
拦截所有请求,只处理带有该注解的方法
// 元注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
在setting和upload等方法上,必须要求用户登录方可进行操作
@LoginRequired
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage() {
return "/site/setting";
}
@LoginRequired
@RequestMapping(path = "/upload", method = RequestMethod.POST)
public String uploadHeader(MultipartFile headerImage, Model model) {
前缀树:
发布帖子的时候需要对帖子的标题和内容进行敏感词,通过Trie实现敏感词过滤算法,过滤敏感词首先需要建立一颗字典树,并且读取一份保存敏感词的文本文件,并用文件初始化字典树,最后将敏感词作为一个服务,让需要过滤敏感词的服务进行调用即可。
// 替换符
private static final String REPLACEMENT = "***";
// 初始化根节点
private TrieNode rootNode = new TrieNode();
@PostConstruct // 标识这是初始化方法
public void init() {
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
) {
String keyword;
while ((keyword = reader.readLine()) != null) {
// 添加敏感词到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感词文件失败" + e.getMessage());
}
}
AJAX
使用HTMLUtils的方法来防止xss注入
评论表:
id | user_id | entity_type | entity_id | target_id | content | status | create_time |
---|
其中:
添加评论: (将添加评论和更新评论数量放在一个事务中)使用spring声明式事务管理@Transactional实现
Aop实现对service层所有的业务方法记录日志
Redis是一款基于键值对的NoSQL数据库,它的值支持多种数据结构:字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人。 同时,Redis还可以将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。
Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等。
访问Redis:
redisTemplate.opsForValue()
redisTemplate.opsForHash()
redisTemplate.opsForList()
redisTemplate.opsForSet()
redisTemplate.opsForZSet()
Key | Value |
---|---|
like:entity:entityType:entityId | Stringset(userId) |
value使用set集合存放userId是为了能看对谁点了赞。
点赞时同样需要记录点赞实体的用户id
某个用户收到的赞
Key | Value |
---|---|
like:user:userId | int |
使用Redis实现了每一个用户的粉丝列表,以及每一个用户的关注列表。 Redis set实现共同关注 :取交集 : sinter myset2 myset3
某个用户关注的实体
Key | Value |
---|---|
followee:userId:entityType | int |
使用zset以当前时间作为分数排序
某个实体拥有的粉丝
Key | Value |
---|---|
follower:entityType:entityId | zset(userId,now) |
在项目中,会有一些不需要实时执行但是是非常频繁的操作或者任务,为了提升网站的性能,可以使用异步消息的形式进行发送,再次消息队列服务器kafka来实现。
评论,点赞,关注等事件是非常频繁的操作,发送关系其的系统通知却并不是需要立刻执行的。主要实现分为下面几步:
// Event类
private String topic;
private int userId;
private int entityType;
private int entityUserId;
private Map<String,object> data;
//处理事件(发送事件)
public void fireEvent(Event event){
//将事件发布到指定的主题
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
}
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
public void handleCommentMessage(ConsumerRecord record) {
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
//发送站内的通知
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());//comment like follow
message.setCreateTime(new Date());
message.setContent(JSONObject.toJSONString(content));
System.out.println(content);
//调用messageService添加到数据库中
messageService.addMessage(message);
}
origin/master 以上对本项目的相对比较重要的功能进行了总结,总体实现了用户模块、登录模块、发帖模块、和点赞关注模块等,部分功能进行了性能优化,使用技术栈比较适中。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。