互助交流论坛系统 第四章:Redis,一站式高性能存储方案

8 篇文章 0 订阅
5 篇文章 0 订阅

Redis入门

1. 概念

  • Redis是一款基于键值对的NoSQL数据库,它的key都是String,它的值支持多种数据结构:
    Nosql:关系型数据库以外的数据库
  • 字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
  • Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人。
  • 同时,Redis还可以将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。
    • 存储快照比较耗时,并且产生存储阻塞,不适合实时去做,适合几小时做一次
    • 以日志的形式存储称为aof,存储命令,可以做到实时存储,以追加的形式存,体积比较大;如果想恢复,是直接把命令再执行一遍
  • Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等。
    • 缓存:访问非常频繁
    • 排行榜:热门帖子访问频繁,进行缓存后效率就高
    • 计数器:比如访问帖子,浏览量+1
    • 社交网络:点赞、点踩、关注等
    • 消息队列:redis不是专门做消息队列的工具 -> kafka

2. 安装

  • 下载
    https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100
  • 配置环境变量
    在这里插入图片描述

3. 常用命令

内置16个库,索引是0-15

  • redis-cli:使用redis
  • Select index:选择哪个库
  • flushdb:刷新数据库
  • 存set类型的数据
    • set key value [EX seconds] [PX milliseconds] [NX|XX] :存数据
      其中key是以字符串形式存的
      set test:count 1 key起名叫test:count
    • get test:count 取数据
    • incr test:count 让key对应的value + 1
    • decr test:count 让key对应的value - 1
  • 存哈希类型的数据
    • 存数据:hset test:user id 1 hset test:user username zhangsan
    • 取数据: hget test:user id hget test:user username
  • 存列表类型的数据
    可以想象为横向容器,列表可以左进也可以右进
    • 左近右出 lpush test:ids 101 102 103 ->[103 102 101] (l表示left,从左侧push进)
    • 查看列表长度:llen test:ids
    • 查看列表中某个索引的值: lindex test:ids 0 -> [103 102 101]
    • 查看某个范围内索引的值: lrange test:ids 0 2
        1. “103”
      1. “102”
      2. “101”
    • 从右侧弹出值:rpop test:ids
  • 存集合类型的数据
    列表有序,集合无序,而且集合中的值不能重复
    • 往集合里存数据:sadd test:teacher aaa bbb ccc ddd eee
    • 统计集合中有多少个元素: scard test:teacher
    • 从集合中随机弹出一个元素: spop test:teacher
      (可以利用这样的命令实现抽奖,每次弹出随机的用户)
    • 查看集合中还有多少个元素: smembers test:teacher
  • 有序的集合
    把存的每个值附加了分数,按分数排序
    • 存数:zadd test:students 10 aaa 20 bbb 30 ccc 40 ddd 50 eee
      aaa的分数是10 bbb的分数是20 以此类推
    • 统计有序集合中有多少个数据:zcard test:students
    • 查询某一个值的分数:zscore test:students ccc ->[30]
    • 返回某个值的排名,默认是从小到大(从0开始): zrank test:students ccc ->[2]
    • 取某个范围内的数据:zrange test:students 0 2
  • 全局命令
    • 查看库里总共有多少个key :keys *
      [图片]
    • 查看库里以test开头的key有多少个
      [图片]
    • 查看某个key值的类型: type test:user
    • 查看某个key是否存在,0表示不存在,1表示存在: exists test:user
    • 删除key:del test:user
    • 设置某个key的过期时间,到了这个时间就自动删除:expire test:students 10 (10秒后过期)
      在这里插入图片描述
    • 查看库里以test开头的key有多少个
      在这里插入图片描述
    • 查看某个key值的类型: type test:user
    • 查看某个key是否存在,0表示不存在,1表示存在: exists test:user
    • 删除key:del test:user
    • 设置某个key的过期时间,到了这个时间就自动删除:expire test:students 10 (10秒后过期)

Spring整合Redis

1. 引入依赖

  • spring-boot-starter-data-redis
    如果不写版本号,就默认是当前springboot自定义的版本号
<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-redis</artifactId>
</dependency>

这里要加client依赖,不然我的报错了

<dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <!--<version>2.9.0</version>-->
</dependency>

2. 配置Redis

  • 配置数据库参数
# redisProperties
# 一共有16个库,这里选索引为11的库
spring.redis.database=11
# ip地址
spring.redis.host=localhost
# 端口
spring.redis.port=6379
  • 编写配置类,构造RedisTemplate(config.redisConfig)
    • 序列化:序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。
