• 社区系统项目复盘-4


    Redis高性能存储

    使用redis实现了点赞、关注相关的功能,优化了登录模块。

    什么是Redis?

    Redis是一个开源的key-value存储系统,支持多种数据类型,包括string(字符串),list(链表),set(集合),zset(有序集合)和hash(哈希)。Redis操作都是原子性的(有一个失败则都失败)。Redis支持各种方式的排序。Redis数据可以缓存在内存中,也可以周期性的写入到磁盘中(即持久化操作),在此基础上实现了主从(master-slave)同步。

    为什么使用Redis?

    Redis将所有的数据都存放在内存中,所以读写速度很快。同时,Redis将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。

    Spring是怎么整合Redis的?

    第一步:引入依赖

    • spring-boot-starter-data-redis
    <dependency>
    	<groupId>org.springframework.bootgroupId>
    	<artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    第二步:对redis进行配置

    • 配置数据库参数
    # RedisProperties
    spring.redis.database=11
    spring.redis.host=localhost
    spring.redis.port=6379
    
    • 1
    • 2
    • 3
    • 4
    • 编写配置类,构造Spring Redis的核心组件RedisTemplate,这个组件是由Spring提供的
    @Configuration
    public class RedisConfig {
        @Bean
        public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
            RedisTemplate<String,Object> template = new RedisTemplate<>();
            template.setConnectionFactory(factory);
    
            // 设置key的序列化方式
            template.setKeySerializer(RedisSerializer.string());
            // 设置value的序列化方式
            template.setValueSerializer(RedisSerializer.json());
            // 设置hash的key的序列化方式
            template.setHashKeySerializer(RedisSerializer.string());
            // 设置hash的value的序列化方式
            template.setHashValueSerializer(RedisSerializer.json());
    
            template.afterPropertiesSet();
            return template;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    第三步:访问redis的的不同数据类型

    • redisTemplate.opForValue()
    • redisTemplate.opForHash()
    • redisTemplate.opForList()
    • redisTemplate.opForSet()
    • redisTemplate.opForZset()

    Redis事务管理

    redis也是一个数据库,它也是支持事务的,redis事务管理的机制:当启用事务以后,再执行redis命令的时候,并不会立马执行命令,而是将命令放到一个队列里先存着,直到提交事务的时候才会将队列中的命令一次性发送给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");
    				  // 查询时是没有结果的。所以redis在管理事务的时候中间不要做查询,无效
                    System.out.println(operations.opsForSet().members(redisKey));
                    return operations.exec();  // 提交事务
                }
            });
            System.out.println(obj);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    点赞

    点赞部分主要有两大块功能,第一块就是点赞,第二块就是收到的赞

    主要显示页面:首页的帖子列表部分显示帖子的赞的数量;帖子详情上需要显示赞的数量以及点赞状态;个人主页上需要显示收到的赞的数量。

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

    详细步骤:点赞,首先调用点赞业务,查询点赞数量,当前点赞状态,~~根据点赞状态确定是否出发点赞事件(后面系统通知部分的功能),重新计算帖子分数(后面热帖的功能),~~最后返回响应。

    注意⚠️:点赞部分的存储使用redis的set来实现,将点赞者的用户ID存到set中。

    收到的赞:需要重构点赞功能,以用户为key,记录点赞数量,增加点赞数量使用increment(key),减少点赞数量使用decrement(key);开发个人主页,以用户为key,显示用户收到的点赞的数量。

    详细步骤:业务层重构点赞方法,另外需要添加一个查询某个用户获得的赞的数量的业务层方法。开发个人主页时,需要显示的内容主要有:用户信息,获得点赞数量,关注数量,粉丝数量,是否已关注,下一部分实现关注相关功能。

    注意⚠️:使用了redis事务管理,把点赞与记录点赞数量两个redis命令放在一个事务。
    收获的赞的数量使用redis的String来存储,存储的值为整数类型。

    在这里插入图片描述

    关注

    关注、取消关注

    • 需求:开发关注、取消关注功能;统计用户的关注数、粉丝数。实现个人首页显示关注数量,粉丝数量,是否已关注的功能。
    • 关键:若A关注了B,则A是B的Follower(粉丝),B是A的Followee(目标);关注的用户可以是用户、帖子、题目等,在实现时将这些目标抽象为实体【这一句比较重要,抽象化】

    ⚠️注意:

    • 关注、取关功能,也是需要使用redis事务管理的,因为需要同时更新存储关注的rediskey和存储粉丝的rediskey。
    • 对于关注或者粉丝的存储,采用的是redis的zset数据结构,score使用的是当前系统时间。
    • 关注、取关功能为异步请求

    关注列表、粉丝列表

    • 需求:在个人主页,点击关注或者粉丝,会相应的显示关注列表和粉丝列表,分页显示。
    • 关键:
      • 业务层:查询某个用户关注的人,支持分页;查询某个用户的粉丝,支持分页。
      • 表现层:处理“查询关注的人”,“查询粉丝”请求。

    ⚠️注意:业务层查询的信息用map来存,因为列表中不只需要有用户的信息,还需要有关注的时间。主要步骤是:先通过Redis查出来userid集合,然后遍历该集合,从根据userid从user数据库中查用户信息;关注时间是通过Redis来查询的,score就是关注时间。

    在这里插入图片描述

    优化登录模块

    主要使用Redis优化3个方面:存储验证码,存储用户登录凭证,缓存用户信息

    为什么要使用redis进行优化呢?

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

    在这里插入图片描述

    ⚠️注意:

    • 生成验证码的时候,用户还没有登录,此时无法获得userid,无法通过userid构造验证码对应的rediskey。所以生成验证码时临时给客户端颁发一个代表验证码归属的字符串就可以,这个字符串使用cookie发给客户端,同时使用该字符串构造存储验证码的rediskey。
    // 验证码的归属
    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 登录时检查验证码,此时标识验证码归属的字符串通过cookie传过来,然后我们通过这个字符串从redis里获得验证码的正确值,然后再校验验证码是否正确。
    //检查验证码
    // String kaptcha = (String) session.getAttribute("kaptcha");
    String kaptcha = null;
    if(StringUtils.isNoneBlank(kaptchaOwner)){
    	String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
    	kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
    }
    if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)){
    	model.addAttribute("codeMsg","验证码不正确!");
    	return "/site/login";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 使用redis存取登录凭证,首先使用@Deprecated注解废弃LoginTicketMapper,修改登录时存储登录凭证的方式,修改退出登录时修改登录凭证状态的方式,修改查询登录凭证的方式。之前是将登录凭证存储在mysql数据库表中,现在把它存储到redis中里。
    • 查询user时,先尝试从缓存中取值,如果有,则直接返回值,如果没有,就从数据库中查找,将查找结果存到缓存中。数据变更时(激活的时候,修改密码的时候,上传头像的时候),需要更新数据库,这时候为了保持缓存和数据库中信息的一致性,可以有两种方法:一是同步更新缓存,二是直接将缓存中数据删掉,本项目采取方法二。
    // 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);
            return user;
        }
        // 3.数据变更时清除缓存数据
        private void clearCache(int userId){
            String redisKey = RedisKeyUtil.getUserKey(userId);
            redisTemplate.delete(redisKey);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    小结:点赞部分用了Set数据结构,点赞数量用的String数据结构。关注用的Zset数据结构。验证码的存取用的String数据结构。登录凭证的存取用的String数据结构。缓存用户信息用的String数据结构。

  • 相关阅读:
    2021.06青少年软件编程(Python)等级考试试卷(二级)
    AVL树和红黑树的简单实现
    BF518/BF516/BF514/BF512系列DSP的开发教程四十一:数字信号处理-FFT
    Arm架构与神经网络
    【深入】k-means和FCM的差别
    uni.scanCode不支持h5扫码(用拍照或者获取相册识别二维码和条码)
    【Java 进阶篇】JDBC ResultSet 类详解
    C++重温笔记(七): C++模板与泛型程序设计
    Navicat操作mysql分区
    DNS大全(114DNS 、阿里DNS、百度DNS 、360 DNS、Google DNS)
  • 原文地址:https://blog.csdn.net/wangws_sb/article/details/128100757