最近做了一个评论的点赞功能,感觉有必要记录一下。
思路:
点赞功能,看起来挺简单,但是做的高效稳定还是需要一些处理。
归纳思路如下:
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<String, Object> paramsMap = new HashMap<String, Object>();
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<String, Object> 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<String, Object> 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<String, String> allMap = new HashMap<String, String>();
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<String, String> likeMap = new HashMap<String, String>();
likeMap = mpJedis.hgetAll(redisKey);
if(!likeMap.isEmpty()) {
Set<String> hashKeyList = likeMap.keySet();
//把rediskey转化成list存入数据库记录,
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
List<Map<String, Object>> insertLikeRecordList = new ArrayList<Map<String,Object>>();
for(String hashKey : hashKeyList) {
String likeValue = mpJedis.hget(redisKey, hashKey);
Map<String, Object> map = this.getHashMapByHashKey(hashKey);
map.put("hashKey", hashKey);
map.put("likeValue", likeValue);
list.add(map);
//查询点赞记录表是否存在该条记录,存在则更新,不存在则批量插入
List<Map<String, Object>> likeRecord = new ArrayList<Map<String,Object>>();
likeRecord = appCommentsTaskMapper.getLikeRecordByMap(map);
String originLikeValue = "";
if(likeRecord.size() > 0) {
originLikeValue = null == likeRecord.get(0).get("is_like") ? "" : likeRecord.get(0).get("is_like").toString();
if(likeValue.equals(originLikeValue)) {//如果和上次操作一样则是废弃操作
continue;
}
try {
appCommentsTaskMapper.updateLikeRecordByMap(map);
} catch (Exception e) {
e.printStackTrace();
logger.info("更新点赞记录表出错 fail to updateLikeRecordByMap!");
throw new RuntimeException();
}
} else {
insertLikeRecordList.add(map);
}
//更新评论表的点赞数
if("1".equals(map.get("level").toString())) {//一级评论
List<Map<String, Object>> commentLevel1List = new ArrayList<Map<String,Object>>();
commentLevel1List = appCommentsTaskMapper.getCommentLevel1ListByMap(map);
if(commentLevel1List.size() > 0) {
int likeNum = Integer.parseInt(null == commentLevel1List.get(0).get("like_num") ? "0" : commentLevel1List.get(0).get("like_num").toString());
if("1".equals(likeValue)) {
map.put("likeNum", likeNum + 1);
} else if("0".equals(likeValue)){
map.put("likeNum", likeNum - 1 > 0 ? likeNum - 1 : 0);
}
try {
appCommentsTaskMapper.updateCommentLevel1ByMap(map);
} catch (Exception e) {
e.printStackTrace();
logger.info("更新一级评论表出错 fail to updateCommentLevel1ByMap!");
throw new RuntimeException();
}
}
} else if("2".equals(map.get("level").toString())) {//二级评论
List<Map<String, Object>> commentLevel2List = new ArrayList<Map<String,Object>>();
commentLevel2List = appCommentsTaskMapper.getCommentLevel2ListByMap(map);
if(commentLevel2List.size() > 0) {
int likeNum = Integer.parseInt(null == commentLevel2List.get(0).get("like_num") ? "0" : commentLevel2List.get(0).get("like_num").toString());
if("1".equals(likeValue)) {
map.put("likeNum", likeNum + 1);
} else if("0".equals(likeValue)){
map.put("likeNum", likeNum - 1 > 0 ? likeNum - 1 : 0);
}
try {
appCommentsTaskMapper.updateCommentLevel2ByMap(map);
} catch (Exception e) {
e.printStackTrace();
logger.info("更新二级评论表出错 fail to updateCommentLevel2ByMap!");
throw new RuntimeException();
}
}
}
}
//批量插入insertLikeRecordList
if(insertLikeRecordList.size() > 0) {
Map<String, Object> paramsMap = new HashMap<String, Object>();
paramsMap.put("likeRecordValuesSql", this.getLikeRecordValuesSql(insertLikeRecordList));
try {
appCommentsTaskMapper.insertLikeRecordByMap(paramsMap);
} catch (Exception e) {
e.printStackTrace();
logger.info("批量插入点赞记录表出错 fail to insertLikeRecordByMap!");
throw new RuntimeException();
}
}
//批量删除hashKey
for(Map<String, Object> map : list) {
mpJedis.hdel(redisKey, map.get("hashKey").toString());
}
num++;
this.getLikeOrCancelLikeFromRedis(prefix, num);
}
}
private String getLikeRecordValuesSql(List<Map<String, Object>> insertLikeRecordList) {
String sql = "";
StringBuilder str = new StringBuilder();
for(Map<String, Object> map : insertLikeRecordList) {
str.append("('" + map.get("moduleTypeId").toString() + "',");
str.append(map.get("topicId").toString() + ",'");
str.append(map.get("commentId").toString() + "','");
str.append(map.get("level").toString() + "','");
str.append(map.get("uid").toString() + "','");
str.append(map.get("userId").toString() + "','");
str.append(map.get("likeValue").toString() + "','");
str.append("0'),");
}
if(StringUtil.isNotBlank(str.toString())) {
sql = str.toString().substring(0, str.toString().length() - 1);
}
return sql;
}
private Map<String, Object> getHashMapByHashKey(String hashKey) {
Map<String, Object> map = new HashMap<String, Object>();
String moduleTypeId = hashKey.split("@")[0];
String topicId = hashKey.split("@")[1];
String commentId = hashKey.split("@")[2];
String level = hashKey.split("@")[3];
String uid = hashKey.split("@")[4];
String userId = hashKey.split("@")[5];
map.put("moduleTypeId", moduleTypeId);
map.put("topicId", Long.parseLong(topicId));
map.put("commentId", commentId);
map.put("level", level);
map.put("uid", uid);
map.put("userId", userId);
return map;
}
}
上面代码中定义一个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的信息到数据库,并变更评论表里的点赞数,插入或更新点赞记录表,确保每人对每条评论只有一条点赞记录数据。
本文只展示思路以及代码示例,数据库表就省略不建了,基本用到就两个表,一个是评论表(记录的点赞信息),一个是点赞或取消点赞记录表(记录的谁对谁在哪条评论里点的赞或取消点赞信息)。