0%

仿牛客论坛项目05-Redis

仿牛客论坛项目05-Redis

点赞功能、我收到的赞、关注和取消关注、关注列表和粉丝列表、Redis优化

Redis 入门

  • NoSQL(not only):关系型数据库之外的数据库
  • Redis 是基于键值对的 NoSQL 数据库,key 是 String 类型,值支持多种数据结构:字符串、哈希、列表、集合、有序集合
  • Redis 将所有数据都存放在内存,读写性能很强;并将内存中的数据以快照或日志的形式保存到硬盘上,保证数据安全性。
  • 典型应用场景:缓存、排行榜、计数器(帖子浏览量)、社交网络(点赞)、消息队列。
    安装方法:brew install redis
    启动 redis 服务器:redis-server
    连接到 redis 服务器:redis-cli

Spring 整合 Redis

  • 点赞
    • 针对帖子、评论点赞
    • 第一次点赞、第二次点赞取消
  • 首页显示点赞数量
    • 统计帖子点赞数量
  • 详情页显示点赞数量
    • 统计点赞数量
    • 显示点赞状态
      用 redis 提高性能。
      直接在业务层,在redis中存数据

点赞

  • 点赞
    • 对帖子、评论点赞
    • 第一次点赞、第二次取消点赞
  • 首页点赞数量
    • 统计帖子点赞数
  • 详情页点赞数量
    • 统计点赞数量
    • 显示点赞状态
      redis中存储格式:like:entity:entityType:entityId -> set(userId)
      like:user:userId -> int

我收到的赞

重构点赞功能

  • 以用户为key,记录点赞数量
    开发个人主页
  • 以用户为key,查询点赞数量
    点赞业务中加上维护用户收到的赞,一个业务中有两个更新,要用 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
        public void like(int userId, int entityType, int entityId, int entityUserId) {  
    // String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
    // System.out.println(entityLikeKey);
    // // 判断当前用户是否点过赞,userId是否在redis集合里
    // boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
    // if (isMember) {
    // // 如果点过赞,就取消点赞
    // redisTemplate.opsForSet().remove(entityLikeKey, userId);
    // } else {
    // // 如果没点过赞,就点赞
    // redisTemplate.opsForSet().add(entityLikeKey, userId);
    // }
    redisTemplate.execute(new SessionCallback() {
    @Override
    public Object execute(RedisOperations operations) throws DataAccessException {
    String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
    String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);

    // 查询不要放在事务里
    boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);

    operations.multi(); // 开启事务
    if (isMember) {
    operations.opsForSet().remove(entityLikeKey, userId);
    operations.opsForValue().decrement(userLikeKey);
    } else {
    operations.opsForSet().add(entityLikeKey, userId);
    operations.opsForValue().increment(userLikeKey);
    }
    return operations.exec();
    }
    });
    }

关注、取消关注

  • 需求
    • 实现关注、取消关注
    • 统计用户关注数、粉丝数
  • 关键
    • 关系:A关注B,A是B的粉丝(follower),B是A的关注目标(followee)
    • 关注的目标可以用用户、帖子、题目,实现时抽象为实体(存在redis)
      关注和取消关注是异步请求。(页面不会刷新)
      使用redis有序列表zset,新建两个主键:某个用户关注的实体,某个实体拥有的粉丝。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      public void follow(int userId, int entityType, int entityId) {  
      redisTemplate.execute(new SessionCallback() {
      @Override
      public Object execute(RedisOperations operations) throws DataAccessException {
      String followeeKey = RedisKeyUtil.getPrefixFolloweeKey(userId, entityType);
      String followerKey = RedisKeyUtil.getPrefixFollowerKey(entityType, entityId);

      operations.multi();

      // 某个用户关注的实体
      operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
      // 某个实体拥有的粉丝
      operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());

      return operations;
      }
      });
      }

redis 中存储格式:followee:userId:entityType -> zset(entityId, now)
follower:entityType:entityId -> zset(userId, now)

关注列表、粉丝列表

  • 业务层
    • 查询某个用户关注的人,分页
    • 查询某个用户的粉丝,分页
  • 表现层
    • 查询关注的人、查询粉丝请求
    • 查询关注的人、查询粉丝模版
      查询关注的人,需要分页信息、关注的人列表、当前用户是否关注过这个人
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      @RequestMapping(path = "/followees/{userId}", method = RequestMethod.GET)  
      public String getFollowees(@PathVariable("userId") int userId, Model model, Page page) {
      User user = userService.findUserById(userId);
      if(user == null) {
      throw new RuntimeException("该用户不存在!");
      }
      model.addAttribute("user", user);
      //分页信息
      page.setLimit(5);
      page.setPath("/followees/" + userId);
      page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER));

      // 关注列表
      List<Map<String,Object>> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit());
      if(userList != null) {
      for(Map<String, Object> map : userList) {
      // 判断是否已关注
      User u = (User) map.get("user");
      map.put("hasFollowed", hasFollowed(u.getId()));
      }
      }
      model.addAttribute("users", userList);
      return "/site/followee";
      }