@Configuration
public class RedisConfig {

    // 定义第三方bean
    @Bean
    // 当在参数里注入工厂参数RedisConnectionFactory, spring 会自动把bean注入进来
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // tempalate 具有工厂之后就具有访问数据库的能力
        template.setConnectionFactory(factory);
        // 设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
        // 设置value的序列化方式:value可能是各种类型的数据,序列化为json
        template.setValueSerializer(RedisSerializer.json());
        // 设置hash的key的序列化方式
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置hash的value的序列化方式
        template.setHashValueSerializer(RedisSerializer.json());
        
        // 做完设置之后触发生效
        template.afterPropertiesSet();
        return template;
    }
}

3. 访问Redis

  • redisTemplate.opsForValue()
  • redisTemplate.opsForHash()
  • redisTemplate.opsForList()
  • redisTemplate.opsForSet()
  • redisTemplate.opsForZSet()

访问String

@Test
public void testStrings() {

    String redisKey = "test:count";

    // 新增一个set类型的值
    redisTemplate.opsForValue().set(redisKey, 1);
    // 获取value
    System.out.println(redisTemplate.opsForValue().get(redisKey));
    // value + 1
    System.out.println(redisTemplate.opsForValue().increment(redisKey));
    // value - 1
    System.out.println(redisTemplate.opsForValue().decrement(redisKey));
}

访问hash

@Test
public void testHashes() {
    String redisKey = "test:user";

    // 存值
    redisTemplate.opsForHash().put(redisKey, "id", 1);
    redisTemplate.opsForHash().put(redisKey, "username", "张三");

    // 取值
    System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
    System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));;

}

访问列表

@Test
public void testLists() {
    String redisKey = "test:ids";

    redisTemplate.opsForList().leftPush(redisKey, 101);
    redisTemplate.opsForList().leftPush(redisKey, 102);
    redisTemplate.opsForList().leftPush(redisKey, 103);

    System.out.println(redisTemplate.opsForList().size(redisKey));
    System.out.println(redisTemplate.opsForList().index(redisKey, 0));
    System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));

    System.out.println(redisTemplate.opsForList().leftPop(redisKey));
    System.out.println(redisTemplate.opsForList().leftPop(redisKey));
    System.out.println(redisTemplate.opsForList().leftPop(redisKey));
}

/**
 * 6
 * 103
 * [103, 102, 101]
 * 103
 * 102
 * 101
 */

访问集合

@Test
public void testSets() {
    String redisKey = "test:teachers";

    redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");

    System.out.println(redisTemplate.opsForSet().size(redisKey));
    // pop不分左右,随机弹出一个值
    System.out.println(redisTemplate.opsForSet().pop(redisKey));
    System.out.println(redisTemplate.opsForSet().members(redisKey));
}

访问有序集合

@Test
public void testSortedSets() {
    String redisKey = "test:students";

    redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);
    redisTemplate.opsForZSet().add(redisKey, "悟空", 90);
    redisTemplate.opsForZSet().add(redisKey, "八戒", 50);
    redisTemplate.opsForZSet().add(redisKey, "沙僧", 70);
    redisTemplate.opsForZSet().add(redisKey, "白龙马", 60);

    System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
    System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒"));
    // 返回索引
    System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));
    // 由大到小排名
    System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
} 

访问key

@Test
public void testKeys() {
    redisTemplate.delete("test:user");
    System.out.println(redisTemplate.hasKey("test:user"));
    // 一段时间后删除
    redisTemplate.expire("test:student", 10, TimeUnit.SECONDS);
}
多次访问同一个key
// 多次访问一个key,以绑定的方法存在
@Test
public void testBoundOperations() {
    String redisKey = "test:count";
    BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
    operations.increment();
    operations.increment();
    operations.increment();
    operations.increment();
    operations.increment();
    System.out.println(operations.get());

事务管理

当启用事务以后,redis不会立刻执行,而是会放进队列里,事务提交时统一批量地执行
不能在事务中间做查询,要么在事务之前查,要么在提交后查
redis也支持编程式事务和声明式事务,一般用编程式事务

// 编程式事务
@Test
public void testTransactional(){
    Object obj = redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String redisKey = "test:tx";
            operations.multi();

            // 往集合中存数据
            operations.opsForSet().add(redisKey, "zhangsan");
            operations.opsForSet().add(redisKey, "lisi");
            operations.opsForSet().add(redisKey, "wangwu");

            // 在事务执行期间查询
            System.out.println(operations.opsForSet().members(redisKey));
            // 提交事务
            return operations.exec();
        }
    });

    System.out.println(obj);
}

