博客项目集成Redis的简单介绍

一.背景

  本文不涉及Redis的详细介绍,只是简单的介绍一下本项目集成Redis的思路与做法,本文中所有使用的Redis工具类和Redis配置函数,可直接跳到第三部分下载。

二.详细介绍

 2.1 前期准备

  2.1.1 基本配置(以阿里云和CenterOS7为例)

  1. 配置阿里云安全组,放行Redis端口,默认端口为6379
  2. 防火墙放行Redis端口,默认为6379  firewall-cmd --zone=public --add-port=6379/tcp --permanent
  3. 重启防火墙 firewall-cmd --reload 

  2.1.2 配置Redis,修改对应配置文件

  1. 将bind 127.0.0.1 注释,否则只能本地连接
  2. 将protected-mode修改为no
  3. 将daemonize no修改为yes, 默认情况下,redis不在后台运行的,如果需要在后台运行,把该项的值更改为yes。
  4. 设置timeout 300  当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
  5. 设置密码 requirepass xxxxxx ,远程连接一定要设置密码,否则非常不安全,且最好直接修改配置文件
     若使用命令行修改 config set requirepass xxxxxx,每次都要修改,非常麻烦
  6. 通过指定配置文件方式重启Redis ./redis-server  ../redis.conf

  2.1.3 Redis与SpringBoot整合导入相关依赖

        <!-- 序列化的依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId> 
            <version>1.2.75</version>
        </dependency>
        <!-- redis与springboot整合的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>

 2.2 默认配置

  2.2.1 配置Redis的相关属性

Spring:
  redis:
      database: 0             //选择你所使用的Redis数据库,默认有16个数据库,可通过修改配置文件增加
      password: 123456        //redis数据库的密码,本地测试可以不设密码,但跨网络一定要设密码
      port: 6379              //端口号,默认为6379
      host: 127.0.0.1         //redis的地址,本地为127.0.0.1

  2.2.2 定制化RedisTemplate
  使用jackson序列化对象,请注意SpringBoot2版本的Redis连接池不再使用Jedis,转而使用基于netty的lettuce,请勿使用失效的方法。本部分内容可以直接复制使用

@Configuration
public class RedisConfigAutoConfiguration {
    //编写redisTemplate

    @Bean(name = "MyRedis")
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    @ConditionalOnMissingBean(name="redisTemplate")
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory){
        StringRedisTemplate template=new StringRedisTemplate();
        template.setConnectionFactory(factory);
        return template;
    }
}

  2.2.3 封装Redis工具类
  本部分不会详细介绍工具类中的各个函数,请自行去第三部分下载使用

@Component
public final class RedisUtils {
    @Autowired
    @Qualifier("MyRedis")       //与上文中@Bean内容对应
    private RedisTemplate<String, Object> redisTemplate;
    //封装redisTemplate.opsForValue()的各种方法,简化使用

  2.2.4 实体类实现Serializable接口,以便对象的序列化与反序列化

 2.3 相关数据结构的使用

  本项目中主要使用了String,List,Hash三种数据结构,下文将根据各个模块所使用的的数据结构进行相关介绍

  2.3.1 底部栏相关信息

  底部栏中提供了本网站的大致信息,包括文章总数,访问总数,留言总数,评论总数四个信息,这些信息在Redis中都是通过String存储的,以key:value的形式存储,避免了每次都需要去数据库中计算数据的麻烦,下文以文章总数为例进行介绍
  没有集成Redis时,计算文章总数需要去数据库中的博客表计算表行数,几乎需要遍历博客表中的每一行,而且每个界面都要用到该数据,那么怎么样才能优化呢
  计算博客数的SQL语句

   select count(*) from t_blog

  显然我们只需要在Redis中用String存储相对应关键字,对应值为博客总数即可,只在第一次访问时去数据库中计算,之后直接从Redis缓存中取值
  修改相应Service层的方法即可

