Spring Data Redis + RabbitMQ - 基于 string 实现缓存、计数功能(同步数据)

目录

[一、Spring Data Redis](#一、Spring Data Redis)

1.1、缓存功能

1.1.1、分析

1.1.2、案例实现

1.1.3、效果演示

[1.2、计数功能(Redis + RabbitMQ)](#1.2、计数功能(Redis + RabbitMQ))

1.2.1、分析

1.2.2、案例实现


一、Spring Data Redis


1.1、缓存功能

1.1.1、分析

使用 redis 作为缓存, MySQL 作为数据库组成的架构

整体思路:

应用服务器访问数据的时候,先查询 Redis,如果 Redis 上存在该数据,就从 Redis 中取数据直接交给应用服务器,不用继续访问数据库了;如果 Redis 上不存在该数据,就会去 MySQL 中把读到的结构返回给应用服务器,同时,把这个数据也写入到 Redis 中.

由于 Redis 这样的缓存经常用来存储 "热点数据",也就是高频使用的数据,那什么样的数据算高频呢?这里暗含了一层假设,某个数据一旦被用到了,那么可能在最近这段时间就可能被反复用到.

随着时间推移,越来越多的 key 在 redis 上访问不到,那 redis 的数据不是越来越多么?

  1. 把数据写给 redis 的同时,会给这个 key 设置一个过期时间.
  2. Redis 也有内存不足的时候,因此提供了 淘汰策略(之前的文章展开讲过).

1.1.2、案例实现

例如论坛网站,有些帖子的访问评论很高,就需要设置成热点文章,缓存起来(比起去 MySQL 数据库中查询文章要快的多).

实现思路:

根据上面理论,暗含假设当前使用的文章就是热点文章,也就是说,如果在缓存中有该文章,就直接返回,如果没有,就去数据库中查,然后再缓存起来,同时设置 30min(不同场景合理分配) 的过期时间.

帖子实体类.

java 复制代码
@Data
public class Article {

    private String title;
    private String content;

}

文章 mapper.

java 复制代码
@Mapper
public interface ArticleMapper {

    /**
     * 根据 id 查询文章
     * @param id
     * @return
     */
    Article selectArticleById(@Param("id") Integer id);

}
java 复制代码
    <select id="selectArticleById" resultType="com.example.cyk.cache.Article">
        select * from article where id = #{id};
    </select>

帖子 controller

java 复制代码
@RestController
@RequestMapping("/article")
public class ArticleController {


    @Autowired
    private IArticleService articleService;

    @GetMapping("/get")
    public HashMap<String, Object> get(@NonNull Integer id) {
        //1.获取文章服务
        Article article = articleService.getArticleInfo(id);
        //2.返回响应
        return HandlerResponse(1000, "操作成功", article);
    }

    /**
     * 处理返回格式
     * @param code
     * @param msg
     * @param data
     * @return
     */
    private HashMap<String, Object> HandlerResponse(Integer code, String msg, Object data) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        result.put("data", data);
        return result;
    }

}

帖子 service .

java 复制代码
@Slf4j
@Service
public class ArticleService implements IArticleService {

    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Article getArticleInfo(Integer id) {
        //1.非空校验
        if(id == null) {
            log.warn("文章 id 为空");
            throw new RuntimeException("文章 id 为空");
        }
        //2.先去 redis 上看有没有文章对应的这个id
        //我这里约定 redis 上存储格式:
        //key: art:id
        //value: $title$content  ($ 是分隔符)
        //例如 key: art:1    value: $决定$今天要好好学习
        String articleInfo = redisTemplate.opsForValue().get("art:" + id);
        if(articleInfo != null) {
            //存在直接返回
            log.info("从 redis 中获取到文章数据");
            //1) 解析格式
            Article article = analysisArticle(articleInfo);
            //2) 返回数据
            return article;
        }
        //3.redis 上没有数据,因此需要从 mysql 中取
        Article article = articleMapper.selectArticleById(id);
        if(article == null) {
            log.warn("文章不存在");
            throw new RuntimeException("文章不存在!");
        }
        //4.将文章存到 redis 中
        //1) 合成 redis 所需格式的文章
        articleInfo = synthesisArticle(article);
        //2) 设置 5 分钟过期时间(为了演示效果)
        redisTemplate.opsForValue().set("art:" + id, articleInfo, 5, TimeUnit.SECONDS);
        log.info("从 mysql 中获取到文章数据");
        return article;
    }

    /**
     * 合成 redis 需要的格式(提前约定好的)
     * @param article
     * @return
     */
    private String synthesisArticle(Article article) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("$");
        stringBuilder.append(article.getTitle());
        stringBuilder.append("$");
        stringBuilder.append(article.getContent());
        return stringBuilder.toString();
    }

    /**
     * 解析文章格式
     * @param articleInfo
     * @return
     */
    private Article analysisArticle(String articleInfo) {
        Article article = new Article();
        String title = articleInfo.split("\\$")[1];
        String content = articleInfo.split("\\$")[2];
        article.setTitle(title);
        article.setContent(content);
        return article;
    }

}

1.1.3、效果演示

1.2、计数功能(Redis + RabbitMQ)

1.2.1、分析

许多都会使应用用 Redis 作为计数的基础⼯具,它可以实现快速计数、查询缓存的功能,例如网站视频的播放量,点赞数量......