// []
// [1, 1, 1, [lisi, wangwu, zhangsan]]
// 事务中间的查询无效

点赞

1. 需求

点赞需要考虑性能问题,因为可能同时有很多人同时进行点赞。

  • 点赞
    • 支持对帖子、评论点赞。
    • 第1次点赞,第2次取消点赞。
  • 首页点赞数量
    • 统计帖子的点赞数量。
  • 详情页点赞数量
    • 统计点赞数量。
    • 显示点赞状态。

2. 代码实现

创建生成key的工具类 util.RedisKeyUtil

public class RedisKeyUtil {

    // string在拼时以":"分隔单词
    private static final String SPLIT = ":";
    // 希望存实体的赞,以下面的前缀开头
    private static final String PRIEIX_ENTITY_LIKE = "like:entity";

    // 某个实体的赞
    // 传入关键信息,对于redis而言就是type和id
    // like:entity:entityType:entityId -> set(userId)
    public static String getEntityLikeKey(int entityType, int entityId) {
        return PRIEIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
    }
}

业务层

  • 实现点赞
    a. 拼出数据的key
    B. 判断点没点过赞,如果点过就取消点赞 - 判断在不在集合里
public void like(int userId, int entityType, int entityId) {
    // 1、拼出数据的key
    String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
    // 2。判断点没点过赞,如果点过就取消点赞 - 判断在不在集合里
    Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
    if(isMember) {
        redisTemplate.opsForSet().remove(entityLikeKey, userId);
    } else {
        redisTemplate.opsForSet().add(entityLikeKey, userId);
    }
}
  • 查询实体点赞的数量
public long findEntityLikeCount(int entityType, int entityId) {
    String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
    return redisTemplate.opsForSet().size();
}
- 查询某人对实体的点赞状态
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
    String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
    return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
}

表现层

点赞在帖子详情页面。
是异步请求,点赞页面不刷新

@ResponseBody
public String like(int entityType, int entityId) {
    // 获取当前用户
    User user = hostHolder.getUser();
    // 实现点赞
    likeService.like(user.getId(), entityType, entityId);
    // 统计点赞的数量
    long likeCount = likeService.findEntityLikeCount(entityType, entityId);
    // 获取点赞的状态
    int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
    // 使用map封装数量和状态
    Map<String, Object> map = new HashMap<>();
    map.put("likeCount", likeCount);
    map.put("likeStatus", likeStatus);
    return CommunityUtil.getJSONString(0, null, map);
}
  • 对于帖子/评论点赞的处理

我收到的赞

1. 需求

在个人主页显示获得多少的赞

  • 构点赞功能
    • 以用户为key,记录点赞数量
    • increment(key),decrement(key)
  • 开发个人主页
    • 以用户为key,查询点赞数量

2. 代码实现

工具类
like:user:userId -> int

// 某个用户的赞
// like:user:userId -> int
public static String getUserLikeKey(int userId) {
    return PREFIX_USER_LIKE + SPLIT + userId;
}

改造实现点赞的业务方法LikeService

  • 需要有两次事务的实现,要对事务进行管理(编程式事务管理)
public void like(int userId, int entityType, int entityId, int entityUserId) {
    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();
        }
    });
}
  • 查询某个用户点赞的数量
// 查询某个用户获得的赞
public int findUserLikeCount(int userId) {
    String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
    Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
    return count == null ? 0 : count.intValue();
}

表现层

  • 增加entityLikeUserId作为传入参数
  • 查询个人主页,查询用户和点赞数量
@RequestMapping(path = "/profile/{userId}", method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
    User user = userService.findUserById(userId);
    if(user == null) {
        throw new RuntimeException("该用户不存在!");
    }

    // 用户
    model.addAttribute("user", user);
    // 点赞数量
    int likeCount = likeService.findUserLikeCount(userId);
    model.addAttribute("likeCount", likeCount);
    
    return "/site/profile";
}

关注、取消关注

1. 需求

  • 需求
    • 开发关注、取消关注功能。
    • 统计用户的关注数、粉丝数。
  • 关注
    • 若A关注了B,则A是B的Follower(粉丝),B是A的Followee(目标)。
    • 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体。

2. 代码实现

实现关注取关

  • 创建构建粉丝和目标的实体工具类
// 某个用户关注的实体(不一定是人),关注的目标
// entityType 是关注的对象,zset体现关注的对象的id
// followee:userId:entityType -> zset(entityId,now) zset有分数,以当前时间作为分数
public static String getFolloweeKey(int userId, int entityType) {
    return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
}