 public Integer getBlogTotal() {
        if(redisUtils.hasKey("blogtotal")) return (Integer) redisUtils.get("blogtotal");//如果有该关键字,直接取值
        else redisUtils.set("blogtotal",blogDao.getBlogTotal()); //否则去数据库中计算,并更新Redis缓存
        return blogDao.getBlogTotal();
    }

  为了维护数据一致性,我们需要思索什么时候该数据需要更新,显然在新增博客和删除博客时,需要同步修改博客数量

  redisUtils.incr("blogtotal",1);   //博客数量减1
  redisUtils.decr("blogtotal",1);   //博客数量加1

  从后,除第一次外,统计博客数量只需从Redis中取值即可,再也不需访问数据库了,其他信息也基本类似,步骤均为第一次访问从数据库取值,并同步设置Redis缓存值,此后均在Redis中取值,更新数据时同步更新Redis缓存即可

  2.3.2 博客详细信息

  博客详细内容查询的SQL语句如下:

  <resultMap id="detailedBlog" type="DetailedBlog">
        <id property="id" column="bid"/>
        <result property="firstPicture" column="first_picture"/>
        <result property="flag" column="flag"/>
        <result property="title" column="title"/>
        <result property="content" column="content"/>
        <result property="typeName" column="name"/>
        <result property="views" column="views"/>
        <result property="commentCount" column="comment_count"/>
        <result property="createTime" column="create_time"/>
        <result property="commentabled" column="commentabled"/>
        <result property="shareStatement" column="share_statement"/>
        <result property="appreciation" column="appreciation"/>
        <result property="nickname" column="username"/>
        <result property="avatar" column="avatar"/>
    </resultMap>

    <select id="getDetailedBlog" parameterType="long" resultMap="detailedBlog">
        select b.id bid,b.first_picture,b.flag,b.title,b.content,b.views,b.comment_count,b.create_time,b.commentabled,b.share_statement,b.appreciation, u.username,u.avatar,t.name
        from t_blog b,t_user u, t_type t
        where b.user_id = u.id and b.type_id = t.id and b.id = #{id}
    </select>

  查询博客内容需要访问数据库联结三张表进行查询,非常的浪费时间,使用Redis缓存,修改Service层如下

    //取值时
    @Override
    public DetailedBlog getDetailedBlog(Long id) {
        DetailedBlog detailedBlog=(DetailedBlog) redisUtils.hget("detailedBlog","blog"+id);
        if(detailedBlog!=null)
        {
            Integer commentCount= (Integer) redisUtils.get("comment"+id);
            Integer viewCount= (Integer) redisUtils.get("view"+id);
            detailedBlog.setCommentCount(commentCount);
            detailedBlog.setViews(viewCount);
            redisUtils.incr("view"+id,1);
            redisUtils.incr("blogviewtotal",1);
            return detailedBlog;
        }
        detailedBlog = blogDao.getDetailedBlog(id);
        if (detailedBlog == null) {
            throw new NotFindException("该博客不存在");
        }
        String content = detailedBlog.getContent();
        detailedBlog.setContent(MarkdownUtils.markdownToHtmlExtensions(content));
        redisUtils.hset("detailedBlog","blog"+id,detailedBlog);
        redisUtils.set("view"+id,detailedBlog.getViews()+1);
        redisUtils.incr("blogviewtotal",1);
        redisUtils.set("comment"+id,blogDao.getCommentCountById(id));
        return detailedBlog;
    }

    //修改时
    @Override
    public int updateBlog(Blog blog) {
        redisUtils.del("AllRecommendBlog","AllBlog","AllType","AllFirstPageBlog"); //删除相关的关键字
        redisUtils.hdel("detailedBlog","blog"+blog.getId());    //删除表中指定的博客即可
        blog.setUpdateTime(new Date());
        return blogDao.updateBlog(blog);
    }

