java实现点赞功能示例

最近做了一个评论的点赞功能,感觉有必要记录一下。

思路:

点赞功能,看起来挺简单,但是做的高效稳定还是需要一些处理。
归纳思路如下:
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> list = new ArrayList>();
            List> insertLikeRecordList = new ArrayList>();
            for(String hashKey : hashKeyList) {
                String likeValue = mpJedis.hget(redisKey, hashKey);
                Map map = this.getHashMapByHashKey(hashKey);
                map.put("hashKey", hashKey);
                map.put("likeValue", likeValue);
                list.add(map);
                
                //查询点赞记录表是否存在该条记录,存在则更新,不存在则批量插入
                List> likeRecord = new ArrayList>();
                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> commentLevel1List = new ArrayList>();
                    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> commentLevel2List = new ArrayList>();
                    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 paramsMap = new HashMap();
                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 map : list) {
                mpJedis.hdel(redisKey, map.get("hashKey").toString());
            }
            
            num++;
            this.getLikeOrCancelLikeFromRedis(prefix, num);
        }
    }

    private String getLikeRecordValuesSql(List> insertLikeRecordList) {
        String sql = "";
        StringBuilder str = new StringBuilder();
        for(Map 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 getHashMapByHashKey(String hashKey) {
        Map map = new HashMap();
        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的信息到数据库,并变更评论表里的点赞数,插入或更新点赞记录表,确保每人对每条评论只有一条点赞记录数据。

本文只展示思路以及代码示例,数据库表就省略不建了,基本用到就两个表,一个是评论表(记录的点赞信息),一个是点赞或取消点赞记录表(记录的谁对谁在哪条评论里点的赞或取消点赞信息)。

你可能感兴趣的:(java实现点赞功能示例)