// 某个实体拥有的粉丝
// follower:entityType:entityId -> zset(userId,now)
public static String getFollowerKey(int entityType, int entityId) {
    return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;
}
  • 实现关注和取关的service方法
/**
 * 实现关注
 */
public void follow(int userId, int entityType, int entityId) {
    redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
            String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);

            operations.multi();
            // 在事务中间做两次存储的操作
            operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
            operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());

            return operations.exec();
        }
    });
}

/**
 *
 * 实现取关
 */
public void unfollow(int userId, int entityType, int entityId) {
    redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
            String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);

            operations.multi();
            // 在事务中间做两次存储的操作
            operations.opsForZSet().remove(followeeKey, entityId);
            operations.opsForZSet().remove(followerKey, userId);

            return operations.exec();
        }
    });
}
  • 实现关注和取关的controller请求
@RequestMapping(path = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
    User user = hostHolder.getUser();

    followService.follow(user.getId(), entityType, entityId);
    return CommunityUtil.getJSONString(0, "已关注");
}

@RequestMapping(path = "/unfollow", method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType, int entityId) {
    User user = hostHolder.getUser();

    followService.follow(user.getId(), entityType, entityId);
    return CommunityUtil.getJSONString(0, "已取消关注");
}

实现统计用户的关注数、粉丝数

  • 实现service层查询数量、查询是否关注的方法
/**
 * 查询关注的实体的数量
 */
public long findFolloweeCount(int userId, int entityType) {
    String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
    // zCard(): 用于统计集合中元素的数量
    return redisTemplate.opsForZSet().zCard(followeeKey);
}

/**
 * 查询实体的粉丝数量
 */
public long findFollowerCount(int entityType, int userId) {
    String followerKey = RedisKeyUtil.getFolloweeKey(entityType, entityType);
    // zCard(): 用于统计集合中元素的数量
    return redisTemplate.opsForZSet().zCard(followerKey);
}

/**
 * 查询当前用户是否已关注该实体
 */
public boolean hasFollowed(int userId, int entityType, int entityId) {
    String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
    // 如果查询到分数就是有,如果没有查询到分数就是没有关注该实体
    return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
}
  • 实现controller层的方法 - UserController里写
// 关注数量
long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);
model.addAttribute("followeeCount", followeeCount);
// 粉丝数量
long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId);
model.addAttribute("followerCount", followerCount);
// 是否已关注
boolean hasFollowed = false;
if(hostHolder.getUser() != null) {
    hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
model.addAttribute("hasFollowed", hasFollowed);

关注列表、粉丝列表

1. 需求

  • 业务层
    • 查询某个用户关注的人,支持分页。
    • 查询某个用户的粉丝,支持分页。
  • 表现层
    • 处理“查询关注的人”、“查询粉丝”请求。
    • 编写“查询关注的人”、“查询粉丝”模板。

2. 代码实现

业务层
这里的set方法是redis内置的,是有序的集合

// 查询某用户关注的人
    public List<Map<String, Object>> findFollowees(int userId, int offset, int limit) {
        String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
        Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);

        if (targetIds == null) {
            return null;
        }

        List<Map<String, Object>> list = new ArrayList<>();
        for (Integer targetId : targetIds) {
            Map<String, Object> map = new HashMap<>();
            User user = userService.findUserById(targetId);
            map.put("user", user);
            Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);
            map.put("followTime", new Date(score.longValue()));
            list.add(map);
        }

        return list;
    }

    // 查询某用户的粉丝
    public List<Map<String, Object>> findFollowers(int userId, int offset, int limit) {
        String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);
        Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1);

        if (targetIds == null) {
            return null;
        }

        List<Map<String, Object>> list = new ArrayList<>();
        for (Integer targetId : targetIds) {
            Map<String, Object> map = new HashMap<>();
            User user = userService.findUserById(targetId);
            map.put("user", user);
            Double score = redisTemplate.opsForZSet().score(followerKey, targetId);
            map.put("followTime", new Date(score.longValue()));
            list.add(map);
        }

        return list;
    }

控制层

@RequestMapping(path = "/followees/{userId}", method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) {
    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";
}

// 查询用户的粉丝
@RequestMapping(path = "/followers/{userId}", method = RequestMethod.GET)
public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) {
    User user = userService.findUserById(userId);
    if (user == null) {
        throw new RuntimeException("该用户不存在!");
    }
    model.addAttribute("user", user);

    page.setLimit(5);
    page.setPath("/followers/" + userId);
    page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId));

    List<Map<String, Object>> userList = followService.findFollowers(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/follower";
}

