Spring开发社交模块小记
字数统计: 5.6k字 | 阅读时长≈ 25分
Spring开发社交模块小记
一、引言
社交模块作为热点数据来说,可能会频繁改动字段,因此用Mysql是肯定不现实的,一般使用Redis。这里我以发表朋友圈动态为例,社交模块包括发表动态,点赞、评论、收藏、关注以及签到统计等模块,这里我简单实现了动态发表,点赞、评论这三个模块。
关注功能模块,使用Redis集合Set,一个人两个集合数据,定时更新到数据库
https://blog.csdn.net/INGNIGHT/article/details/107066022 https://www.cnblogs.com/linjiqin/p/12828315.html
点赞、收藏模块,Set(点赞视频、点赞人评论)和Hash(like::url =1或0)结构都比较合适
https://juejin.cn/post/6904816415912493069#heading-10 https://juejin.cn/post/6895185457110319118#heading-20 https://juejin.cn/post/6844903967168675847
评论模块,可以选择list,用list和zset存储id,其他存储内容
https://juejin.cn/post/6844903709374169102 https://blog.csdn.net/qq171563857/article/details/107406409 https://symonlin.github.io/2019/07/29/redis-1/
登录统计、签到,使用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 '视频地址,若是图片朋友圈,那么地址中间用 |
三、动态发表模块设计
1、介绍
Feed流产品在我们手机APP中几乎无处不在,常见的Feed流比如微信朋友圈、新浪微博、今日头条等。对Feed流的定义,可以简单理解为只要大拇指不停地往下划手机屏幕,就有一条条的信息不断涌现出来。
大多数Feed流产品都包含两种Feed流,一种是基于算法推荐,另一种是基于关注(好友关系)。例如下图中的微博和知乎,顶栏的页卡都包含“关注”和“推荐”这两种。两种Feed流背后用到的技术差别会比较大(读扩散、写扩散)。
2、Redis结构选择
动态发布因为考虑到先缓存到Redis,在异步保存到MySql,因此动态主键使用Redis的自增函数,通过Redis生成MySql的动态主键;
对于动态数据的存储,我使用了list存储结构,新的数据从左边压入list,考虑到feed流查询,我还设置了一个伴生list列表,用来与动态同步存储主键值,首先通过lastid查询上一次浏览的值,查询list的index,在通过存储动态的列表返回一个列表;同时使用了读写锁,是为了保证原子性;
最后异步或定时检查列表长度,若过长可以从右边舍弃,或者设置列表过期时间,插入的时候重新刷新过期时间

四、评论模块设计
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<A> copy到List<B> * @param list List<B> * @param clazz B * @return List<B> */ public static <T> List<T> copy(List<?> list,Class<T> 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> T copy(Object ob,Class<T> 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<VideoCommentVO> 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<VideoCommentVO> getVideoComment(Integer videoId) { CommentQuery query = new CommentQuery() .where().ownerId().eq(videoId).end() .where().state().eq(0).end() .where().deleted().eq(0).end(); List<CommentEntity> commentEntities = commentMapper.listEntity(query); //列表拷贝 List<VideoCommentVO> videoCommentVOList = ObjectConversion.copy(commentEntities, VideoCommentVO.class); //列表通过pcommentid进行分组 Map<Integer, List<VideoCommentVO>> collect = videoCommentVOList.stream().collect(Collectors.groupingBy(VideoCommentVO::getPCommentId)); //分组后遍历每一个数组设置孩子 videoCommentVOList.forEach( videoComment->videoComment.setChild(collect.get(videoComment.getCommentId())) ); System.out.println(videoCommentVOList); //找出父结点并返回,排序默认从小到大 List<VideoCommentVO> 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、评论简单过滤

简单原理如上图所示,创建结点类,里面包含是否是敏感词结束符,以及一个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<keyword.length();i++){ char c = keyword.charAt(i); // 进行空值判断,当多个敏感词首字母相同时,可以指向不同结点 TreeNode subNode = tempNode.getKeywordNode(c); if(subNode==null){ subNode = new TreeNode(); tempNode.addKeywordNode(c,subNode); } // 指向下一个结点 tempNode=subNode; } // 结尾设置结束符 tempNode.setKeywordEnd(true); } /* * 敏感词过滤器 * @param text 文本 * @return {@link String} / public String filter(String text){ if(StringUtils.isBlank(text)){ return null; } // 规则树,用来匹配敏感词 TreeNode tempNode = rootNode; // begin指针,指向文本中某个敏感词的第一位 int begin=0; // end指针,指向文本中某个敏感词的最后一位 int end = 0; StringBuilder sb = new StringBuilder(); while (end<text.length()){ char c = text.charAt(end); // 跳过符号(防止敏感词混合符号,比如 ☆赌☆博) if(this.isSymbol(c)){ // 若tempNode结点在开头,代表还没有匹配敏感字,这个特殊符号加入返回词,且 begin 指针指向下一个 if(tempNode == rootNode){ sb.append(c); begin++; } // 无论符号在开头还是在中间,指针 end 都会向下走一步 end++; continue; } // 检查子节点 tempNode = tempNode.getKeywordNode(c); if(tempNode==null){ // 以指针 begin 开头的字符串不是敏感词 sb.append(text.charAt(begin)); // 进入下一位的判断 end++; begin=end; // 这里需要把规则树重新指向根节点 tempNode=rootNode; }else if(tempNode.isKeyWordEnd()){ // 发现敏感词,将 begin~end 的字符串替换掉 sb.append(REPLACEMENT); end++; begin=end; tempNode=rootNode; }else{ // 敏感词匹配过程中 end++; } } // 将最后一批字符计入结果(如果最后一次循环的字符串不是敏感词,上述的循环逻辑不会将其加入最终结果) sb.append(text.substring(begin)); return sb.toString(); } /* * 是否是符号 */ private boolean isSymbol(Character c){ // 0x2E80~0x9FFF 是东亚文字范围 return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 |
五、点赞模块设计
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<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> 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 |
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 if(null one |
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 |
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<UserLikeDynamicEntity> getDBList(int dynamicType) { List<UserLikeDynamicEntity> userLikeDynamicEntityList = new ArrayList<>(); try { Cursor<Map.Entry<Object,Object>> cursor = redisTemplate.opsForHash() .scan(RedisKeyUtil.getDynamicLikeHashKey(dynamicType), ScanOptions.NONE); while(cursor.hasNext()){ Map.Entry<Object, Object> 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)%5B)
Spring容器加入bean的几种方式
](https://blog.shawncoding.top/posts/47ffe934.html)
项目分区导航:⬅️ 03-Blog模块 | 04-Spring开发社交模块小记 | ➡️ 05-拓展模块 评论点赞
💬 评论