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<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的信息到数据库,并变更评论表里的点赞数,插入或更新点赞记录表,确保每人对每条评论只有一条点赞记录数据。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容