文章目录

  • 功能需求
  • 1. dao层设计redis对应的key
  • 设计储存关注对象信息的健值对
  • key
  • value
  • 设计储存粉丝信息的健值对
  • key
  • value
  • 2. Service层处理关注和取关的业务
  • 1. 触发关注、取关事件-redis事务处理
  • ==opsForZSet().add(key, value)==
  • ==opsForZSet().remove(key, value)==
  • 2. 查询关注对象的数量
  • ==opsForZSet().size(key)==
  • 3. 查询当前对象的粉丝数量
  • 4. 查询当前用户是否对目标用户的关注状态
  • 3.Controller层处理请求
  • 1. 处理关注、取关异步请求
  • 2. 更新访问用户个人主页的请求
  • 4. View层处理模板页面
  • 1.处理关注、取关事件按钮
  • 按钮样式根据关注状态动态改变
  • 关注的显示根据关注状态动态显示
  • 关注事件对应的js文件定义
  • 在按钮标签前添加隐藏标签
  • ==$(btn).prev().val()==
  • 2. 显示指定用户关注了多少人
  • 3. 显示指定用户的粉丝数
  • 测试结果:


参考牛客网高级项目教程

狂神说Redis教程笔记

功能需求

redis存用户数据并检索 redis 用户相关信息存储技巧_redis存用户数据并检索

  • 1.在用户的个人信息页面,点击关注,可以关注该用户,并将关注数据用redis存储
  • 在关注状态下,再次点击,会取消关注
  • 统计该用户关注了多少人,以及粉丝有多少人
  • 与点赞功能类似,数据特点是数据量大、变化快、且数据字段较少,因此采用redis储存比较合适

1. dao层设计redis对应的key

  • 若A关注了B,则A是B的fans(粉丝),B是A的Followee(目标)。
  • 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体
  • 若关注的目标是帖子、题目等,也就是收藏,直接根据实体类型和id确定,今后开发收藏功能也可以复用

设计储存关注对象信息的健值对

key

  • 与点赞不同,点赞不需要查询统计我一共点了多少赞,故,不需要储存目标entityId,但关注需要
  • 故,关注需要指明实体类型,对人是关注,对其他实体则是是收藏
  • 关注需要指明当前用户的userId,这样方便被关注者统计多少粉丝,
  • 因为value不能放userId,要放被关注目标的id,这样,方便统计关注了多少人
// 实体类型为user时,为某人关注了某人
// 实体类型为帖子时,为某人收藏了某帖子
follow:target:userId:entityType

value

  • value用有序集合Zset储存关注目标的id,和分数,分数以关注时间表示
  • 这样,便于统计关注了多少实体,以及这些实体展示时,可以按照一定规则排序
/**
 * 某个用户关注的实体,要保存关注的实体类型,还要保存是谁关注的,方便被关注者统计,
 * 但也要指明被关注的对象具体id,放入value
 * follow:target:userId:entityType -> zset(entityId,now)
 * @param userId        当前用户的id
 * @param entityType    当前用户关注对象的实体类型
 * @return              储存关注对象信息的key
 */
public static String getFollowTarget(int userId, int entityType) {
    return PREFIX_FOLLOW_TARGET + SPLIT + userId + SPLIT + entityType;
}

设计储存粉丝信息的健值对

key

  • 与点赞逻辑类似,要储存某个实体的粉丝信息,即某个实体收到的关s注或收到的收藏的粉丝信息
  • 因此,要指明这个实体的类型和id
  • value中储存粉丝的id,因此能发出关注或收藏的主体只能是user类型,因此只需储存userId即可

value

  • 要储存粉丝的id,为今后方便将粉丝按照一定规则罗列出来,用zset还要储存一个对应的分数,用关注时间表示
/**
 * 某个实体的粉丝
 * follow:fans:entityType:entityId -> zset(userId,now)
 * @param entityType    要储存的实体类型
 * @param entityId      要储存的实体id
 * @return              返回储存实体类型粉丝信息的key
 */
public static String getFollowFans(int entityType, int entityId) {
    return PREFIX_FOLLOW_FANS + SPLIT + entityType + SPLIT + entityId;
}

2. Service层处理关注和取关的业务