Ps:这些都是相比较 MySQL 数据库而言的,Redis 可以通过简单的键值对操作完成计数任务并且实在内存中完成的,而 MySQL 就需要先查询数据库,然后 +1,然后再存入数据库,是在需要进行硬盘存储的

1.2.2、案例实现

实现思路:

假设,用户点击某个帖子,此时需要进行访问量 + 1 的操作,这时候应用服务器就会直接去操作 Redis ,执行 incr 命令,然后将返回的数据反馈给用户,最后 Redis 会以异步的方式(RabbitMQ 实现异步)将播放量同步到 MySQL 数据库中(异步就表示:这里并不是每一个播放请求,都需要立即写入数据~ 至于什么时候写入,需要根据实际的业务需求场景而定),将数据持久化.

Ps:实际中要开发⼀个成熟、稳定的真实计数系统,要⾯临的挑战远不⽌如此简单:防作弊、按 照不同维度计数、避免单点问题、数据持久化到底层数据源等。

文章实体类

java 复制代码
@Data
public class Article implements Serializable {

    private Integer id;
    private String title;
    private String content;
    private Long visits; //访问量

}

rabbit 交换机、队列、绑定配置.

java 复制代码
public class MqFinal {

    //处理文章的直接交换机
    public static final String UPDATE_DIRECT = "article.update.direct";
    //用于修改文章数据的队列
    public static final String UPDATE_QUEUE = "article.update.queue";
    //bindingKey
    public static final String UPDATE_KEY = "article.update.key";

}
java 复制代码
@Configuration
public class MqConfig {

    /**
     * 消息转化器
     * @return
     */
    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public DirectExchange ArticleDirectExchange() {
        return new DirectExchange(MqFinal.UPDATE_DIRECT, true, false);
    }

    @Bean
    public Queue ArticleUpdateQueue() {
        return new Queue(MqFinal.UPDATE_QUEUE, true);
    }

    @Bean
    public Binding ArticleUpdateBinding() {
        return BindingBuilder.bind(ArticleUpdateQueue()).to(ArticleDirectExchange()).with(MqFinal.UPDATE_KEY);
    }

}

mq 监听配置

java 复制代码
@Slf4j
@Component
public class MqListenerArticle {

    @Autowired
    private ArticleMapper articleMapper;

    /**
     * 同步数据库
     */
    @RabbitListener(queues = MqFinal.UPDATE_QUEUE)
    public void syncVisits(HashMap<String, Object> data) {
        Integer id = (Integer) data.get("id");
        // Rabbitmq 这里有一个问题,Map<String, Object> 中 Object 传入为 Long 类型,需要用 Integer 来接受,否则报错
        // 因此发送消息之前,体现将 Long 类型转化为 String,接收到消息之后只需要将 String 转化为 Long 即可
        String visits = (String) data.get("visits");
        articleMapper.updateArticleVisits(id, Long.valueOf(visits));
        log.info("访问量数据同步完成!");
    }

}

访问量增加服务(这里为了可读性,只展示了本业务的核心逻辑)

java 复制代码
    @Override
    public Article getArticleInfo(Integer id) {
        //1.非空校验
        if(id == null) {
            log.warn("文章 id 为空");
            throw new RuntimeException("文章 id 为空");
        }

        //2.访问量 +1
        //注意:incr 这个命令执行时,即使 key 不存在,也会自动生成 key,然后自增
        Long visits = redisTemplate.opsForValue().increment("v_art:" + id);
        //3.rabbitmq 实现异步数据同步(发送一个消息即可)
        HashMap<String, Object> visitsInfo = new HashMap<>();
        visitsInfo.put("id", id);
        visitsInfo.put("visits", visits.toString()); //转化原因前面解释过了
        rabbitTemplate.convertAndSend(MqFinal.UPDATE_DIRECT, MqFinal.UPDATE_KEY, visitsInfo);

        //4.获取文章数据
        //业务逻辑(这里为了可读性,就先不展示这里了)......

        //5.放入文章
        Article article = new Article();
        article.setVisits(visits);
        article.setId(id);
        return article;
    }
相关推荐
郝开1 小时前
Spring Boot 2.7.18(最终 2.x 系列版本)1 - 技术选型:连接池技术选型对比;接口文档技术选型对比
java·spring boot·spring
知兀2 小时前
【Spring/SpringBoot】SSM(Spring+Spring MVC+Mybatis)方案、各部分职责、与Springboot关系
java·spring boot·spring
向阳而生,一路生花2 小时前
redis离线安装
java·数据库·redis
hzk的学习笔记3 小时前
Redisson 的 Watchdog 机制
数据库·redis·分布式·缓存
伊布拉西莫3 小时前
Spring 6.x HTTP interface 使用说明
spring·restclient
hzk的学习笔记5 小时前
Redisson解锁失败,watchdog会不会一直续期下去?
数据库·redis·缓存
YDS8296 小时前
苍穹外卖 —— Spring Cache和购物车功能开发
java·spring boot·后端·spring·mybatis
bing.shao6 小时前
如何降低redis哈希值冲突概率
数据库·redis·哈希算法
Elieal6 小时前
Spring 框架核心技术全解析
java·spring·sqlserver
ckm紫韵6 小时前
redis查询速度快的原因?
数据库·redis·缓存