  对于博客的访问量评论量等相关信息,我采取了同2.3.1博客总数同样的存储方式,只不过关键词改为"view"+id,避免去数据库中联表查询
  而对于博客内容的信息,此处可以采用String的方式,关键词用"blog"+id存储,也可以和我一样采用Hash存储的方式,将所有博客数据存到一个哈希表中,哈希表名为detailedBlog,哈希表内部,对于每个博客可以将关键词设为"blog"+id,与字符串方法相比,可以大大减少关键词数量,查询速度也非常快。具体查询方式与字符串方式类似,第一次去数据库中读取,并将其存在Redis缓存中的哈希表里,之后每次均从哈希表中读
  为了维护数据一致性,我们需要思索什么时候需要更新Redis缓存,显然在删除博客和修改博客时,需要同步修改缓存中的数据,这两种情况中,都可以直接删除哈希表中对应的"blog"+id关键字,下次访问时,会重新写入该数据。当然我们也可以在修改时直接更新缓存,但并没有太多的必要。如果担心第一次访问过慢的问题,可以在修改后直接调用博客访问方法,将最新数据写入缓存

  2.3.3 日记

  日记功能只需要在日记表中select * 就完事了,本身效率并不低,但我们可以在Redis内存中用链表存储相关数据,新增日记时,直接push进链表即可,修改之后的Service层如下:

    //取值时
    @Override
    public List<MicroBlog> listAllMicroBlog() {
        List<MicroBlog>list = new ArrayList<>();
        if(redisUtils.lGet("AllMicroBlog",0,-1).size()!=0)
        {
            for(Object c: Objects.requireNonNull(redisUtils.lGet("AllMicroBlog",0,-1))) {
                list.add((MicroBlog) c);
            }
        }
        else
        {
            list=microBlogDao.listAllMicroBlog();
            for(MicroBlog microBlog: list) {
                redisUtils.lSet("AllMicroBlog",microBlog);
            }
        }
        return list;
    }

    //修改时可直接往链表中加数据
    public int saveMicroBlog(MicroBlog microBlog) {
        microBlog.setCreateTime(new Date());
        redisUtils.leftSet("AllMicroBlog",microBlog);
        return microBlogDao.saveMicroBlog(microBlog);
    }

  关键词AllMicroBlog对应一个存储所有日记的链表,在取值时,可直接从缓存中获得该链表,将其中每个对象强制转换后即可返回,同String一样,第一次去数据库中读并写入缓存,之后直接从缓存中取即可,同一类型的多个对象且关键词较短,也不需要访问指定对象时均可采取链表的形式存储,比如本项目中的日记和友链都是这么存储的。在需要修改或删除时,直接选择删除对应链表即可

  2.3.4 总结

  Redis作为一种内存数据库,效率大大高于MySQL,但也要求我们选择合适的方式去存储数据,而不是一味的使用String,关键字过多也会导致查询效率的下降,本项目中,博客内容采用了Hash存储,访问数量,评论数量采取了String存储,博客首页,日记,友链采取了List存储。在使用Redis过程中,最关键的是要明白何时该更新缓存,避免出现数据不一致的情况,修改数据时,应当确认与该数据相关的所有Redis关键字,并逐一确认是否需要修改,避免错误,我们也可以结合Java中的定时机制,选择一个无人访问的时间,清空Redis数据库,确保数据的一致性。本项目采用了每周清空一次缓存的策略,可以有效提高缓存数据的准确性

    //记得主函数添加@EnableScheduling注解
    @Scheduled(cron="0 0 4 ? * 4")//每周三四点执行一次
    public void clearCache()
    {
        redisUtils.clear();
    }

    //clear函数内容如下
     public void clear()
    {
        Set<String> keys = redisTemplate.keys("*");
        redisTemplate.delete(keys);
    }

三.下载链接

  Redis工具类在线下载: 请点击这儿下载
  Redis配置类在线下载: 请点击这儿下载

  如果您有任何疑问,请通过邮箱联系我

end
  • 作者:Yuan(联系作者)
  • 发表时间:2021-12-23 16:57
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载博主转载的文章,请附上原文链接
  • 公众号转载:请联系作者
  • 评论