老司机使用 Redis 缓存复杂查询

2024-06-03 11:18

本文主要是介绍老司机使用 Redis 缓存复杂查询,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

2017-03-13 baya(jgm)-GitHub ITPUB

点击上方“蓝字”可以关注我们哦



|转载自:baya.github.io

|原文链接:https://baya.github.io/



最近上线了一个复杂的报表, 这个报表后面是一个几百行的 sql 查询,很不幸但又是预料之中, 这个 sql 查询性能非常低下,并且需要在网站的一个访问量非常大的页面显示这个 sql 的查询结果。幸运的是这个查询结果不需要
实时更新,只要每天更新一次即可, 于是为这个 sql 查询加上缓存就成为了一个很好的优化方法。开始我们使用 Rails.cache 来缓存这个查询结果,Rails.cache 的 backend 配置如下:

   

  # config/environments/production.rb

couch_host = YAML.load_file(Rails.root.join("config/couch.yml")).symbolize_keys[:host]

  config.cache_store = :mem_cache_store, couch_host , { :namespace => 'rails_cache' }

   

  # config/couch.yml

  host: xxx.xxx.xxx.xxx:xxx

   

从上面的代码可以看出我们使用了 couchdb 作为 Rails.cache 的 backend, 我开始不太清楚为什么会使用 couchdb, 因为我们的系统中已经使用了 Redis, 并且 Redis 无论是使用舒适度还是性能都不输 couchdb, 后来我打开 Gemfile 发现:


# Gemfile

gem 'rails', '3.0.9'

#gem 'redis-rails', '3.1.3'

#gem "redis-store", "~> 1.0.0"

   

我们看到 redis-rails 和 redis-store 都被加在了 Gemfile 里,然后又被注释掉了,由此我估计前面的同事也想使用 Redis, 但是由于我们的 Rails 版本过老(现在 Rails 5 都发布了,我们还在使用 Rails 3), 导致 redis-rails 和 redis-store 无法使用,而我们既不想冒升级 Rails 的风险(这个升级的跨度有点大了), 也不愿意花时间去改造 redis-store 使其兼容 Rails 3(每天改 ticket 已经让人心力交瘁了,这个借口让自己无法反驳)。 报表上线之初,没有什么问题,后来随着数据量变大,发现报表展示的速度变慢但由于还可以容忍,也就没有去花时间去研究速度变慢的原因,直到不久运行 couchdb 的机器莫名宕机,造成整个网站 502(前端请求拿不到缓存就会去读数据库,这个查询很耗时就一直挂着,请求量一大数据库就受不住了,导致整个网站不可访问), 这时候我们决定重新设计下这个报表的缓存。我们的设计如下:


使用 Redis 替换 couchdb, 主要原因是对 Redis 熟悉,并且系统中的很多异步队列服务用的是 Redis,非常稳定。


前端请求只能从 Redis 中读取已经被缓存的查询结果,而不能直接读数据库,如果缓存为空则返回空数组,这样做是为了防止数据库被大量的耗时请求拖垮,保证整个网站的可访问性。


缓存的过期时间设置为 999 天,其实就是缓存不过期的意思,并且写一个 rake task 每天运行一次用于更新 Redis 缓存。


这个设计的 2 和 3 步其实就是一个典型的生产&消费模式, rake task 作为生产者每天定时生成一次查询结果存入 Redis, 前端请求作为消费者通过读取 Redis 获得查询结果供页面展示。


有了设计我们并不急于编写代码,而是画一个设计图,一方面是为了梳理下思路看看设计是否会有缺陷,另一方面是为了更好地编写代码。


设计图如下:

通过设计图我们可以看到数据是单向流动的, 这样生产者和消费者是互不干扰的隔离状态, 前端请求不生产数据,只从 Redis 中拿数据,这样情况下前端请求对数据库的访问压力几乎为0。从设计图中我们也可以看出我们的代码大概会


分成三部分:


1、生产者的代码

2、Redis的代码(主要是读写 Redis)

3、消费者的代码


其中生产者和消费者都依赖 Redis 的代码, 因为两者都需要和 Redis 产生交互。


Redis 相关代码


前面说过由于我们使用的 Rails 版本过低, 将 Redis 整合到 Rails.cache 会是一件比较费力费时的事情,所以我们将直接使用 Reids。


