最近做了一个评论的点赞功能,感觉有必要记录一下。
思路:
点赞功能,看起来挺简单,但是做的高效稳定还是需要一些处理。
归纳思路如下:
1.点赞接口要利用redis做点赞次数限制,比如一分钟之内最多点赞或取消点赞四次
2.点赞是很高频随兴的操作,最好不要直接操作数据库,先把点赞信息放入redis缓存,然后跑定时任务每15秒去同步到数据库,同步完之后把同步好的信息批量从redis删除。
3.把点赞信息放入redis缓存的时候选用hashset类型存储,结构大概为hset(rediskey,hashKey,value)的形式。
4.要保证定时任务同步的时候不会内存溢出,所以存储在redis里的时候要分页去存储,比如每5000条生成一个redisKey,然后递归去取值点赞记录同步到数据库,这样每次取值最大也就5000条,避免因数据量太大导致的内存溢出问题。
5.每个人对每条评论只会有一条数据,要么点赞要么取消点赞。
示例
点赞或取消点赞controller:
/**
* app点赞或取消点赞
* @param uid
* @param appId
* @param lang
* @param reqid
* @param paramsObj
* @return
*/
@PostMapping(value = "/comments/venucia/app/likeorcancellike")
public ResponseVO likeOrCancelLike(
@RequestHeader(required = true) String uid,
@RequestHeader(required = false) String appId,
@RequestHeader(required = false) String lang,
@RequestHeader(name = "reqid", required = false) String reqid,
@RequestBody JSONObject paramsObj) {
ResponseVO paramsVo = CommentsUtil.validLikeOrCancelLikeParamsObj(paramsObj, lang);
if(ErrorCodeEntity.ERROR_1.equals(paramsVo.getResult())){
logger.info("appLikeOrCancellike请求参数:{},reqid{},uid{}", paramsObj.toJSONString(), reqid, uid);
Map paramsMap = new HashMap();
paramsMap.put("moduleTypeId", paramsObj.getString("moduleTypeId"));//模块类型id
paramsMap.put("topicId", Long.parseLong(paramsObj.getString("topicId")));//文章或主题id
paramsMap.put("commentId", paramsObj.getString("commentId"));//评论id
paramsMap.put("level", paramsObj.getString("level"));//1为一级评论2为2级评论
paramsMap.put("userId", paramsObj.getString("userId"));//评论人id
paramsMap.put("type", paramsObj.getString("type"));//1是点赞,0是取消点赞
paramsMap.put("lang", AppFrameworkUtil.getLang(lang));
paramsMap.put("uid", uid);//点赞人id
ResponseVO resultVo = appCommentsService.likeOrCancelLike(paramsMap);
return resultVo;
}
return paramsVo;
}
点赞或取消点赞impl(把点赞信息存储在redis里,每个key最多存5000条数据):
public ResponseVO likeOrCancelLike(Map paramsMap) {
String lang = paramsMap.get("lang").toString();
String code = ErrorCodeEntity.ERROR_1;
String message = ErrorMsgLang.errorMsg(code, lang);
String moduleTypeId = paramsMap.get("moduleTypeId").toString();
String topicId = paramsMap.get("topicId").toString();
String commentId = paramsMap.get("commentId").toString();
String level = paramsMap.get("level").toString();
String uid = paramsMap.get("uid").toString();
//检验一分钟内同一用户对同一条评论不能超过四次
String validKey = Constant.REDIS_PREFIX + "likeOrCRequestNum:"
+ moduleTypeId + ":" + topicId + ":" + commentId + ":" + level + ":" + uid;
int requestNum = 1;
if(StringUtil.isBlank(mpJedis.get(validKey))) {
mpJedis.set(validKey, String.valueOf(requestNum));
mpJedis.expire(validKey, 60);
} else{
requestNum = Integer.parseInt(mpJedis.get(validKey).toString());
if(requestNum < 4) {
mpJedis.incrBy(validKey, 1l);
} else {
code = ErrorCodeEntity.ERROR_LIKETOOFAST_3206;
message = ErrorMsgLang.errorMsg(code, lang);
}
}
if(ErrorCodeEntity.ERROR_1.equals(code)) {
//把点赞信息存入redis
int num = 0;
try {
this.putLikeOrCancelLikeToRedis(num, paramsMap);
} catch (Exception e) {
e.printStackTrace();
logger.info("点赞或取消点赞出错 fail to likeOrCancelLike to Redis!");
code = ErrorCodeEntity.ERROR_RUNTIMEEXCEPTION_3001;
message = ErrorMsgLang.errorMsg(code, lang);
}
}
ResponseVO resultVo = new ResponseVO();
resultVo.setResult(code);
resultVo.setMsg(message);
return resultVo;
}
private void putLikeOrCancelLikeToRedis(int num, Map paramsMap) throws Exception {
String moduleTypeId = paramsMap.get("moduleTypeId").toString();
String topicId = paramsMap.get("topicId").toString();
String commentId = paramsMap.get("commentId").toString();
String level = paramsMap.get("level").toString();
String uid = paramsMap.get("uid").toString();
String userId = paramsMap.get("userId").toString();
String type = paramsMap.get("type").toString();
String value = "1";
if("0".equals(type)) {
value = "0";
}
String likeOrCancelLikeRedisKey = Constant.REDIS_PREFIX + "likeOrCancelLike" + num;
String hashKey = moduleTypeId + "@" + topicId + "@" + commentId + "@" + level + "@"
+ uid + "@" + userId;
Map allMap = new HashMap();
allMap = mpJedis.hgetAll(likeOrCancelLikeRedisKey);
if(allMap.isEmpty()) {
mpJedis.hset(likeOrCancelLikeRedisKey, hashKey, value);
} else if(allMap.size() < 5000) {
mpJedis.hset(likeOrCancelLikeRedisKey, hashKey, value);
} else if(allMap.size() >= 5000) {
num++;
this.putLikeOrCancelLikeToRedis(num, paramsMap);
}
}
定时任务AppCommentsTask.java
package com.ly.mp.iov.controller;
import java.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import com.ly.mp.iov.service.AppCommentsTaskService;
/**
* app评论服务定时任务
* @author zhaohy
*
*/
@Configuration //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class AppCommentsTask {
private static Logger logger = LoggerFactory.getLogger(AppCommentsTask.class);
@Autowired
AppCommentsTaskService appCommentsTaskService;
//3.添加定时任务,每15秒执行一次
@Scheduled(cron = "0/15 * * * * ?")
//或直接指定时间间隔,例如:5秒
//@Scheduled(fixedRate=5000)
private void likeCancelLikeTask() {
//System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
logger.info("appCommentsTaskBegin..." + LocalDateTime.now());
appCommentsTaskService.likeCancelLikeTask();
logger.info("appCommentsTaskEnd..." + LocalDateTime.now());
}
}
AppCommentsTaskService.java
package com.ly.mp.iov.service;
public interface AppCommentsTaskService {
void likeCancelLikeTask();
}
AppCommentsTaskServiceImpl.java
package com.ly.mp.iov.service.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ly.mp.iov.common.Constant;
import com.ly.mp.iov.mapper.AppCommentsTaskMapper;
import com.ly.mp.iov.service.AppCommentsTaskService;
import com.ly.mp.jedis.multi.MpJedis;
import jodd.util.StringUtil;
@Service("AppCommentsTaskService")
public class AppCommentsTaskServiceImpl implements AppCommentsTaskService {
private static Logger logger = LoggerFactory.getLogger(AppCommentsTaskService.class);
@Autowired
private MpJedis mpJedis;
@Autowired
private AppCommentsTaskMapper appCommentsTaskMapper;
@Transactional
public void likeCancelLikeTask() {
int num = 0;
String prefix = Constant.REDIS_PREFIX + "likeOrCancelLike";
try {
this.getLikeOrCancelLikeFromRedis(prefix, num);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
}
private void getLikeOrCancelLikeFromRedis(String prefix, int num) throws Exception {
String redisKey = prefix + num;
Map likeMap = new HashMap();
likeMap = mpJedis.hgetAll(redisKey);
if(!likeMap.isEmpty()) {
Set hashKeyList = likeMap.keySet();
//把rediskey转化成list存入数据库记录,
List
上面代码中定义一个likeRedisKey=前缀名+"like"+num,hashKey为:模块id+文章id+评论id+评论层级+点赞人id+被点赞人id,用@符号分隔,点赞的value为1,每5000条num++;
定义一个cancelLikeRedisKey=前缀名+"cancelLike"+num,hashKey为:模块id+文章id+评论id+评论层级+点赞人id+被点赞人id,用@符号分隔,取消点赞的value为0,每5000条num++;
定时任务递归依次同步likeRedisKey和cancelLikeRedisKey的信息到数据库,并变更评论表里的点赞数,插入或更新点赞记录表,确保每人对每条评论只有一条点赞记录数据。
本文只展示思路以及代码示例,数据库表就省略不建了,基本用到就两个表,一个是评论表(记录的点赞信息),一个是点赞或取消点赞记录表(记录的谁对谁在哪条评论里点的赞或取消点赞信息)。