1. 触发关注、取关事件-redis事务处理

  • 可以与之前的点赞逻辑一样,先判断当前用户是否关注了目的对象,根据关注状态处理不同的事件
  • 也可以结合前端设计模块触发不同的事件
  • 即关注与取关前端按钮不同点击不同按钮会触发不同事件
  • 无需在一个按钮触发后,进行查询判断,本例中采用此种策略
  • 注意,点击关注或取关后,目标对象的粉丝信息也会发生改变,因此,这两个事件应该放在一个事务中处理
opsForZSet().add(key, value)
opsForZSet().remove(key, value)
/**
 * 关注实体事件-被关注对象的粉丝信息也同时更新-增加redis事件处理
 * @param userId        当前用户id
 * @param entityType    关注对象类型
 * @param entityId      关注对象id
 */
public void follow(int userId, int entityType, int entityId) {
    redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String followTargetKey = RedisKeyUtil.getFollowTarget(userId, entityType);
            String followFans = RedisKeyUtil.getFollowFans(entityType, entityId);

            // 开启事务
            operations.multi();
            // 储存关注对象信息-关注对象的粉丝信息
            operations.opsForZSet().add(followTargetKey, entityId, System.currentTimeMillis());
            operations.opsForZSet().add(followFans, userId, System.currentTimeMillis());
            // 提交事务并返回
            return operations.exec();
        }
    });
}

/**
 * 取消关注实体事件-被关注对象的粉丝信息也同时更新-增加redis事件处理
 * @param userId        当前用户id
 * @param entityType    关注对象类型
 * @param entityId      关注对象id
 */
public void unFollow(int userId, int entityType, int entityId) {
    redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String followTargetKey = RedisKeyUtil.getFollowTarget(userId, entityType);
            String followFans = RedisKeyUtil.getFollowFans(entityType, entityId);

            // 开启事务
            operations.multi();
            // 储存关注对象信息-关注对象的粉丝信息
            operations.opsForZSet().remove(followTargetKey, entityId);
            operations.opsForZSet().remove(followFans, userId);
            // 提交事务并返回
            return operations.exec();
        }
    });
}

2. 查询关注对象的数量

opsForZSet().size(key)
  • 与opsForZSet().zCard(key)功能一样,查询成员数量
/**
 * 获取当前用户指定类型关注对象的数量
 * @param userId
 * @param entityType
 * @param entityId
 * @return
 */
public long findFollowTargetCnt(int userId, int entityType, int entityId) {
    String followTarget = RedisKeyUtil.getFollowTarget(userId, entityType);
    return redisTemplate.opsForZSet().size(followTarget);
}

3. 查询当前对象的粉丝数量

/**
 * 获取当前用户的粉丝数
 * @param entityType
 * @param entityId
 * @return
 */
public long findFollowFans(int entityType, int entityId) {
    String followFans = RedisKeyUtil.getFollowFans(entityType, entityId);
    return redisTemplate.opsForZSet().zCard(followFans);
}

4. 查询当前用户是否对目标用户的关注状态

opsForZSet().score(key, member)

  • 通过查询成员函数的分数是否存在,来判断menber是否在集合中
/**
 * 判断当前用户userid是否关注了目标对象entityId-通过判断目标对象的粉丝中有无当前对象
 * @param userId
 * @param entityType
 * @param entityId
 * @return
 */
public boolean hasFollowed(int userId, int entityType, int entityId) {
    String followTargetKey = RedisKeyUtil.getFollowTarget(userId, entityType);
    return redisTemplate.opsForZSet().score(followTargetKey, entityId) != null;
}

3.Controller层处理请求

1. 处理关注、取关异步请求

  • 要拦截判断是否登录
  • 从拦截器中的当前线程容器中获取登录用户的id
/**
 * 处理关注的异步请求
 * @param entityType    关注对象类型
 * @param entityId      关注对象id
 * @return
 */
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
    User user = hostHolder.getUser();
    if(user == null) {  
        throw new IllegalArgumentException("用户没有登录!");
    }
    followService.follow(user.getId(), entityType, entityId);
    return CommunityUtil.getJSONString(0, "关注成功!");
}

/**
 * 处理取消关注的异步请求
 * @param entityType
 * @param entityId
 * @return
 */
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType, int entityId) {
    User user = hostHolder.getUser();
    if(user == null) {  
        throw new IllegalArgumentException("用户没有登录!");
    }
    followService.unFollow(user.getId(), entityType, entityId);
    return CommunityUtil.getJSONString(0, "已取消关注!");
}

2. 更新访问用户个人主页的请求

  • 注意:获取当前用户信息时,要先判断是否为null,未登录状态也可以访问指定用户主页
