前言
这篇文章是关于使用redis中的hash存储结构来保存点赞数据,并加入了定时任务框架quartz,让redis中的数据写入到mysql,这篇文章是对我上一篇实现点赞功能的优化和拓展,若有不足请大家指出。 (如何高效的使用java实现类似抖音的评论点赞列表(互动消息)以及点赞的基本操作 - 掘金 (juejin.cn))
本文借鉴了Redis如何高效实现点赞、取消点赞功能 - 掘金 (juejin.cn)这篇文章的思路,与自己的数据库和不同的需求结合完成。
为什么要使用redis实现点赞
由于点赞可能是用户频繁点击的操作,有时会发生误触等,就会多次发起请求,会增加mysql的压力。
实现功能
点赞时先把点赞数据存储到redis中,在每隔一段时间后把redis中的数据写入到mysql中,若用户查看自己的点赞列表或被点赞列表则直接执行该定时任务。
由于我认为点赞数据中应该有点赞时间这个字段,方便排序和展示,所以我在value中存了点赞状态和点赞时间两个状态,取出时需要进行字符串拆分,若有什么更好的方法请在评论区写出。
代码实现
因为我在数据库中没有点赞状态这个字段,数据库只会存储点赞的数据,未点赞或取消点赞等数据不会存储在数据库中,节约了资源。
数据库设计
数据库设计和实体类VO对象等在我的上一篇文章可以查看 (如何高效的使用java实现类似抖音的评论点赞列表(互动消息)以及点赞的基本操作 - 掘金 (juejin.cn))
edis实现类
redis中key和value的工具类行key拼接和value拆分)
public class RedisKeyUtils {
/**
* 评论缓存key
*/
public static final String CACHE_COMMENT_LIKE = "COMMENT_LIKE";
/**
* 用户被点赞数量key
*/
//public static final String CACHE_COMMENT_LIKE_COUNT = "COMMENT_LIKE_COUNT";
public static final String LIKE = "1";
public static final String UNLIKE = "0";
/**
* 拼接被点赞的用户id和点赞的人的id作为key。格式 222222::333333
*
* @param userId 用户id
* @param commentId 评论id
* @return {@link String}
*/
public static String getLikedKey(Integer userId, Integer commentId){
StringBuilder builder = new StringBuilder();
builder.append(userId);
builder.append("::");
builder.append(commentId);
return builder.toString();
}
/**
* 拼接点赞状态和点赞时间作为value。格式 1::2023-6-24
*
* @param status 状态
* @param time 时间
* @return {@link String}
*/
public static String getValue(String status, Timestamp time){
StringBuilder builder = new StringBuilder();
builder.append(status);
builder.append("&");
builder.append(time);
return builder.toString();
}
}
redis点赞
public void likes(Integer userId, Integer commentId) {
String likedKey = RedisKeyUtils.getLikedKey(userId, commentId);
Timestamp time = new Timestamp(System.currentTimeMillis());
String value = RedisKeyUtils.getValue(LIKE,time);
redisTemplate.opsForHash().put(CACHE_COMMENT_LIKE, likedKey, value);
}
redis取消点赞
public void unLikes(Integer userId, Integer commentId) {
String likedKey = RedisKeyUtils.getLikedKey(userId, commentId);
Timestamp time = new Timestamp(System.currentTimeMillis());
String value = RedisKeyUtils.getValue(UNLIKE,time);
redisTemplate.opsForHash().put(CACHE_COMMENT_LIKE, likedKey,value);
commentMapper.updateLikeNum(commentId,-1);
}
存储redis缓存中数据并删除缓存
由于数据库中没有点赞状态这个字段,所以使用了map进行数据传输
public Map<Like,String> getLikedDataFromRedis() {
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(CACHE_COMMENT_LIKE, ScanOptions.NONE);
Map<Like,String> likeStatusMap = new HashMap<>();
while (cursor.hasNext()){
Map.Entry<Object, Object> entry = cursor.next();
String key = (String) entry.getKey();
//分离出 userId,commentId
String[] split = key.split("::");
Integer userId = Integer.parseInt(split[0]);
Integer commentId = Integer.parseInt(split[1]);
String value = (String) entry.getValue() ;
//把点赞数据存入likeStatusMap
Timestamp time = new Timestamp(System.currentTimeMillis());
Like like = new Like();
like.setUserId(userId);
like.setCommentId(commentId);
like.setCreateTime(time);
likeStatusMap.put(like,value);
//存到 list 后从 Redis 中删除
redisTemplate.opsForHash().delete(CACHE_COMMENT_LIKE, key);
}
return likeStatusMap;
}
mysql实现类
是否点赞
@Override
public boolean isLike(Integer userId, Integer commentId) {
//生成评论点赞数据的key
String LikeKey = RedisKeyUtils.getLikedKey(userId,commentId);
//从redis中查看是否存在
if(redisTemplate.opsForHash().hasKey(CACHE_COMMENT_LIKE,LikeKey)){
String value = Objects.requireNonNull(redisTemplate.opsForHash().get(CACHE_COMMENT_LIKE, LikeKey)).toString();
String status = redisService.getStatus(value)[0];
return status.equals("1");
}else {
LambdaQueryWrapper<Like> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Like::getUserId,userId)
.eq(Like::getCommentId,commentId);
Long count = likeMapper.selectCount(queryWrapper);
if (count>0){
return true;
} else{
redisService.likes(userId,commentId);
return false;
}
}
}
点赞或取消点赞
public boolean like(Integer userId, Integer commentId) {
//生成评论点赞数据的key
String LikeKey = RedisKeyUtils.getLikedKey(userId,commentId);
//从redis中查看是否存在
if(redisTemplate.opsForHash().hasKey(CACHE_COMMENT_LIKE,LikeKey)){
String value = Objects.requireNonNull(redisTemplate.opsForHash().get(CACHE_COMMENT_LIKE, LikeKey)).toString();
String status = redisService.getStatus(value)[0];
if(status.equals("1")){
redisService.unLikes(userId,commentId);
return false;
}
if(status.equals("0")){
redisService.likes(userId,commentId);
return true;
}
}
//redis中不存在该点赞数据,从数据库中查找
LambdaQueryWrapper<Like> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Like::getCommentId,commentId)
.eq(Like::getUserId,userId);
Like like = likeMapper.selectOne(queryWrapper);
//判断点赞数据是否为空,若为空则没有点赞记录,添加点赞数据
if(like==null){
redisService.likes(userId,commentId);
return true;
}
//点赞数据不为空,说明已经点赞,则进行取消点赞
redisService.unLikes(userId,commentId);
return false;
}
redis写入数据库
这里对不同的状态进行了多次判断,是由于我的数据库中没有点赞状态这个字段,数据库内只会存储是点赞的数据。
public void transLikedFromRedis2DB() {
Map<Like,String> likedStatusMap = redisService.getLikedDataFromRedis();
for (Map.Entry<Like,String> entry : likedStatusMap.entrySet()) {
//获得点赞状态和点赞时间
String status = redisService.getStatus(entry.getValue())[0];
String time = redisService.getStatus(entry.getValue())[1];
//查询数据库中数据
LambdaQueryWrapper<Like> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Like::getCommentId,entry.getKey().getCommentId())
.eq(Like::getUserId,entry.getKey().getUserId());
Like like = likeMapper.selectOne(queryWrapper);
if(status.equals("1")){
//判断状态是点赞还是未点赞
if (like!=null){
//数据库内有数据
if(entry.getKey().getCreateTime()!=like.getCreateTime()){
like.setCreateTime(Timestamp.valueOf(time));
likeMapper.updateById(like);
}
}else{
//数据库内没有数据
likeMapper.insert(entry.getKey());
}
}else {
if (like!=null){
//数据库内有数据
likeMapper.deleteById(like.getLikeId());
}
}
}
}
定时任务实现
quartz配置类
@Configuration
public class QuartzConfig {
private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz";
@Bean
public JobDetail quartzDetail(){
return JobBuilder.newJob(LikeTask.class).withIdentity(LIKE_TASK_IDENTITY).storeDurably().build();
}
@Bean
public Trigger quartzTrigger(){
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(5) //一分钟执行一次
.repeatForever();
return TriggerBuilder.newTrigger().forJob(quartzDetail())
.withIdentity(LIKE_TASK_IDENTITY)
.withSchedule(scheduleBuilder)
.build();
}
}
task任务
@Slf4j
public class LikeTask extends QuartzJobBean {
@Resource
LikeService likeService;
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("LikeTask-------- {}", sdf.format(new Date()));
//将 Redis 里的点赞信息同步到数据库里
likeService.transLikedFromRedis2DB();
}
}
用户点赞列表和被点赞列表
用户点赞列表和被点赞列表与我的上一篇文章相同,但是在查询列表前加入了redis写入mysql的方法如何高效的使用java实现类似抖音的评论点赞列表(互动消息)以及点赞的基本操作 - 掘金 (juejin.cn)
//也就是在查询先执行该操作
//立即执行redis存入mysql的定时任务
transLikedFromRedis2DB();
总结
由于数据库内没有点赞状态这个字段,这段代码看起来更加复杂,大家有什么见解可以写在评论区。