--- title: "04-Spring开发社交模块小记" created: 2025-12-02 tags: - 项目 aliases: - Spring开发社交模块小记 --- # Spring开发社交模块小记 [2021-12-28](https://blog.shawncoding.top/posts/26648891.html) 字数统计: 5.6k字 | 阅读时长≈ 25分 ## Spring开发社交模块小记 ## 一、引言 社交模块作为热点数据来说,可能会频繁改动字段,因此用Mysql是肯定不现实的,一般使用Redis。这里我以发表朋友圈动态为例,社交模块包括发表动态,点赞、评论、收藏、关注以及签到统计等模块,这里我简单实现了动态发表,点赞、评论这三个模块。 **关注功能模块**,使用Redis集合Set,一个人两个集合数据,定时更新到数据库 > > **点赞、收藏模块**,Set(点赞视频、点赞人评论)和Hash(like::url =1或0)结构都比较合适 > > > **评论模块**,可以选择list,用list和zset存储id,其他存储内容 > > > **登录统计、签到**,使用Redis的Bitmap > ## 二、数据库设计 数据库自行参考,可以考虑持久化到数据库。这里说一下我的设计思路: 动态分为视频动态和图片形式的动态,类似于抖音和微信朋友圈,该模块单独编写,需要信息从其他模块获取;评论为二级评论,后端包装后返回,评论可以点赞等操作;点赞优先经过Redis,若没有查询数据库 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 ``` | ``` create database if not exists lamp_social; use lamp_social; -- 评论表 drop table if exists social_comment; CREATE TABLE social_comment ( comment_id int(11) NOT NULL AUTO_INCREMENT COMMENT '评论表id', owner_id int(11) NOT NULL COMMENT '文章或视频id', user_id int(11) NOT NULL COMMENT '用户id', content text COMMENT '评论内容', star_num int(11) not null default 0 COMMENT '点赞数量', p_comment_id int(11) NOT NULL DEFAULT 0 COMMENT '若父评论则为0,默认一级评论;子评论对应其相应的评论父Id', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示未审核,1表示审核通过,2表示不通过', type int(2) NOT NULL DEFAULT 0 COMMENT '评论类型,默认为0,可以是对人、对资源、对视频等,暂时不用', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', deleted tinyint not null default 0 comment '数据删除位 0正常 1逻辑删除', primary key(comment_id) )AUTO_INCREMENT=1 ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表'; -- 个人动态表 drop table if exists social_dynamic; CREATE TABLE social_dynamic ( dynamic_id int(11) NOT NULL AUTO_INCREMENT COMMENT '动态id', dynamic_url varchar(5000) default '' COMMENT '视频地址,若是图片朋友圈,那么地址中间用|进行分隔', user_id int(11) NOT NULL COMMENT '用户id', content text COMMENT '朋友圈内容', star_num int(11) NOT NULL default 0 COMMENT '点赞数量', collection_num int(11) NOT NULL default 0 COMMENT '收藏数', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示未审核,1表示审核通过,2表示不通过', type int(2) NOT NULL DEFAULT 0 COMMENT '动态类型,默认为0,表示视频,1表示图片朋友圈,每个数字可以对应不同视频类型', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', deleted tinyint not null default 0 comment '数据删除位 0正常 1逻辑删除', primary key(dynamic_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人动态表'; -- 用户表可以添加一个点赞数字段,可选 drop table if exists social_user; CREATE TABLE social_user ( user_id int(11) NOT NULL comment '用户id', school_id int(11) NOT NULL comment '学校id', star_num int(11) NOT NULL default 0 COMMENT '点赞数量', focus_num int(11) NOT NULL default 0 COMMENT '关注数量', fan_num int(11) NOT NULL default 0 COMMENT '粉丝数量', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(user_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户点赞表'; -- 用户点赞表 drop table if exists social_user_like_dynamic; CREATE TABLE social_user_like_dynamic ( liked_id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', dynamic_id int(11) NOT NULL COMMENT '动态id', user_id int(11) NOT NULL COMMENT '用户id', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示点赞,1表示取消点赞', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(liked_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户点赞表'; -- 用户收藏表 drop table if exists social_user_collect_dynamic; CREATE TABLE social_user_collect_dynamic ( collection_id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', dynamic_id int(11) NOT NULL COMMENT '动态id', user_id int(11) NOT NULL COMMENT '用户id', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示收藏,1表示取消收藏', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(collection_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏表'; -- 用户关注与粉丝表 drop table if exists social_user_focus; CREATE TABLE social_user_focus ( focus_id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', user_id int(11) NOT NULL COMMENT '用户id', focus_user_id int(11) NOT NULL COMMENT '关注用户id', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示关注,1表示取消关注', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(focus_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关注与粉丝表'; ``` | ## 三、动态发表模块设计 ## 1、介绍 Feed流产品在我们手机APP中几乎无处不在,常见的Feed流比如微信朋友圈、新浪微博、今日头条等。对Feed流的定义,可以简单理解为只要大拇指不停地往下划手机屏幕,就有一条条的信息不断涌现出来。 大多数Feed流产品都包含两种Feed流,一种是基于算法推荐,另一种是基于关注(好友关系)。例如下图中的微博和知乎,顶栏的页卡都包含“关注”和“推荐”这两种。两种Feed流背后用到的技术差别会比较大(读扩散、写扩散)。 > 参考: ## 2、Redis结构选择 动态发布因为考虑到先缓存到Redis,在异步保存到MySql,因此动态主键使用Redis的自增函数,通过Redis生成MySql的动态主键; 对于动态数据的存储,我使用了list存储结构,新的数据从左边压入list,考虑到feed流查询,我还设置了一个伴生list列表,用来与动态同步存储主键值,首先通过lastid查询上一次浏览的值,查询list的index,在通过存储动态的列表返回一个列表;同时使用了读写锁,是为了保证原子性; 最后异步或定时检查列表长度,若过长可以从右边舍弃,或者设置列表过期时间,插入的时候重新刷新过期时间 ![](http://qnypic.shawncoding.top/blog/38cd060cb74444e481ace785d68270af.png) ## 四、评论模块设计 ## 1、介绍 mysql表字段,评论父表和字表存储在同一个数据表,根据`p_comment_id`字段分辨,返回的时候先查询出总的list,在使用JDK8的Stream流形成树形结构返回。ORM映射使用了Fluent MyBatis ,树形结构格式转换; ## 2、对象类型转换工具 首先创建转换工具类,这里先将对象转化为json,在通过解析json进行复制操作 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ``` | ``` import com.alibaba.fastjson.JSON; import java.util.List; /** * 两个对象或集合同名属性赋值 */ public class ObjectConversion { /** * 从List copy到List * @param list List * @param clazz B * @return List */ public static List copy(List list,Class clazz){ String oldOb = JSON.toJSONString(list); return JSON.parseArray(oldOb, clazz); } /** * 从对象A copy到 对象B * @param ob A * @param clazz B.class * @return B */ public static T copy(Object ob,Class clazz){ String oldOb = JSON.toJSONString(ob); return JSON.parseObject(oldOb, clazz); } } ``` | ## 3、多级评论树型拼接 我的VO类,主要用将数据库的评论拼装返回前端 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 ``` | ``` @Data @Accessors(chain = true) public class VideoCommentVO { /** * 评论表id */ private Integer commentId; /** * 创建时间 */ private Date createTime; /** * 评论内容 */ private String content; /** * 文章或视频id */ private Integer ownerId; /** * 若父评论则为0,默认一级评论;子评论对应其相应的评论父Id */ private Integer pCommentId; /** * 点赞数量 */ private Integer starNum; /** * 用户id */ private Integer userId; /** * 孩子 */ private List child; } ``` | 树形结构拼装,用了jdk8新特性 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ``` | ``` @Service public class VideoCommentService { @Autowired CommentMapper commentMapper; //就先二级评论吧 public List getVideoComment(Integer videoId) { CommentQuery query = new CommentQuery() .where().ownerId().eq(videoId).end() .where().state().eq(0).end() .where().deleted().eq(0).end(); List commentEntities = commentMapper.listEntity(query); //列表拷贝 List videoCommentVOList = ObjectConversion.copy(commentEntities, VideoCommentVO.class); //列表通过pcommentid进行分组 Map> collect = videoCommentVOList.stream().collect(Collectors.groupingBy(VideoCommentVO::getPCommentId)); //分组后遍历每一个数组设置孩子 videoCommentVOList.forEach( videoComment->videoComment.setChild(collect.get(videoComment.getCommentId())) ); System.out.println(videoCommentVOList); //找出父结点并返回,排序默认从小到大 List result = videoCommentVOList.stream() .filter(s -> s.getPCommentId().equals(0)) .sorted(Comparator.comparing(VideoCommentVO::getStarNum).reversed()) .collect(Collectors.toList()); return result; } } ``` | 如果遇到下面问题,回退版本号,我当时遇到了 | | | | --- | --- | | ``` 1 2 ``` | ``` // fastJson1.2.78版本会概率性出现该错误,回退到1.2.76即可 Comparison method violates its general contract ``` | ## 4、评论简单过滤 ![](http://qnypic.shawncoding.top/blog/16208973b157451caf70e64f7258585f.png) 简单原理如上图所示,创建结点类,里面包含是否是敏感词结束符,以及一个HashMap,哈希里key值存储的是敏感词的一个词,value指向下一个结点(即指向下一个词),一个哈希表中可以存放多个值,比如赌博、赌黄这两个都是敏感词。 敏感词文件存在在`resources`文件夹下,通过类加载器获取里面的敏感词。在springboot中,被`@PostConstruct`修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。`PostConstruct`在构造函数之后执行,`init()`方法之前执行。 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 ``` | ``` /** * 敏感词过滤器 * * @author Shawn * @date 2021年11月20日11:09 **/ @Component public class SensitiveFilter { private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class); /** * 将敏感词替换成 *** */ private static final String REPLACEMENT = "***"; private final TreeNode rootNode = new TreeNode(); /** * 被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。 * 初始化敏感词的结构树 */ @PostConstruct public void init(){ // 带资源的try语句,try块退出时,会自动调用res.close()方法,关闭资源。 try ( InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt"); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resourceAsStream)); ){ String keyword; while((keyword=bufferedReader.readLine())!=null){ this.addKeyWord(keyword); } } catch(IOException e){ logger.error("资源文件加载失败 ==> {}",e.getMessage()); } } /** * 将敏感词加入前缀树里 * @param keyword 敏感词 */ private void addKeyWord(@NotNull String keyword){ TreeNode tempNode = rootNode; for(int i = 0 ;i 0x9FFF); } /** * 树节点 * 敏感词结点类,与hash表类似,所有敏感词形成一个hash表,每个词语的每个词形成一个链表,用map指向下一个 */ private static class TreeNode{ // 关键词结束标识,默认是不是非结束结点 private boolean isKeywordEnd = false; // map的key存储一个敏感词,value指向下一个敏感词结点 HashMap nodeMap = new HashMap<>(); // 返回是否本次词语结束 public boolean isKeyWordEnd(){ return isKeywordEnd; } // 设置是否是结束词 public void setKeywordEnd(boolean keywordEnd){ isKeywordEnd = keywordEnd; } // 添加敏感词,key表示字符 public void addKeywordNode(Character c, TreeNode treeNode){ nodeMap.put(c,treeNode); } // 获取当前词是否是敏感词,若没有在表中,则返回null public TreeNode getKeywordNode(Character c){ return nodeMap.get(c); } } } ``` | ## 五、点赞模块设计 ## 1、问题描述 考虑到点赞是字段频繁变动的,用Mysql肯定不合适,使用需要使用Redis内存数据库。这里以动态点赞为例子,点赞模块需要解决的几个问题 - 用户对某个动态点赞/取消点赞 - 该动态获得了多少赞 - 用户是否已经点赞该动态 - 用户的总点赞数是多少 - 数据的持久化 ## 2、Redis数据结构选择 对于点赞来说,Set和Hash结构都可以选择。set中的值不能重复,是无序不重复的,Hash相当于Map集合,相当于key-Map,通常来存储经常变动的对象。对于点赞,两种结构都可以,根据业务自由选择 ### 1. Set结构存储 这里我选择了一种较为简单的存储方案,不过这种方案很难进行MySql持久化,用Set结构存储某视频点赞的用户,用String结构存储用户点赞数量,查询用户是否点赞只需查询用户是否在这个Set集合里,点赞/取消点赞加入/移除Set,查询某视频点赞数只需统计Set集合中的用户数量,两个存储结构为 - 视频点赞Set的存储结构 `like:dynamic:{dynamicType}:{dynamicId}={userId}` - 用户点赞数量的String存储结构 `like:user:{userId}=value` ### 2. Hash结构存储 这种Hash结构可以记录点赞人和被点赞产品,还有点赞状态(点赞/取消点赞设置值为1/0),还可以固定时间间隔取出 Redis 中所有点赞数据 - 视频点赞Hash的Key结构`like:dynamic:{dynamicType}`,里面的键值对为`{userId}::{dynamicId}=1` - 视频点赞数Hash的Key结构`like:count:dynamic`,里面的键值对为`{dynamicId}={count}` - 用户点咱叔Hash的Key结构`like:count:user`,里面的键值对为`{userId}={count}` ## 3、编码实现 ### 1. 配置redis 首先进行redis配置,实现序列化,否则不能正常显示 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ``` | ``` /** * @author Shawn * @date 2021年11月19日13:22 **/ @Configuration public class RedisConfig { // 编写自己的RedisTemplate @Bean @SuppressWarnings("all") public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); //序列化配置,默认会报错类型转换错误 //FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); FastJson2JsonRedisSerializer fastJsonRedisSerializer = new FastJson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); //String的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash采用String序列方式 template.setHashKeySerializer(stringRedisSerializer); // value采用jackson template.setValueSerializer(fastJsonRedisSerializer); // hash的value采用jackson template.setHashValueSerializer(fastJsonRedisSerializer); template.afterPropertiesSet(); return template; } } ``` | 创建`FastJson2JsonRedisSerializer`类 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ``` | ``` public class FastJson2JsonRedisSerializer implements RedisSerializer { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } private final Class clazz; public FastJson2JsonRedisSerializer(Class clazz) { super(); this.clazz = clazz; } /** * 序列化 */ @Override public byte[] serialize(T t) throws SerializationException { if (null == t) { return new byte[0]; } // 序列化时写入类型信息,默认为false,反序列化是需用到 // 如果序列化是没有加入类型信息SerializerFeature.WriteClassName,就会报错java.lang.ClassCastException return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } /** * 反序列化 */ @Override public T deserialize(byte[] bytes) throws SerializationException { if (null == bytes || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return (T) JSON.parseObject(str, clazz); } } ``` | ### 2. Redis工具类编写 新建RedisKeyUtil,进行key的拼接 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 ``` | ``` /** * 返回redis的key * 每个视频点赞用set进行保存,里面的用户数就是点赞数 * 用户的点赞数用k-v保存 * @author Shawn * @date 2021年11月19日13:52 **/ public class RedisKeyUtil { /** * redis分隔符 */ private static final String SPLIT = ":"; /** * 动态的获赞 */ private static final String PREFIX_DYNAMIC_LIKE = "like:dynamic"; /** * 动态的获赞数 */ private static final String PREFIX_DYNAMIC_LIKE_COUNT = "like:count:dynamic"; /** * 用户的获赞 */ private static final String PREFIX_USER_LIKE = "like:count:user"; /** * 返回redis视频点赞set key值 * 举例like:dynamic:1:11 * @param dynamicType 动态类型 * @param dynamicId 视频id * @return {@link String} */ public static String getDynamicLikeKey(int dynamicType,int dynamicId){ return PREFIX_DYNAMIC_LIKE + SPLIT + dynamicType + SPLIT + dynamicId; } /** * 返回redis视频点赞key值,使用hash * 举例hashkey为 like:dynamic:1 * value为 dynamicId:userId(1111::2222) * @param dynamicType 动态类型 * @return {@link String} */ public static String getDynamicLikeHashKey(int dynamicType){ return PREFIX_DYNAMIC_LIKE + SPLIT + dynamicType; } /** * 动态的获赞数的key值,使用hash * hashkey是动态id * 定时更新到数据库 * @return {@link String} */ public static String getDynamicLikeCountHashKey(){ return PREFIX_DYNAMIC_LIKE_COUNT ; } /** * hash的用户key (userId::dunamicId) * 4444::3333 */ public static String getDynamicUserLikeHashKey(int userId, int dynamicId){ return userId + SPLIT + SPLIT + dynamicId ; } /** * 用户总的点赞数key * 举例like:user:2 * @param userId 用户id * @return {@link String} */ public static String getUserLikeKey(int userId){ return PREFIX_USER_LIKE + SPLIT + userId; } /** * 用户总的点赞数key hash * 举例like:count:user * @return {@link String} */ public static String getUserLikeHashKey(){ return PREFIX_USER_LIKE; } } ``` | ### 3. 使用Set结构存储点赞 redis点赞模块service代码,自己写的,没有持久化,仅供参考 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 ``` | ``` @Autowired private RedisTemplate redisTemplate; /** * 某个视频用户是否点赞 * * @param dynamicType 动态类型 * @param userId 用户id * @param dynamicId 动态id * @return {@link Boolean} */ public Boolean getDynamicIsLikeByUser(int dynamicType, int userId,int dynamicId){ String dynamicLikeKey = RedisKeyUtil.getDynamicLikeKey(dynamicType,dynamicId); Boolean member = redisTemplate.opsForSet().isMember(dynamicLikeKey, userId); return member; } /** * @param userId 用户id * @param dynamicId 动态id * @return boolean */ public boolean putDynamicLikedByRedis(int dynamicType, int userId,int dynamicId){ redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { String dynamicLikeKey = RedisKeyUtil.getDynamicLikeKey(dynamicType,dynamicId); String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); Boolean member = redisTemplate.opsForSet().isMember(dynamicLikeKey, userId); //开启redis事务 redisTemplate.multi(); //如果已经点过赞了,就去除 if(Boolean.TRUE.equals(member)){ redisTemplate.opsForSet().remove(dynamicLikeKey,userId); redisTemplate.opsForValue().decrement(userLikeKey); }else{ //如果没有点赞,就点赞 redisTemplate.opsForSet().add(dynamicLikeKey,userId); redisTemplate.opsForValue().increment(userLikeKey); } // 返回每条成功执行的记录 redisTemplate.exec(); return true; } } ); return true; } /** * 获取视频点赞数 * @param dynamicType 动态类型 * @param dynamicId 动态id * @return {@link Long} */ public Long getDynamicLikeCount(int dynamicType,int dynamicId){ String dynamicLikeKey = RedisKeyUtil.getDynamicLikeKey(dynamicType,dynamicId); Long size = redisTemplate.opsForSet().size(dynamicLikeKey); return size; } /** * 这里有个小坑,如果没有记录,返回的是null * @param userId * @return */ public Integer getUserLikeCount(int userId){ String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); Integer result = (Integer) redisTemplate.opsForValue().get(userLikeKey); return result==null ? 0 : result; } ``` | ### 4. 使用Hash结构存储点赞 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 ``` | ``` @Autowired private RedisTemplate redisTemplate; /** * 某个视频用户是否点赞 * * @param dynamicType 动态类型 * @param userId 用户id * @param dynamicId 动态id * @return {@link Boolean} */ public Boolean getDynamicIsLikeByUser1(int dynamicType, int userId,int dynamicId){ try { String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId,dynamicId); // redis Set的值 String dynamicLikeSetKey = RedisKeyUtil.getDynamicLikeKey(dynamicType,dynamicId); // 首先查询缓存 Object redisResult = redisTemplate.opsForHash().get(dynamicLikeKey, dynamicLikeHashkey); // 查询二级缓存Set Boolean member = redisTemplate.opsForSet().isMember(dynamicLikeSetKey, userId); // 缓存没有查询数据库 if(redisResult == null && member == false){ log.warn("redis查询失败"); throw new SqlException("redis查询失败"); } }catch (SqlException e){ UserLikeDynamicQuery userLikeDynamicQuery = new UserLikeDynamicQuery() .where().dynamicId().eq(dynamicId).end() .where().userId().eq(userId).end() .where().state().eq(0).end(); UserLikeDynamicEntity one = userLikeDynamicMapper.findOne(userLikeDynamicQuery); if(null == one || one.getState()==0){ // 数据库查询不到或者状态位0,则没有点赞 return false; } }catch (Exception e){ log.error("点赞模块发生未知错误"); return false; } return true; } /** * 点赞,返回点赞数 * @param userId 用户id * @param dynamicId 动态id * @return boolean */ @SuppressWarnings("all") public int putDynamicLikedByRedis1(int dynamicType, int userId, int dynamicId){ String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId,dynamicId); // 首先查询缓存 Object redisResult = redisTemplate.opsForHash().get(dynamicLikeKey, dynamicLikeHashkey); if(null != redisResult && redisResult.equals(1)){ return -1; } Long result = (Long) redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId, dynamicId); String userLikeHashKey = RedisKeyUtil.getUserLikeHashKey(); // 点赞数记数 String dynamicLikeCountHashKey = RedisKeyUtil.getDynamicLikeCountHashKey(); redisTemplate.multi(); redisTemplate.opsForHash().put(dynamicLikeKey, dynamicLikeHashkey, 1); redisTemplate.opsForHash().increment(userLikeHashKey,String.valueOf(userId),1); // 自增 Long increment = redisTemplate.opsForHash().increment(dynamicLikeCountHashKey, String.valueOf(dynamicId), 1); List exec = redisTemplate.exec(); return exec.get(exec.size()-1); } }); return result.intValue(); } ``` | | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 ``` | ``` /** * 取消点赞,返回点赞数 * @param userId 用户id * @param dynamicId 动态id * @return boolean */ @SuppressWarnings("all") public int putDynamicDislikedByRedis1(int dynamicType, int userId,int dynamicId){ String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId,dynamicId); // 首先查询缓存 Object redisResult = redisTemplate.opsForHash().get(dynamicLikeKey, dynamicLikeHashkey); if(null == redisResult || redisResult.equals(0)){ return -1; } Long result = (Long) redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId, dynamicId); String userLikeHashKey = RedisKeyUtil.getUserLikeHashKey(); // 点赞数记数 String dynamicLikeCountHashKey = RedisKeyUtil.getDynamicLikeCountHashKey(); redisTemplate.multi(); redisTemplate.opsForHash().put(dynamicLikeKey, dynamicLikeHashkey, 0); redisTemplate.opsForHash().increment(userLikeHashKey,String.valueOf(userId),-1); // 自增 Long increment = redisTemplate.opsForHash().increment(dynamicLikeCountHashKey, String.valueOf(dynamicId), -1); List exec = redisTemplate.exec(); return exec.get(exec.size()-1); } }); return result.intValue(); } /** * 获取点赞数 * @param dynamicId 动态id * @return {@link Long} */ public Integer getDynamicLikeCount1(int dynamicId) throws Exception { Integer result; // 点赞数记数 String dynamicLikeCountHashKey = RedisKeyUtil.getDynamicLikeCountHashKey(); try { // Hash这里操作都需要String Object o = redisTemplate.opsForHash().get(dynamicLikeCountHashKey, String.valueOf(dynamicId)); // 如果缓存挂了,查询数据库 if(null == o){ log.info("redis查询失败"); throw new Exception("redis查询为空"); } result = (Integer) o; }catch (Exception e){ DynamicEntity byId = dynamicMapper.findById(dynamicId); if(null == byId){ throw new Exception("没有该id动态"); } Integer starNum = byId.getStarNum(); result=starNum; //将结果缓存到redis redisTemplate.opsForHash().put(dynamicLikeCountHashKey, String.valueOf(dynamicId), starNum); } return result; } ``` | ### 5. Quartz定时任务持久化 对于Hash结构存储的,还可以根据::分离出点赞人和被赞动态,拆分后进行持久化,配置好Quartz定时任务后,下面举例其中一个 | | | | --- | --- | | ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 ``` | ``` // 返回需要插入数据库的列表 public List getDBList(int dynamicType) { List userLikeDynamicEntityList = new ArrayList<>(); try { Cursor> cursor = redisTemplate.opsForHash() .scan(RedisKeyUtil.getDynamicLikeHashKey(dynamicType), ScanOptions.NONE); while(cursor.hasNext()){ Map.Entry entry = cursor.next(); String key = (String) entry.getKey(); //分离出 UserId,dynamicId String[] split = key.split("::"); Integer userId = Integer.valueOf(split[0]); Integer dynamicId = Integer.valueOf(split[1]); Integer value = (Integer) entry.getValue(); String dynamicLikeSetKey = RedisKeyUtil.getDynamicLikeKey(dynamicType, dynamicId); // 设置过期,如果热点数据将会一直存在,如果不是会自动删除 redisTemplate.expire(dynamicLikeSetKey,2, TimeUnit.DAYS); redisTemplate.opsForSet().add(dynamicLikeSetKey,userId); //组装成 UserLike 对象 UserLikeDynamicEntity userLikeDynamicEntity = new UserLikeDynamicEntity(); userLikeDynamicEntity.setUserId(userId).setDynamicId(dynamicId).setState(value); userLikeDynamicEntityList.add(userLikeDynamicEntity); //存到 list 后从 Redis 中删除 redisTemplate.opsForHash().delete(RedisKeyUtil.getDynamicLikeHashKey(dynamicType),key); } cursor.close(); }catch (Exception e){ log.error("cursor关闭异常"); } // 返回需要插入的列表 return userLikeDynamicEntityList; } ``` | --- 以上是我暂时做的,可能有很多问题,如果有问题,希望能够指出,后期不一定可放源码 -------------本文结束^-^感谢您的阅读------------- [ Vue Axios请求封装 ]([https://blog.shawncoding.top/posts/cbf77484.html)[](https://blog.shawncoding.top/posts/cbf77484.html)%5B) Spring容器加入bean的几种方式 ]() --- **项目分区导航**:⬅️ [[03-Blog模块|03-Blog模块]] | 04-Spring开发社交模块小记 | ➡️ [[05-拓展模块 评论点赞|05-拓展模块 评论点赞]]