private boolean hasFollowed(int userId) {
    if(hostHolder.getUser() == null) {
        return false;
    }
    return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}

处理模板

优化登录模块

1. 需求

  • 使用Redis存储验证码
    • 验证码需要频繁的访问与刷新,对性能要求较高。
    • 验证码不需永久保存,通常在很短的时间后就会失效。(目前是存在session里)
    • 分布式部署时,存在Session共享的问题。
  • 使用Redis存储登录凭证
    • 处理每次请求时,都要查询用户的登录凭证,访问的频率非常高。
  • 使用Redis缓存用户信息
    • 处理每次请求时,都要根据凭证查询用户信息,访问的频率非常高。

2. 代码重构

2.1 使用Redis存储验证码

定义RedisKey
private static final String PREFIX_KAPTCHA = "kaptcha";

// 登录验证码
// 不能用userId,因为此时用户没有登录,owner是用户的临时凭证
public static String getUserKey(String owner) {
    return PREFIX_KAPTCHA + SPLIT + owner;
}
重构controller层
  • getKacha方法
    原本验证码是用session存储的,现在改为redis存储
// 生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);

// 将验证码存入session
// session.setAttribute("kaptcha", text);

// 验证码的归属
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
// 将验证码存储到redis里
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);
  • 重构login方法
// String kaptcha = (String) session.getAttribute("kaptcha");
// 检查验证码
String kaptcha = null;
if(StringUtils.isNotBlank(kaptchaOwner)) {
    String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
    kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
}

2.2 使用Redis存储登录凭证

定义RedisKey
private static final String PREFIX_TICKET = "ticket";
// 登录的凭证
    public static String getTicketKey(String ticket) {
        return PREFIX_TICKET + SPLIT + ticket;
    }

废弃LoginTicketMapper
@Deprecated - 设为不推荐使用

重构方法

login方法

String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
// redis 会把这个对象序列化成json格式的字符串
redisTemplate.opsForValue().set(redisKey, loginTicket);

logout方法

public void logout(String ticket) {

    // loginTicketMapper.updateStatus(ticket, 1);
    String redisKey = RedisKeyUtil.getTicketKey(ticket);
    LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    loginTicket.setStatus(1);
    redisTemplate.opsForValue().set(redisKey, loginTicket);
}

查询凭证的方法findLoginTicket

public LoginTicket findLoginTicket(String ticket) {
    // return loginTicketMapper.selectByTicket(ticket);
    String redisKey = RedisKeyUtil.getTicketKey(ticket);
    return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}

2.4 使用Redis缓存用户信息

为为了提高效率,临时存一下,主要重构findUserById方法

定义RedisKey
private static final String PREFIX_USER = "user";

// 用户
    public static String getUserKey(int userId) {
        return PREFIX_USER + SPLIT + userId;
    }
    ```
实现变更缓存的方法
当数据发生变化时,重新把缓存删除
如果更新数据,可能会存在并发的问题
// 当数据发生变化时,重新把缓存删除
// 如果更新数据,可能会存在并发的问题
```java 
// 1. 优先从缓存中取值
private User getCache(int userId) {
    String redisKey = RedisKeyUtil.getUserKey(userId);
    return (User) redisTemplate.opsForValue().get(redisKey);
}
// 2. 取不到时初始化缓存数据
private User initCache(int userId) {
    User user = userMapper.selectById(userId);
    String redisKey = RedisKeyUtil.getUserKey(userId);
    redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
}
// 3. 数据变更时清除缓存数据
private void clearCache(int userId) {
    String redisKey = RedisKeyUtil.getUserKey(userId);
    redisTemplate.delete(redisKey);
}
重构UserService中的方法
  • findUserById
public User findUserById(int id){

    // return userMapper.selectById(id);
    User user = getCache(id);
    if(user == null) {
        user = initCache(id);
    }
    return user;
}
  • updateHeader
// 更新用户头像
public int updateHeader(int userId, String headerUrl) {
    int rows = userMapper.updateHeader(userId, headerUrl);
    clearCache(userId);
    return rows;
}
  • activation
public int activation(int userId, String code){
    User user = userMapper.selectById(userId);
    // 已经激活过了
    if(user.getStatus() == 1){
        return ACTIVATION_REPEAT;
    }else if(user.getActivationCode().equals(code)){
        userMapper.updateStatus(userId, 1);
        clearCache(userId);
        return ACTIVATION_SUCCESS;
    }else {
        return ACTIVATION_FAILURE;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值