首先配置 Redis,


  # config/redis_store.yml

  cache:

    host: xxx.xxx.xxx.xxx

    port: xxx

    db: 2

    driver: hiredis

    thread_safe: true

    timeout: 200 

   

  # config/initializers/redis.rb

  $redis = Redis.new(YAML.load_file("#{Rails.root}/config/redis_store.yml").symbolize_keys[:cache])

   

从上面的代码中可以看到我们定义了一个全局变量 $redis 用来访问 Redis。


接下来是将 $redis 封装到一个 service 中,这样做的目的是方便进行测试,也便于使用及以后的扩展。


class ReportCacheService

    def initialize(data = {})

      # 设置过期时间

      @expire_in = data[:expire_in]

      # 强制更新缓存,用于生产者生产数据

      @refresh_cache = data[:refresh_cache]

    end

    # block 里面是数据计算的过程

    def call(&block)

      if @refresh_cache == true

        res = nil

      else

        res = read()

      end

      if res.nil?

        res = block.call

        write(res)

      end

      res

    end

    # 读缓存

    def read

      value = $redis.get(@key)

      if value.present?

      JSON.parse(value)

      end

    end

    # 写缓存

    def write(res)

      value = res.to_json

      value = $redis.set(@key, value)

      $redis.expire(@key, @expire_in)

      true

    end

  end

   

这样我们就完成了 Redis 这部分的代码,接下来是生产者的代码。


生产者代码


我们将和报表相关的业务和逻辑封装到了一个叫 StmReport 的模型中, 我们为 StmReport 定义了一个 class 方法: warm_cache 用于生产报表数据。


  # app/models/stm_report.rb

  class StmReport

    NON_EXPIRED = 999.days.seconds

    def self.warm_cache(parasm = {})

      # refresh_cache: true 表示强制更新缓存,即生产数据

  # 生产的数据的过期时间是 999 天

      cs  = ReportCacheService.new(key: build_cache_key(params),

                                 refresh_cache: true,

                                 expire_in: NON_EXPIRED)

      cs.call do

    # do some heavy works

    ...

  end

    end

  end

   

接着我们编写一个 rake task, 并且使用 crontab 每天定时运行此 rake task 用于生产数据。


  # lib/tasks/cron.rake

  task :warn_stm_report_cache => :environment do

    puts "#{DateTime.now}: start cron warm cache"

    StmReport.warm_cache(qualified: true)

    StmReport.warm_cache(qualified: true, per_page: 50)

    puts "#{DateTime.now}: end cron warm cache"

  end

   

生产者的代码也完成,接下来是消费者的代码。


消费者的代码


在本文中,消费的过程即创建报表的过程, 创建报表的过程很自然地也封装到了 StmReport 模型中,


  class StmReport

    NON_EXPIRED = 999.days.seconds

    def self.create(params = {})

      cs  = ReportCacheService.new(key: build_cache_key(params))

      # 从缓存中读取数据

      items = cs.read

      # 如果缓存中没有数据即返回空数组,避免从数据库中查询数据

  items = [] if items.nil?

      # do some other works

  ...

    end

  end

   

这样整个实现就完成了,重新上线后的报表运行地非常稳定迅速,证明这个实现是成功的。


 我知道一种学习

于坚


这篇关于老司机使用 Redis 缓存复杂查询的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1026829

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

k8s按需创建PV和使用PVC详解

《k8s按需创建PV和使用PVC详解》Kubernetes中,PV和PVC用于管理持久存储,StorageClass实现动态PV分配,PVC声明存储需求并绑定PV,通过kubectl验证状态,注意回收... 目录1.按需创建 PV(使用 StorageClass)创建 StorageClass2.创建 PV

Redis 基本数据类型和使用详解

《Redis基本数据类型和使用详解》String是Redis最基本的数据类型,一个键对应一个值,它的功能十分强大,可以存储字符串、整数、浮点数等多种数据格式,本文给大家介绍Redis基本数据类型和... 目录一、Redis 入门介绍二、Redis 的五大基本数据类型2.1 String 类型2.2 Hash

MyBatis延迟加载与多级缓存全解析

《MyBatis延迟加载与多级缓存全解析》文章介绍MyBatis的延迟加载与多级缓存机制,延迟加载按需加载关联数据提升性能,一级缓存会话级默认开启,二级缓存工厂级支持跨会话共享,增删改操作会清空对应缓... 目录MyBATis延迟加载策略一对多示例一对多示例MyBatis框架的缓存一级缓存二级缓存MyBat

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三