Redis + Mysql 实现点赞功能
本文所有代码已在gitee开源 :freefancy1、需求首先说明功能需求:用户可以对动态点赞/取消点赞,用户可以查看已经点赞的动态,动态下显示点赞的数量和点赞的用户。2、分析传统数据库是可以实现这个需求的,动态表需要有一个字段like_num记录点赞数,另外需要一个表记录点赞,需要有的字段是用户iduser_id、动态idarticle_id、点赞状态status、创建时间gmt_create
本文所有代码已在gitee开源 :freefancy
1、需求
首先说明功能需求:用户可以对动态点赞/取消点赞,用户可以查看已经点赞的动态,动态下显示点赞的数量和点赞的用户。
2、分析
传统数据库是可以实现这个需求的,动态表需要有一个字段like_num
记录点赞数,另外需要一个表记录点赞,需要有的字段是用户iduser_id
、动态idarticle_id
、点赞状态status
、创建时间gmt_create
、最后修改时间gmt_modified
。
功能的实现也很简单不赘述。mysql应对查询可能还能应付,但点赞是一个很随意的操作,且多数为写操作,用户量一大数据库压力也会很大,可以考虑将点赞数据先写在缓存中,然后定期的写会数据库,虽有宕机丢失数据的风险,但对于点赞数据,还是可以容忍的。
因为redis有丰富的数据结构方便使用,这里就使用redis作为缓存。
这里直接贴出我的实现思路:
对于一次点赞/取消点赞,使用一个名为user_article_like_hash_${ userId}_${articleId}
的hash表保存,表中保存两个字段,status
和time
,分别记录本次操作的结果和操作时间。
这样做相比用一个hash表保存全部的点赞/取消点赞操作,一个hash表正好对应了数据库里的一条数据,不需要将业务需要的信息编码成value,也省去了解码的操作,编码和解码的开销是很大的;这样同时也十分好扩展业务,直接加字段就可以了。
另外为了记录点赞数,我们需要article_${articleId}_like_counter
来记录点赞数,点赞+1,取消点赞-1,这个值是可负的,动态的真实点赞数 = redis计数器 + mysql like_num
字段。
为了满足上面的设计,还需要一些辅助的数据结构来帮助我们记录信息:集合 article_set
记录article的id, 集合 user_article_like_set_${articleId}
来记录对某个article点赞或者取消点赞的用户id。
举例:点赞
这里举例点赞操作怎么进行,其余的操作会在代码中体现。
用户对动态点赞,将该动态的id放入article_set
,将该用户的id放入user_article_like_set_${articleId}
,将hash 表user_article_like_hash_${ userId}_${articleId}
中keystatus
的value设置为1,keytime
的value记录点赞的时间。
数据持久化
redis的数据定期写回mysql中,大致的操作是:利用article_set
和user_article_like_set_${articleId}
将所有的user_article_like_hash_${ userId}_${articleId}
写回mysql,将article_${articleId}_like_counter
与mysql中相应字段相加,最后将上述redis中的数据都删除。
代码
注:代码中使用的工具类可以在第一行的项目中找到。
服务层方法
package com.whut.idea.freefancy.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.whut.idea.freefancy.mapper.ArticleMapper;
import com.whut.idea.freefancy.mapper.UserArticleLikeMapper;
import com.whut.idea.freefancy.pojo.entity.Article;
import com.whut.idea.freefancy.pojo.entity.UserArticleLike;
import com.whut.idea.freefancy.service.UserArticleLikeService;
import com.whut.idea.freefancy.util.DateUtils;
import com.whut.idea.freefancy.util.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Set;
/**
* @author LiMing
* @date 2022/2/13 19:45
*/
@Service
public class UserArticleLikeServiceImpl extends ServiceImpl<UserArticleLikeMapper, UserArticleLike> implements UserArticleLikeService {
@Autowired
UserArticleLikeMapper userArticleLikeMapper;
@Autowired
ArticleMapper articleMapper;
/**
* 点赞标志
*/
private final static String LIKE = "1";
/**
* 未点赞标志
*/
private final static String UNLIKE = "0";
@Override
public void like(Long userId, Long articleId) {
String userId_str = userId.toString();
String articleId_str = articleId.toString();
//article id放入article set中
RedisUtils.sadd("article_set", articleId_str);
//user id 放入user_article_like_set_{$article_id} 集合中,保存为article点赞的所有用户
RedisUtils.sadd("user_article_like_set_" + articleId_str, userId_str);
//将点赞的相关信息写入key为user_article_like_hash_{$user_id}_{$article_id}的字段中
HashMap<String, String> map = new HashMap<>();
map.put("status", LIKE);
map.put("time", DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD_HH_MM_SS));
RedisUtils.hmset("user_article_like_hash_" + userId_str + "_" + articleId_str, map);
//article点赞计数器增1
RedisUtils.incr("article_" + articleId_str + "_like_counter");
}
@Override
public void cancelLike(Long userId, Long articleId) {
String userId_str = userId.toString();
String articleId_str = articleId.toString();
//article id放入article set中
RedisUtils.sadd("article_set", articleId_str);
//user id 放入user_article_like_set_{$article_id} 集合中,保存为article点赞的所有用户
RedisUtils.sadd("user_article_like_set_" + articleId_str, userId_str);
//将点赞的相关信息写入key为user_article_like_hash_{$user_id}_{$article_id}的字段中
HashMap<String, String> map = new HashMap<>(2);
map.put("status", UNLIKE);
map.put("time", DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD_HH_MM_SS));
RedisUtils.hmset("user_article_like_hash_" + userId_str + "_" + articleId_str, map);
//article点赞计数器减1
RedisUtils.decr("article_" + articleId_str + "_like_counter");
}
@Override
public boolean judgeLike(Long userId, Long articleId) {
String userId_str = userId.toString();
String articleId_str = articleId.toString();
//先判断redis中是否记录
String status = RedisUtils.hget("user_article_like_hash_" + userId_str + "_" + articleId_str, "status");
if (status != null) {
if (LIKE.equals(status)) {
return true;
}else if (UNLIKE.equals(status)) {
return false;
}
}
// 再判断mysql中
QueryWrapper<UserArticleLike> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId).eq("article_id", articleId).eq("status", 1);
UserArticleLike userArticleLike = userArticleLikeMapper.selectOne(queryWrapper);
if (userArticleLike != null) {
return true;
}else{
return false;
}
}
@Override
public long LikeCounter(Long articleId) {
//article的点赞数 = mysql表 + redis计数器
long count = 0;
QueryWrapper<Article> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id", articleId).eq("is_deleted", 0);
Article article = articleMapper.selectOne(queryWrapper);
if (article != null) {
count += article.getLikeNum();
}
String redisCount = RedisUtils.get("article_" + articleId.toString() + "_like_counter");
if (redisCount != null) {
count += Long.parseLong(redisCount);
}
return count;
}
@Override
public void persistence() {
Set<String> article_set = RedisUtils.smembers("article_set");
RedisUtils.del("article_set");
for (String articleId_str : article_set) {
Set<String> user_set = RedisUtils.smembers("user_article_like_set_" + articleId_str);
RedisUtils.del("user_article_like_set_" + articleId_str);
for (String userId_str : user_set) {
String status = RedisUtils.hget("user_article_like_hash_" + userId_str + "_" + articleId_str, "status");
String time = RedisUtils.hget("user_article_like_hash_" + userId_str + "_" + articleId_str, "time");
RedisUtils.del("user_article_like_hash_" + userId_str + "_" + articleId_str);
QueryWrapper<UserArticleLike> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", Long.parseLong(userId_str)).eq("article_id", Long.parseLong(articleId_str));
UserArticleLike userArticleLike = userArticleLikeMapper.selectOne(queryWrapper);
if (userArticleLike != null) {
//更新
userArticleLike.setStatus(Integer.parseInt(status));
userArticleLike.setGmtModified(DateUtils.parseDate(time));
userArticleLikeMapper.updateById(userArticleLike);
}else {
//新建
UserArticleLike like = new UserArticleLike();
like.setUserId(Long.parseLong(userId_str));
like.setArticleId(Long.parseLong(articleId_str));
like.setStatus(Integer.parseInt(status));
like.setGmtCreate(DateUtils.parseDate(time));
userArticleLikeMapper.insert(like);
}
}
//更新点赞数
Article article = articleMapper.selectById(Long.parseLong(articleId_str));
long increaseCount = Long.parseLong(RedisUtils.get("article_" + articleId_str + "_like_counter"));
if (article != null) {
article.setLikeNum(article.getLikeNum() + increaseCount);
articleMapper.updateById(article);
}
RedisUtils.del("article_" + articleId_str + "_like_counter");
}
}
}
控制层
package com.whut.idea.freefancy.controller;
import com.whut.idea.freefancy.common.response.ResponseJson;
import com.whut.idea.freefancy.pojo.entity.UserArticleLike;
import com.whut.idea.freefancy.service.UserArticleLikeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
/**
* @author LiMing
* @date 2022/2/13 19:50
*/
@Api(value = "UserArticleLikeController",tags = "点赞功能接口")
@RestController
@RequestMapping("/like")
public class UserArticleLikeController {
@Autowired
UserArticleLikeService userArticleLikeService;
@ApiOperation(value = "点赞", notes = "只需要传user id 和article id")
@PostMapping
public Object like(@RequestBody UserArticleLike userArticleLike) {
userArticleLikeService.like(userArticleLike.getUserId(), userArticleLike.getArticleId());
return ResponseJson.success();
}
@ApiOperation(value = "取消点赞", notes = "只需要传user id 和article id")
@PutMapping
public Object cancelLike(@RequestBody UserArticleLike userArticleLike) {
userArticleLikeService.cancelLike(userArticleLike.getUserId(), userArticleLike.getArticleId());
return ResponseJson.success();
}
@ApiOperation(value = "根据id查询动态的点赞次数")
@GetMapping("/likeCount/{articleId}")
public Object getTopic(@PathVariable Long articleId) {
long count = userArticleLikeService.LikeCounter(articleId);
return ResponseJson.success(count);
}
@ApiOperation(value = "根据user是否对article点赞")
@GetMapping("/judge/{userId}/{articleId}")
public Object judgeLike(@PathVariable @NotEmpty Long userId, @PathVariable @NotEmpty Long articleId) {
boolean judgeLike = userArticleLikeService.judgeLike(userId, articleId);
return ResponseJson.success(judgeLike);
}
}
定时任务
package com.whut.idea.freefancy.common.schedule;
import com.whut.idea.freefancy.service.UserArticleLikeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author LiMing
* @date 2022/2/14 21:55
*/
@Component
@EnableScheduling
public class CachePersistence {
private static final Logger LOG = LoggerFactory.getLogger(CachePersistence.class);
@Autowired
UserArticleLikeService userArticleLikeService;
/**
* 点赞信息持久化
* 固定时间间隔2小时
*/
@Scheduled(fixedRate = 7200000)
private void LikePersistence() {
userArticleLikeService.persistence();
LOG.info("定时任务:========== 点赞信息持久化:redis ---> mysql ==========");
}
}
更多推荐
所有评论(0)