优化登录模块

  • 使用 Redis 存储验证码
    • 验证码要频繁访问和刷新,对性能要求高
    • 验证码不需要永久保存,很短时间后失效
    • 目前存在了 session 中,用户量大时,存储大量验证码会占用内容,影响性能;分布式部署时,不同服务器上的 session 无法共享,导致无法验证。
  • 使用 Redis 存储登录凭证
    • 目前登录凭证存在 Mysql 中,每次登录都要查询用户凭证,访问频率很高
  • 使用 Redis 缓存用户信息
    • 处理每次请求,都要根据凭证查询用户信息,访问频率高

总结适合用 Redis 存储的数据:

  • 需要频繁读写,但对一致性要求不高的数据,如:热点数据、排行版、计数器
  • 需要缓存的数据,如用户信息、文章内容
  • 需要短时存储的数据,如验证码
  • 需要实时统计的数据,如点赞数、浏览量、粉丝数
    Redis 作为缓存 + MySQL 作为持久化存储
  1. 验证码使用 redis 存储

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 存入redis  
    // 验证码的归属,用cookie保存
    String kapthcaOwner = CommunityUtil.generateUUID();
    Cookie cookie = new Cookie("kaptchaOwner", kapthcaOwner);
    cookie.setMaxAge(60);
    cookie.setPath(contextPath);
    response.addCookie(cookie);

    // 从redis中取出验证码
    String kaptcha = null;
    if(StringUtils.isNoneBlank(kaptchaOwner)) {
    String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
    kaptcha = redisTemplate.opsForValue().get(redisKey).toString();
    }
  2. 登录凭证使用 redis 存储
    登录时存入,登出时删掉

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());  
    // redis 将对象序列化为JSON格式的字符串
    redisTemplate.opsForValue().set(redisKey, loginTicket, expiredSeconds, TimeUnit.SECONDS);

    // 退出
    public void logout(String ticket) {
    // loginTicketMapper.updateStatus(ticket, 1);
    String redisKey = RedisKeyUtil.getTicketKey(ticket);
    // 将对象的status改为1
    LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    loginTicket.setStatus(1);
    // 改完再存进去
    redisTemplate.opsForValue().set(redisKey, loginTicket);
    }

    // 根据ticket查询凭证
    public LoginTicket findLoginTicket(String ticket) {
    // return loginTicketMapper.selectByTicket(ticket);
    String redisKey = RedisKeyUtil.getTicketKey(ticket);
    return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    }
  3. redis 缓存用户信息
    调用最多的方法是public User findUserById(int id)
    查询 User 时,先尝试从缓存中取值,能取到就用,取不到就做初始化存入。
    更改用户信息时,把缓存删除。如果更新缓存,会有并发的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 1. 优先从缓存中取值  
    private User getCache(int userId) {
    String redisKey = RedisKeyUtil.getUserKey(userId);
    return (User) redisTemplate.opsForValue().get(redisKey);
    }
    // 2. 取不到时初始化缓存数据
    private User initCache(int userId) {
    // 从 MySQL 中查到数据
    User user = userMapper.selectById(userId);
    String redisKey = RedisKeyUtil.getUserKey(userId);
    redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
    return user;
    }
    // 3. 数据变更时,清楚缓存数据
    public void clearCache(int userId) {
    String redisKey = RedisKeyUtil.getUserKey(userId);
    redisTemplate.delete(redisKey);
    }

问题

  1. 为什么用 redis
    1、Redis是⼀种基于键值对的NoSQL数据库,它⽀持多种数据结构:
    字符串(String)、哈希(hashs)、列表(lists)、集合(sets)、有序集合(sorted sets)等
    2、Redis将所有的数据都存在内存中,所以它的读写性能⼗分惊⼈。
    同时,Redis还可以将内存中的数据以快照或者⽇志的形式保存到硬盘上,以保证数据的安全性。
    3、缓存、排⾏榜(热⻔帖⼦)、计数器、社交⽹络(点赞数)、消息队列等。
  2. 如何优化登陆模块?
    当⽤户点击刷新验证码时,服务端⾸先给当前需要登陆的游客,设置⼀个随机字符串(kaptchaOwner),⽤于标识当前这个游客,然后将随机字符串存⼊到cookie中,返回给浏览器,然后服务端的 redis 保存 key:随机字符串,接着⽤户输⼊⽤户名,密码,验证码,再次点击登陆时,服务端会从 cookie 中拿到 kaptchaOwner ,通过它可以从 Redis 中得到正确的验证码,然后与⽤户输⼊的验证码做⽐较,看是否⼀致。