// 获取当前用户对指定用户的关注状态
boolean hasFollowed = false;
if(hostHolder.getUser() != null) {
    hasFollowed = followService.hasFollowed(
        hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
model.addAttribute("hasFollowed", hasFollowed);
  • 整体代码如下:
/**
 * 访问指定用户个人主页的请求
 * @param userId    指定的用户id
 * @param model
 * @return
 */
@RequestMapping(value = "/profile/{userId}", method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
    // 先获取要访问的用户
    User user = userService.findUserById(userId);
    if(user == null) {
        throw new IllegalArgumentException("该用户不存在!");
    }
    // 将指定用户信息封装
    model.addAttribute("user", user);
    
    // 获取指定用户的点赞数量
    int likeCount = likeService.findUserLikeCount(userId);
    model.addAttribute("likeCount", likeCount);
    
    // 获取指定用户的关注对象数量
    long followTargetCnt = followService.findFollowTargetCnt(userId, ENTITY_TYPE_USER);
    model.addAttribute("followTargetCnt", followTargetCnt);
    
    // 获取指定用户的粉丝数量
    long followFans = followService.findFollowFans(ENTITY_TYPE_USER, userId);
    model.addAttribute("followFans", followFans);
    
   // 获取当前用户对指定用户的关注状态
    boolean hasFollowed = false;
    if(hostHolder.getUser() != null) {
        hasFollowed = followService.hasFollowed(
            hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
    }
    model.addAttribute("hasFollowed", hasFollowed);
    
    return "/site/profile";
}

4. View层处理模板页面

1.处理关注、取关事件按钮

按钮样式根据关注状态动态改变

th:class="|btn ${hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|"

关注的显示根据关注状态动态显示

  • 如果用户没有登录或者访问的就是自己的主页,无需显示关注的按钮
<button type="button"
      th:class="|btn ${hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|"
      th:text="${hasFollowed?'已关注':'关注TA'}" th:if="${loginUser!=null && loginUser.id!=user.id}">关注TA
</button>

关注事件对应的js文件定义

在按钮标签前添加隐藏标签
  • 为了传入指定用户id,在按钮标签前添加隐藏标签
<input type="hidden" id="entityId" th:value="${user.id}">
$(btn).prev().val()
  • 取到指定按钮标签前一个标签的内容
$(function(){
   $(".follow-btn").click(follow);
});

function follow() {
   var btn = this;
   if($(btn).hasClass("btn-info")) { // 关注按钮
      // 关注TA
      $.post(
         CONTEXT_PATH + "/follow",
         {"entityType":3,"entityId":$(btn).prev().val()},
         function (data) {
            data = $.parseJSON(data);
            if(data.code == 0) {
               window.location.reload();  // 为了完整显示当前个人主页数据,需要刷新页面,其他网页关注可以不必刷新
            } else {
               alert(data.msg);
            }
         }
      );
   } else {
      // 取消关注
      $.post(
         CONTEXT_PATH + "/unfollow",
         {"entityType":3,"entityId":$(btn).prev().val()},
         function (data) {
            data = $.parseJSON(data);
            if(data.code == 0) {
               window.location.reload();
            } else {
               alert(data.msg);
            }
         }
      );
   }
}

2. 显示指定用户关注了多少人

<span>关注了 <a class="text-primary" href="followee.html" th:text="${followTargetCnt}">5</a> 人</span>

3. 显示指定用户的粉丝数

<span class="ml-4">粉丝数 <a class="text-primary" href="follower.html" th:text="${followFans}">123</a> 人</span>

测试结果:

  • 未登录状态下,查看某用户个人主页


redis存用户数据并检索 redis 用户相关信息存储技巧_java_02


  • 登录状态下,
  • 访问自己的主页

redis存用户数据并检索 redis 用户相关信息存储技巧_数据库_03

  • 访问别人的主页

redis存用户数据并检索 redis 用户相关信息存储技巧_后端_04

  • 点击关注按钮

redis存用户数据并检索 redis 用户相关信息存储技巧_java_05



redis存用户数据并检索 redis 用户相关信息存储技巧_spring_06

redis存用户数据并检索 redis 用户相关信息存储技巧_spring_07

  • 点击取消关注按钮


redis存用户数据并检索 redis 用户相关信息存储技巧_数据库_08


redis存用户数据并检索 redis 用户相关信息存储技巧_数据库_09